diff --git a/.release-it.rc.json b/.release-it.beta.json similarity index 89% rename from .release-it.rc.json rename to .release-it.beta.json index a1030e611f..5dff68cbd7 100644 --- a/.release-it.rc.json +++ b/.release-it.beta.json @@ -1,5 +1,5 @@ { - "preReleaseId": "rc", + "preReleaseId": "beta", "git": { "requireCleanWorkingDir": false, "commit": false, @@ -11,7 +11,7 @@ }, "npm": { "skipChecks": true, - "tag": "rc" + "tag": "beta" }, "hooks": { "before:init": ["yarn", "yarn clean", "yarn test"] diff --git a/.vscode/launch.json b/.vscode/launch.json index 7074612e85..4253a1d439 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,7 @@ }, "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/babel-node", "port": 9229, "skipFiles": [ "/**" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0666644e24..258ff7e485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,257 @@ +## [0.10.6](https://github.com/payloadcms/payload/compare/v0.10.5...v0.10.6) (2021-09-30) + + +### Bug Fixes + +* allow debug in payload config ([65bf13d](https://github.com/payloadcms/payload/commit/65bf13d7c137eafdbbeadc1d36d26b7b8389088f)) +* relationship + new slate incompatibility ([f422053](https://github.com/payloadcms/payload/commit/f42205307e33916fc3b139f6ee97eb66d5d0816a)) + +## [0.10.5](https://github.com/payloadcms/payload/compare/v0.10.4...v0.10.5) (2021-09-28) + + +### Bug Fixes + +* ensures that fields within non-required groups are correctly not required ([1597055](https://github.com/payloadcms/payload/commit/15970550f7b00ce0527027c362a9550ff8ad5d2a)) +* index creation on localized field parent ([23e8197](https://github.com/payloadcms/payload/commit/23e81971eb94fd5b991aedb02aab84931937ae37)) +* pagination estimatedCount limited to near query ([73bd698](https://github.com/payloadcms/payload/commit/73bd69870c4ff8ae92053e77ef95cfae18c142b5)) + + +### Features + +* adds rich text editor upload element ([aa76950](https://github.com/payloadcms/payload/commit/aa769500c934f4dee51a24c0cfc0297c12b5ae47)) +* updates slate, finishes rte upload ([08db431](https://github.com/payloadcms/payload/commit/08db431c0c4626a0d10f4e1c7bca29fa075eedc6)) + +## [0.10.4](https://github.com/payloadcms/payload/compare/v0.10.0...v0.10.4) (2021-09-22) + + +### Bug Fixes + +* allows image resizing if either width or height is larger ([8661115](https://github.com/payloadcms/payload/commit/866111528377808009fa71595691e6a08ec77dc5)) +* array objects now properly save IDs ([2b8f925](https://github.com/payloadcms/payload/commit/2b8f925e81c58f6aa010bf13a318236f211ea091)) +* date field error message position ([03c0435](https://github.com/payloadcms/payload/commit/03c0435e3b3ecdfa0713e3e5026b80f8985ca290)) +* properly types optional req in local findByID ([02e7fe3](https://github.com/payloadcms/payload/commit/02e7fe3f1f3763f32f100cf2e5a8596aa16f3bd9)) + + +### Features + +* defaults empty group fields to empty object ([8a890fd](https://github.com/payloadcms/payload/commit/8a890fdc15b646c24963a1ef7584237b1d3c5783)) +* allows local update api to replace existing files with newly uploaded ones ([dbbff4c](https://github.com/payloadcms/payload/commit/dbbff4cfa41aa20077e47c8c7b87d4d00683c571)) +* exposes Pill component for re-use ([7e8df10](https://github.com/payloadcms/payload/commit/7e8df100bbf86798de292466afd4c00c455ecb35)) +* performance improvement while saving large docs ([901ad49](https://github.com/payloadcms/payload/commit/901ad498b47bcb8ae995ade18f2fc08cd33f0645)) + +# [0.10.0](https://github.com/payloadcms/payload/compare/v0.9.5...v0.10.0) (2021-09-09) + + +### Bug Fixes + +* admin UI collection id is required ([dc96b90](https://github.com/payloadcms/payload/commit/dc96b90cba01756374dde5b91f7702e0a0c661aa)) +* allow save of collection with an undefined point ([f80646c](https://github.com/payloadcms/payload/commit/f80646c5987db4c228b00beda9549259021c2a40)) +* config validation correctly prevents empty strings for option values ([41e7feb](https://github.com/payloadcms/payload/commit/41e7febf6a21d2fff39a335c033d9e9582294147)) +* ensures hooks run before access ([96629f1](https://github.com/payloadcms/payload/commit/96629f1f0100efdb9c5ad57c1a46add3c15ea65d)) +* ensures proper order while transforming incoming and outgoing data ([c187da0](https://github.com/payloadcms/payload/commit/c187da00b1f18c66d9252a5a3e2029455d75b371)) +* improve id type semantic and restrict possible types to text and number ([29529b2](https://github.com/payloadcms/payload/commit/29529b2c56d4af7c6abce113da2f7ce84f1dcc02)) +* remove media directory to improve test run consistency ([d42d8f7](https://github.com/payloadcms/payload/commit/d42d8f76efcda7a24f2f50d60caf47b1027d81f6)) +* sanitize custom id number types ([c7558d8](https://github.com/payloadcms/payload/commit/c7558d8652780e24479b39e5f2a08a49ffff3358)) +* sort id columns ([114dc1b](https://github.com/payloadcms/payload/commit/114dc1b3fb9a1895e09671aca7a57fd5c7d84911)) + + +### Features + +* add config validation for collections with custom id ([fe1dc0b](https://github.com/payloadcms/payload/commit/fe1dc0b191e73f350b77a90887d8172bf76d46fd)) +* add config validation for collections with custom id ([d0aaf4a](https://github.com/payloadcms/payload/commit/d0aaf4a4128ad585013c392bb608f586985b71ad)) +* add point field type ([7504155](https://github.com/payloadcms/payload/commit/7504155e17a2881b7a60f49e610c062665b46d21)) +* allows user to pass req through local findByID ([8675481](https://github.com/payloadcms/payload/commit/8675481343ef45fefc2eaaea939eda8ed0a2577f)) +* frontend polish to point field ([64ad6a3](https://github.com/payloadcms/payload/commit/64ad6a30a56969127dfb592a7e0c8807e9f3d8f7)) +* graphql support for custom id types ([bc2a6e1](https://github.com/payloadcms/payload/commit/bc2a6e15753c62d2041e9afded3f1ca040dbffa3)) +* point field localization and graphql ([30f1750](https://github.com/payloadcms/payload/commit/30f17509ea9927d923ffd42c703adefc902b66ea)) +* replace the collection idType option with an explicit id field ([4b70a12](https://github.com/payloadcms/payload/commit/4b70a1225f834ecd0aab50c6e92ad50572389962)) +* support custom ids ([3cc921a](https://github.com/payloadcms/payload/commit/3cc921acc92e1b4a372468b644b7e676400d9c26)) + +## [0.9.5](https://github.com/payloadcms/payload/compare/v0.9.4...v0.9.5) (2021-08-23) + + +### Bug Fixes + +* obscure conditional logic bug ([b0dc125](https://github.com/payloadcms/payload/commit/b0dc12560423af5083d36cfd16f464f08ab66d9d)) +* windows compatible absolute paths for staticDir ([b21316b](https://github.com/payloadcms/payload/commit/b21316b6cc392c793614024648c5301c7e03c326)) + +## [0.9.4](https://github.com/payloadcms/payload/compare/v0.9.3...v0.9.4) (2021-08-06) + +## [0.9.3](https://github.com/payloadcms/payload/compare/v0.9.2...v0.9.3) (2021-08-06) + + +### Bug Fixes + +* args no longer optional in collection and global hooks ([a5ea0ff](https://github.com/payloadcms/payload/commit/a5ea0ff61945f3da106f0d9dbb6a90fb1d884061)) + +## [0.9.2](https://github.com/payloadcms/payload/compare/v0.9.1...v0.9.2) (2021-08-06) + + +### Bug Fixes + +* row admin type ([deef520](https://github.com/payloadcms/payload/commit/deef5202c15301b685fe5efc8a6ff59b012ea1d4)) + + +### Features + +* allow completely disabling local file storage ([9661c6d](https://github.com/payloadcms/payload/commit/9661c6d40acc41d21eebc42b0cc1871f28d35a73)) +* allows upload resizing to maintain aspect ratio ([dea54a4](https://github.com/payloadcms/payload/commit/dea54a4cccead86e6ffc9f20457f295e1c08405b)) +* exposes auto-sized uploads on payload req ([9c8935f](https://github.com/payloadcms/payload/commit/9c8935fd51439627cccf3f6625236375f5909445)) +* reduces group heading from h2 to h3 ([907f8fd](https://github.com/payloadcms/payload/commit/907f8fd94d7e6cfa7eac0040c134cc714f29800d)) + +## [0.9.1](https://github.com/payloadcms/payload/compare/v0.9.0...v0.9.1) (2021-08-03) + + +### Bug Fixes + +* groups with failing conditions being incorrectly required on backend ([4cc0ea1](https://github.com/payloadcms/payload/commit/4cc0ea1d81cd7579cb330091eb111a27262ff031)) +* relationship field access control in admin UI ([65db8d9](https://github.com/payloadcms/payload/commit/65db8d9fc2c8b556cc284966b9b69f5d6512aca5)) + + +### Features + +* exposes collection after read hook type ([01a191a](https://github.com/payloadcms/payload/commit/01a191a13967d98ebf57891efd21b2607804e4e3)) + +# [0.9.0](https://github.com/payloadcms/payload/compare/v0.8.2...v0.9.0) (2021-08-02) + +### BREAKING CHANGES + +* Due to greater plugin possibilities and performance enhancements, plugins themselves no longer accept a completely sanitized config. Instead, they accept a _validated_ config as-provided, but sanitization is now only performed after all plugins have been initialized. By config santitization, we refer to merging in default values and ensuring that the config has its full, required shape. What this now means for plugins is that within plugin code, deeply nested properties like `config.graphQL.mutations` will need to be accessed safely (optional chaining is great for this), because a user's config may not have defined `config.graphQL`. So, the only real breaking change here is are that plugins now need to safely access properties from an incoming config. + +### Features + +* removes sanitization of configs before plugins are instantiated ([8af3947](https://github.com/payloadcms/payload/commit/8af39472e19a26453647d1c1ab0bbce15db2c642)) + +## [0.8.2](https://github.com/payloadcms/payload/compare/v0.8.1...v0.8.2) (2021-08-02) + + +### Bug Fixes + +* more advanced conditional logic edge cases ([33983de](https://github.com/payloadcms/payload/commit/33983deb3761813506348f8ff804a2117d1324ef)) + + +### Features + +* export error types ([12cba62](https://github.com/payloadcms/payload/commit/12cba62930b8d35b22e3a7a99cf06df29bd4964a)) + +## [0.8.1](https://github.com/payloadcms/payload/compare/v0.8.0...v0.8.1) (2021-07-29) + + +### BREAKING CHANGES + +* If you have any plugins that are written in TypeScript, we have changed plugin types to make them more flexible. Whereas before you needed to take in a fully sanitized config, and return a fully sanitized config, we now have simplified that requirement so that you can write configs in your own plugins just as an end user of Payload can write their own configs. + +Now, configs will be sanitized **_before_** plugins are executed **_as well as_** after plugins are executed. + +So, where your plugin may have been typed like this before: + +```ts + import { SanitizedConfig } from 'payload/config'; + + const plugin = (config: SanitizedConfig): SanitizedConfig => { + return { + ...config, + } + } +``` + +It can now be written like this: + +```ts + import { Config } from 'payload/config'; + + const plugin = (config: Config): Config => { + return { + ...config, + } + } +``` + +### Features + +* improves plugin writability ([a002b71](https://github.com/payloadcms/payload/commit/a002b7105f5c312e846c80032a350046db10236c)) + +# [0.8.0](https://github.com/payloadcms/payload/compare/v0.7.10...v0.8.0) (2021-07-28) + +### BREAKING CHANGES + +* There have been a few very minor, yet breaking TypeScript changes in this release. If you are accessing Payload config types from directly within the `dist` folder, like any of the following: + +- `import { PayloadCollectionConfig, CollectionConfig } from 'payload/dist/collections/config/types';` +- `import { PayloadGlobalConfig, GlobalConfig } from 'payload/dist/globals/config/types';` +- `import { Config, PayloadConfig } from 'payload/config';` + +You may need to modify your code to work with this release. The TL;DR of the change is that we have improved our naming conventions of internally used types, which will become more important over time. Now, we have landed on a naming convention as follows: + +- Incoming configs, typed correctly for optional / required config properties, are named `Config`, `CollectionConfig`, and `GlobalConfig`. +- Fully defaulted, sanitized, and validated configs are now named `SanitizedConfig`, `SanitizedCollectionConfig`, and `SanitizedGlobalConfig`. + +They can be imported safely outside of the `dist` folder now as well. For more information on how to properly import which types you need, see the following Docs pages which have now been updated with examples on how to properly access the new types: + +- [Base Payload config docs](https://payloadcms.com/docs/configuration/overview) +- [Collection config docs](https://payloadcms.com/docs/configuration/collections) +- [Global config docs](https://payloadcms.com/docs/configuration/globals) + +### Bug Fixes + +* ensures text component is always controlled ([c649362](https://github.com/payloadcms/payload/commit/c649362b95f1ddaeb47cb121b814ca30712dea86)) + + +### Features + +* revises naming conventions of config types ([5a7e5b9](https://github.com/payloadcms/payload/commit/5a7e5b921d7803ec2da8cc3dc8162c1dd6828ca0)) + +## [0.7.10](https://github.com/payloadcms/payload/compare/v0.7.9...v0.7.10) (2021-07-27) + + +### Bug Fixes + +* jest debug testing ([a2fa30f](https://github.com/payloadcms/payload/commit/a2fa30fad2cd9b8ab6ac4f3905706b97d5663954)) +* skipValidation logic ([fedeaea](https://github.com/payloadcms/payload/commit/fedeaeafc9607f7c21e40c2df44923056e5d460c)) + + +### Features + +* improves conditional logic performance and edge cases ([d43390f](https://github.com/payloadcms/payload/commit/d43390f2a4c5ebeb7b9b0f07e003816005efc761)) + +## [0.7.9](https://github.com/payloadcms/payload/compare/v0.7.8...v0.7.9) (2021-07-27) + + +### Bug Fixes + +* missing richtext gutter ([4d1249d](https://github.com/payloadcms/payload/commit/4d1249dd03f441ee872e66437118c3e8703aaefc)) + + +### Features + +* add admin description to collections and globals ([4544711](https://github.com/payloadcms/payload/commit/4544711f0e4ea0e78570b93717a4bf213946d9b3)) +* add collection slug to schema validation errors ([ebfb72c](https://github.com/payloadcms/payload/commit/ebfb72c8fa0723ec75922c6fa4739b48ee82b29f)) +* add component support to collection and global description ([fe0098c](https://github.com/payloadcms/payload/commit/fe0098ccd9b3477b47985222659a0e3fc2e7bb3b)) +* add component support to field description ([e0933f6](https://github.com/payloadcms/payload/commit/e0933f612a70af0a18c88ef96e7af0878e20cf01)) +* add customizable admin field descriptions ([dac60a0](https://github.com/payloadcms/payload/commit/dac60a024b0eb7197d5a501daea342827ee7c751)) +* add descriptions to every allowed field type, globals and collections ([29a1108](https://github.com/payloadcms/payload/commit/29a1108518c7942f8ae06a990393a6e0ad4b6b16)) +* add global slug and field names to schema validation errors ([bb63b4a](https://github.com/payloadcms/payload/commit/bb63b4aad153d125f68bf1fe1e9a3e4a5358ded9)) +* improves group styling when there is no label ([ea358a6](https://github.com/payloadcms/payload/commit/ea358a66e8b8d2e54dd162eae0cf7066128cfabf)) + +## [0.7.8](https://github.com/payloadcms/payload/compare/v0.7.7...v0.7.8) (2021-07-23) + + +### Features + +* fixes group label schema validation ([cbac888](https://github.com/payloadcms/payload/commit/cbac8887ddb7a4446f5502c577d4600905b13380)) + +## [0.7.7](https://github.com/payloadcms/payload/compare/v0.7.6...v0.7.7) (2021-07-23) + + +### Bug Fixes + +* accurately documents the props for the datepicker field ([dcd8052](https://github.com/payloadcms/payload/commit/dcd8052498dd2900f228eaffcf6142b63e8e5a9b)) + + +### Features + +* only attempts to find config when payload is initialized ([266ccb3](https://github.com/payloadcms/payload/commit/266ccb374449b0a131a574d9b12275b6bb7e5c60)) + ## [0.7.6](https://github.com/payloadcms/payload/compare/v0.7.5...v0.7.6) (2021-07-07) ## [0.7.5](https://github.com/payloadcms/payload/compare/v0.7.4...v0.7.5) (2021-07-07) diff --git a/demo/collections/Admin.ts b/demo/collections/Admin.ts index 9fdef8f80b..d89783ee78 100644 --- a/demo/collections/Admin.ts +++ b/demo/collections/Admin.ts @@ -1,4 +1,4 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import roles from '../access/roles'; import checkRole from '../access/checkRole'; @@ -7,7 +7,7 @@ const access = ({ req: { user } }) => { return result; }; -const Admin: PayloadCollectionConfig = { +const Admin: CollectionConfig = { slug: 'admins', labels: { singular: 'Admin', diff --git a/demo/collections/AllFields.ts b/demo/collections/AllFields.ts index 92a1f7348d..fe92c2ed26 100644 --- a/demo/collections/AllFields.ts +++ b/demo/collections/AllFields.ts @@ -1,11 +1,12 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import checkRole from '../access/checkRole'; import Email from '../blocks/Email'; import Quote from '../blocks/Quote'; import NumberBlock from '../blocks/Number'; import CallToAction from '../blocks/CallToAction'; +import CollectionDescription from '../customComponents/CollectionDescription'; -const AllFields: PayloadCollectionConfig = { +const AllFields: CollectionConfig = { slug: 'all-fields', labels: { singular: 'All Fields', @@ -22,6 +23,7 @@ const AllFields: PayloadCollectionConfig = { return null; }, + description: CollectionDescription, }, access: { read: () => true, @@ -40,11 +42,33 @@ const AllFields: PayloadCollectionConfig = { read: ({ req: { user } }) => Boolean(user), }, }, + { + name: 'descriptionText', + type: 'text', + label: 'Text with text description', + defaultValue: 'Default Value', + admin: { + description: 'This text describes the field', + }, + }, + { + name: 'descriptionFunction', + type: 'text', + label: 'Text with function description', + defaultValue: 'Default Value', + maxLength: 20, + admin: { + description: ({ value }) => (typeof value === 'string' ? `${20 - value.length} characters left` : ''), + }, + }, { name: 'image', type: 'upload', label: 'Image', relationTo: 'media', + admin: { + description: 'No selfies', + }, }, { name: 'select', @@ -91,6 +115,7 @@ const AllFields: PayloadCollectionConfig = { name: 'dayOnlyDateFieldExample', label: 'Day Only', type: 'date', + required: true, admin: { date: { pickerAppearance: 'dayOnly', @@ -226,6 +251,9 @@ const AllFields: PayloadCollectionConfig = { label: 'Relationship to One Collection', name: 'relationship', relationTo: 'conditions', + admin: { + description: 'Relates to description', + }, }, { type: 'relationship', @@ -244,6 +272,9 @@ const AllFields: PayloadCollectionConfig = { type: 'textarea', label: 'Textarea', name: 'textarea', + admin: { + description: 'Hello textarea description', + }, }, { name: 'richText', diff --git a/demo/collections/AutoLabel.ts b/demo/collections/AutoLabel.ts index cac61262f3..1dce0679bc 100644 --- a/demo/collections/AutoLabel.ts +++ b/demo/collections/AutoLabel.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const AutoLabel: PayloadCollectionConfig = { +const AutoLabel: CollectionConfig = { slug: 'auto-label', admin: { useAsTitle: 'autoLabelField', diff --git a/demo/collections/Blocks.ts b/demo/collections/Blocks.ts index 7c9a27ddd2..8bb338a8c3 100644 --- a/demo/collections/Blocks.ts +++ b/demo/collections/Blocks.ts @@ -1,10 +1,10 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import Email from '../blocks/Email'; import Quote from '../blocks/Quote'; import NumberBlock from '../blocks/Number'; import CallToAction from '../blocks/CallToAction'; -const Blocks: PayloadCollectionConfig = { +const Blocks: CollectionConfig = { slug: 'blocks', labels: { singular: 'Blocks', diff --git a/demo/collections/Code.ts b/demo/collections/Code.ts index 3f473ba879..92593d0a23 100644 --- a/demo/collections/Code.ts +++ b/demo/collections/Code.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const Code: PayloadCollectionConfig = { +const Code: CollectionConfig = { slug: 'code', labels: { singular: 'Code', @@ -14,6 +14,7 @@ const Code: PayloadCollectionConfig = { required: true, admin: { language: 'js', + description: 'javascript example', }, }, ], diff --git a/demo/collections/Conditions.ts b/demo/collections/Conditions.ts index 45e90687de..8bdc63e707 100644 --- a/demo/collections/Conditions.ts +++ b/demo/collections/Conditions.ts @@ -1,10 +1,10 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import Email from '../blocks/Email'; import Quote from '../blocks/Quote'; import NumberBlock from '../blocks/Number'; import CallToAction from '../blocks/CallToAction'; -const Conditions: PayloadCollectionConfig = { +const Conditions: CollectionConfig = { slug: 'conditions', labels: { singular: 'Conditions', diff --git a/demo/collections/CustomComponents/index.ts b/demo/collections/CustomComponents/index.ts index d4fb667354..2320a8bc1a 100644 --- a/demo/collections/CustomComponents/index.ts +++ b/demo/collections/CustomComponents/index.ts @@ -1,4 +1,4 @@ -import { PayloadCollectionConfig } from '../../../src/collections/config/types'; +import { CollectionConfig } from '../../../src/collections/config/types'; import DescriptionField from './components/fields/Description/Field'; import DescriptionCell from './components/fields/Description/Cell'; import DescriptionFilter from './components/fields/Description/Filter'; @@ -7,8 +7,9 @@ import GroupField from './components/fields/Group/Field'; import NestedGroupField from './components/fields/NestedGroupCustomField/Field'; import NestedText1Field from './components/fields/NestedText1/Field'; import ListView from './components/views/List'; +import CustomDescriptionComponent from '../../customComponents/Description'; -const CustomComponents: PayloadCollectionConfig = { +const CustomComponents: CollectionConfig = { slug: 'custom-components', labels: { singular: 'Custom Component', @@ -38,6 +39,14 @@ const CustomComponents: PayloadCollectionConfig = { }, }, }, + { + name: 'componentDescription', + label: 'Component ViewDescription', + type: 'text', + admin: { + description: CustomDescriptionComponent, + }, + }, { name: 'array', label: 'Array', diff --git a/demo/collections/CustomID.ts b/demo/collections/CustomID.ts new file mode 100644 index 0000000000..3bd8487bc6 --- /dev/null +++ b/demo/collections/CustomID.ts @@ -0,0 +1,22 @@ +import { CollectionConfig } from '../../src/collections/config/types'; + +const CustomID: CollectionConfig = { + slug: 'custom-id', + labels: { + singular: 'CustomID', + plural: 'CustomIDs', + }, + fields: [ + { + name: 'id', + type: 'number', + }, + { + name: 'name', + type: 'text', + required: true, + }, + ], +}; + +export default CustomID; diff --git a/demo/collections/DefaultValues.ts b/demo/collections/DefaultValues.ts index 2274fc5b62..0ca5882a30 100644 --- a/demo/collections/DefaultValues.ts +++ b/demo/collections/DefaultValues.ts @@ -1,11 +1,11 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import checkRole from '../access/checkRole'; import Email from '../blocks/Email'; import Quote from '../blocks/Quote'; import NumberBlock from '../blocks/Number'; import CallToAction from '../blocks/CallToAction'; -const DefaultValues: PayloadCollectionConfig = { +const DefaultValues: CollectionConfig = { slug: 'default-values', labels: { singular: 'Default Value Test', diff --git a/demo/collections/File.ts b/demo/collections/File.ts index be27ed319d..d7957a86c5 100644 --- a/demo/collections/File.ts +++ b/demo/collections/File.ts @@ -1,4 +1,4 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import checkRole from '../access/checkRole'; const access = ({ req: { user } }) => { @@ -19,7 +19,7 @@ const access = ({ req: { user } }) => { return false; }; -const Files: PayloadCollectionConfig = { +const Files: CollectionConfig = { slug: 'files', labels: { singular: 'File', diff --git a/demo/collections/Geolocation.ts b/demo/collections/Geolocation.ts new file mode 100644 index 0000000000..e9adffa5f1 --- /dev/null +++ b/demo/collections/Geolocation.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-param-reassign */ +import { CollectionConfig } from '../../src/collections/config/types'; + +const validateFieldTransformAction = (hook: string, value) => { + if (value !== undefined && value !== null && !Array.isArray(value)) { + console.error(hook, value); + throw new Error('Field transformAction should convert value to array [x, y] and not { coordinates: [x, y] }'); + } + return value; +}; + +const Geolocation: CollectionConfig = { + slug: 'geolocation', + labels: { + singular: 'Geolocation', + plural: 'Geolocations', + }, + access: { + read: () => true, + }, + hooks: { + beforeRead: [ + (operation) => operation.doc, + ], + beforeChange: [ + (operation) => { + // eslint-disable-next-line no-param-reassign,operator-assignment + operation.data.beforeChange = !operation.data.location?.coordinates; + return operation.data; + }, + ], + afterRead: [ + (operation) => { + const { doc } = operation; + doc.afterReadHook = !doc.location?.coordinates; + return doc; + }, + ], + afterChange: [ + (operation) => { + const { doc } = operation; + doc.afterChangeHook = !doc.location?.coordinates; + return doc; + }, + ], + afterDelete: [ + (operation) => { + const { doc } = operation; + operation.doc.afterDeleteHook = !doc.location?.coordinates; + return doc; + }, + ], + }, + fields: [ + { + name: 'location', + type: 'point', + label: 'Location', + hooks: { + beforeValidate: [({ value }) => validateFieldTransformAction('beforeValidate', value)], + beforeChange: [({ value }) => validateFieldTransformAction('beforeChange', value)], + afterChange: [({ value }) => validateFieldTransformAction('afterChange', value)], + afterRead: [({ value }) => validateFieldTransformAction('afterRead', value)], + }, + }, + { + name: 'localizedPoint', + type: 'point', + label: 'Localized Point', + localized: true, + hooks: { + beforeValidate: [({ value }) => validateFieldTransformAction('beforeValidate', value)], + beforeChange: [({ value }) => validateFieldTransformAction('beforeChange', value)], + afterChange: [({ value }) => validateFieldTransformAction('afterChange', value)], + afterRead: [({ value }) => validateFieldTransformAction('afterRead', value)], + }, + }, + ], +}; + +export default Geolocation; diff --git a/demo/collections/HiddenFields.ts b/demo/collections/HiddenFields.ts index 7f71314157..98da52aa44 100644 --- a/demo/collections/HiddenFields.ts +++ b/demo/collections/HiddenFields.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const HiddenFields: PayloadCollectionConfig = { +const HiddenFields: CollectionConfig = { slug: 'hidden-fields', labels: { singular: 'Hidden Fields', diff --git a/demo/collections/Hooks.ts b/demo/collections/Hooks.ts index 94ee285896..89aa279872 100644 --- a/demo/collections/Hooks.ts +++ b/demo/collections/Hooks.ts @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const Hooks: PayloadCollectionConfig = { +const Hooks: CollectionConfig = { slug: 'hooks', labels: { singular: 'Hook', diff --git a/demo/collections/LocalOperations.ts b/demo/collections/LocalOperations.ts index 85fa205e59..2f2a2ba4e1 100644 --- a/demo/collections/LocalOperations.ts +++ b/demo/collections/LocalOperations.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const LocalOperations: PayloadCollectionConfig = { +const LocalOperations: CollectionConfig = { slug: 'local-operations', labels: { singular: 'Local Operation', diff --git a/demo/collections/Localized.ts b/demo/collections/Localized.ts index a2b561fcff..2b0f173d94 100644 --- a/demo/collections/Localized.ts +++ b/demo/collections/Localized.ts @@ -1,6 +1,15 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; +import { PayloadRequest } from '../../src/express/types'; import { Block } from '../../src/fields/config/types'; +const validateLocalizationTransform = (hook: string, value, req: PayloadRequest) => { + if (req.locale !== 'all' && value !== undefined && typeof value !== 'string') { + console.error(hook, value); + throw new Error('Locale transformation should happen before hook is called'); + } + return value; +}; + const RichTextBlock: Block = { slug: 'richTextBlock', labels: { @@ -19,7 +28,7 @@ const RichTextBlock: Block = { ], }; -const LocalizedPosts: PayloadCollectionConfig = { +const LocalizedPosts: CollectionConfig = { slug: 'localized-posts', labels: { singular: 'Localized Post', @@ -46,6 +55,12 @@ const LocalizedPosts: PayloadCollectionConfig = { required: true, unique: true, localized: true, + hooks: { + beforeValidate: [({ value, req }) => validateLocalizationTransform('beforeValidate', value, req)], + beforeChange: [({ value, req }) => validateLocalizationTransform('beforeChange', value, req)], + afterChange: [({ value, req }) => validateLocalizationTransform('afterChange', value, req)], + afterRead: [({ value, req }) => validateLocalizationTransform('afterRead', value, req)], + }, }, { name: 'summary', diff --git a/demo/collections/LocalizedArray.ts b/demo/collections/LocalizedArray.ts index c37d404e37..f3881d9893 100644 --- a/demo/collections/LocalizedArray.ts +++ b/demo/collections/LocalizedArray.ts @@ -1,4 +1,4 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import { FieldAccess } from '../../src/fields/config/types'; import checkRole from '../access/checkRole'; @@ -12,7 +12,7 @@ const PublicReadabilityAccess: FieldAccess = ({ req: { user }, siblingData }) => return false; }; -const LocalizedArrays: PayloadCollectionConfig = { +const LocalizedArrays: CollectionConfig = { slug: 'localized-arrays', labels: { singular: 'Localized Array', diff --git a/demo/collections/Media.ts b/demo/collections/Media.ts index 16920ea69d..ae2962cf45 100644 --- a/demo/collections/Media.ts +++ b/demo/collections/Media.ts @@ -1,6 +1,17 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig, BeforeChangeHook } from '../../src/collections/config/types'; -const Media: PayloadCollectionConfig = { +const checkForUploadSizesHook: BeforeChangeHook = ({ req: { payloadUploadSizes }, data }) => { + if (typeof payloadUploadSizes === 'object') { + return { + ...data, + foundUploadSizes: true, + }; + } + + return data; +}; + +const Media: CollectionConfig = { slug: 'media', labels: { singular: 'Media', @@ -11,12 +22,24 @@ const Media: PayloadCollectionConfig = { }, admin: { enableRichTextRelationship: true, + description: 'No selfies please', + }, + hooks: { + beforeChange: [ + checkForUploadSizesHook, + ], }, upload: { staticURL: '/media', staticDir: './media', adminThumbnail: ({ doc }) => `/media/${doc.filename}`, imageSizes: [ + { + name: 'maintainedAspectRatio', + width: 1024, + height: null, + crop: 'center', + }, { name: 'tablet', width: 640, @@ -44,6 +67,10 @@ const Media: PayloadCollectionConfig = { required: true, localized: true, }, + { + name: 'foundUploadSizes', + type: 'checkbox', + }, ], timestamps: true, }; diff --git a/demo/collections/NestedArrays.ts b/demo/collections/NestedArrays.ts index 0c2b8a4697..fc597d0509 100644 --- a/demo/collections/NestedArrays.ts +++ b/demo/collections/NestedArrays.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const NestedArray: PayloadCollectionConfig = { +const NestedArray: CollectionConfig = { slug: 'nested-arrays', labels: { singular: 'Nested Array', diff --git a/demo/collections/Preview.ts b/demo/collections/Preview.ts index abcfb2f12b..81c7c241fc 100644 --- a/demo/collections/Preview.ts +++ b/demo/collections/Preview.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const Preview: PayloadCollectionConfig = { +const Preview: CollectionConfig = { slug: 'previewable-post', labels: { singular: 'Previewable Post', diff --git a/demo/collections/PublicUsers.ts b/demo/collections/PublicUsers.ts index 2f6b3c2845..996b7fa382 100644 --- a/demo/collections/PublicUsers.ts +++ b/demo/collections/PublicUsers.ts @@ -1,9 +1,9 @@ import checkRole from '../access/checkRole'; -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; const access = ({ req: { user } }) => checkRole(['admin'], user); -const PublicUsers: PayloadCollectionConfig = { +const PublicUsers: CollectionConfig = { slug: 'public-users', labels: { singular: 'Public User', diff --git a/demo/collections/RelationshipA.ts b/demo/collections/RelationshipA.ts index 986e66a69f..8cd3da09b6 100644 --- a/demo/collections/RelationshipA.ts +++ b/demo/collections/RelationshipA.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const RelationshipA: PayloadCollectionConfig = { +const RelationshipA: CollectionConfig = { slug: 'relationship-a', access: { read: () => true, @@ -29,7 +29,7 @@ const RelationshipA: PayloadCollectionConfig = { name: 'postLocalizedMultiple', label: 'Localized Post Multiple', type: 'relationship', - relationTo: ['localized-posts', 'all-fields'], + relationTo: ['localized-posts', 'all-fields', 'custom-id'], hasMany: true, localized: true, }, @@ -49,6 +49,14 @@ const RelationshipA: PayloadCollectionConfig = { relationTo: 'relationship-b', hasMany: false, }, + { + name: 'customID', + label: 'CustomID Relation', + type: 'relationship', + relationTo: 'custom-id', + hasMany: true, + localized: true, + }, ], timestamps: true, }; diff --git a/demo/collections/RelationshipB.ts b/demo/collections/RelationshipB.ts index f8203f349b..9b77ab92ea 100644 --- a/demo/collections/RelationshipB.ts +++ b/demo/collections/RelationshipB.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const RelationshipB: PayloadCollectionConfig = { +const RelationshipB: CollectionConfig = { slug: 'relationship-b', access: { read: () => true, diff --git a/demo/collections/RichText.ts b/demo/collections/RichText.ts index 632e8bbef0..e51996dfaa 100644 --- a/demo/collections/RichText.ts +++ b/demo/collections/RichText.ts @@ -1,8 +1,8 @@ import Button from '../client/components/richText/elements/Button'; import PurpleBackground from '../client/components/richText/leaves/PurpleBackground'; -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const RichText: PayloadCollectionConfig = { +const RichText: CollectionConfig = { slug: 'rich-text', labels: { singular: 'Rich Text', diff --git a/demo/collections/Select.ts b/demo/collections/Select.ts index ba04d84121..aa85c5cce3 100644 --- a/demo/collections/Select.ts +++ b/demo/collections/Select.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const Select: PayloadCollectionConfig = { +const Select: CollectionConfig = { slug: 'select', labels: { singular: 'Select', diff --git a/demo/collections/StrictPolicies.ts b/demo/collections/StrictPolicies.ts index 86ff0adc25..2164d877f0 100644 --- a/demo/collections/StrictPolicies.ts +++ b/demo/collections/StrictPolicies.ts @@ -1,7 +1,7 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; import checkRole from '../access/checkRole'; -const StrictAccess: PayloadCollectionConfig = { +const StrictAccess: CollectionConfig = { slug: 'strict-access', labels: { singular: 'Strict Access', diff --git a/demo/collections/Uniques.ts b/demo/collections/Uniques.ts index 2438397add..1ac59a60d4 100644 --- a/demo/collections/Uniques.ts +++ b/demo/collections/Uniques.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const Uniques: PayloadCollectionConfig = { +const Uniques: CollectionConfig = { slug: 'uniques', labels: { singular: 'Unique', diff --git a/demo/collections/UnstoredMedia.ts b/demo/collections/UnstoredMedia.ts new file mode 100644 index 0000000000..2ef589f0e1 --- /dev/null +++ b/demo/collections/UnstoredMedia.ts @@ -0,0 +1,35 @@ +import { CollectionConfig } from '../../src/collections/config/types'; + +const UnstoredMedia: CollectionConfig = { + slug: 'unstored-media', + labels: { + singular: 'Unstored Media', + plural: 'Unstored Media', + }, + access: { + read: () => true, + }, + upload: { + staticURL: '/unstored-media', + disableLocalStorage: true, + imageSizes: [ + { + name: 'tablet', + width: 640, + height: 480, + crop: 'left top', + }, + ], + }, + fields: [ + { + name: 'alt', + label: 'Alt Text', + type: 'text', + required: true, + localized: true, + }, + ], +}; + +export default UnstoredMedia; diff --git a/demo/collections/Validations.ts b/demo/collections/Validations.ts index c2982b7572..cf8bd95253 100644 --- a/demo/collections/Validations.ts +++ b/demo/collections/Validations.ts @@ -1,6 +1,6 @@ -import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import { CollectionConfig } from '../../src/collections/config/types'; -const Validations: PayloadCollectionConfig = { +const Validations: CollectionConfig = { slug: 'validations', labels: { singular: 'Validation', diff --git a/demo/customComponents/CollectionDescription/index.tsx b/demo/customComponents/CollectionDescription/index.tsx new file mode 100644 index 0000000000..b53f6865f1 --- /dev/null +++ b/demo/customComponents/CollectionDescription/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const CollectionDescription: React.FC = () => ( +
+ Collection description +
+); + +export default CollectionDescription; diff --git a/demo/customComponents/Description/index.tsx b/demo/customComponents/Description/index.tsx new file mode 100644 index 0000000000..41433a43ef --- /dev/null +++ b/demo/customComponents/Description/index.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const CustomDescriptionComponent: React.FC = ({ value }) => ( +
+ Character count: + {' '} + { value?.length || 0 } +
+); + +export default CustomDescriptionComponent; diff --git a/demo/globals/BlocksGlobal.ts b/demo/globals/BlocksGlobal.ts index afbadfefa3..ddd7066bf1 100644 --- a/demo/globals/BlocksGlobal.ts +++ b/demo/globals/BlocksGlobal.ts @@ -1,7 +1,7 @@ import checkRole from '../access/checkRole'; import Quote from '../blocks/Quote'; import CallToAction from '../blocks/CallToAction'; -import { PayloadGlobalConfig } from '../../src/globals/config/types'; +import { GlobalConfig } from '../../src/globals/config/types'; export default { slug: 'blocks-global', @@ -19,4 +19,4 @@ export default { localized: true, }, ], -} as PayloadGlobalConfig; +} as GlobalConfig; diff --git a/demo/globals/GlobalWithStrictAccess.ts b/demo/globals/GlobalWithStrictAccess.ts index 772abf3945..aacbab889d 100644 --- a/demo/globals/GlobalWithStrictAccess.ts +++ b/demo/globals/GlobalWithStrictAccess.ts @@ -1,4 +1,4 @@ -import { PayloadGlobalConfig } from '../../src/globals/config/types'; +import { GlobalConfig } from '../../src/globals/config/types'; import checkRole from '../access/checkRole'; export default { @@ -32,4 +32,4 @@ export default { required: true, }, ], -} as PayloadGlobalConfig; +} as GlobalConfig; diff --git a/demo/globals/NavigationArray.ts b/demo/globals/NavigationArray.ts index aa3151a182..e9da709b88 100644 --- a/demo/globals/NavigationArray.ts +++ b/demo/globals/NavigationArray.ts @@ -1,4 +1,4 @@ -import { PayloadGlobalConfig } from '../../src/globals/config/types'; +import { GlobalConfig } from '../../src/globals/config/types'; import checkRole from '../access/checkRole'; export default { @@ -7,6 +7,9 @@ export default { update: ({ req: { user } }) => checkRole(['admin', 'user'], user), read: () => true, }, + admin: { + description: 'A description for the editor', + }, fields: [ { name: 'array', @@ -24,4 +27,4 @@ export default { }], }, ], -} as PayloadGlobalConfig; +} as GlobalConfig; diff --git a/demo/payload.config.ts b/demo/payload.config.ts index ba43ea5e92..834c59ef75 100644 --- a/demo/payload.config.ts +++ b/demo/payload.config.ts @@ -9,6 +9,7 @@ import Conditions from './collections/Conditions'; import CustomComponents from './collections/CustomComponents'; import File from './collections/File'; import Blocks from './collections/Blocks'; +import CustomID from './collections/CustomID'; import DefaultValues from './collections/DefaultValues'; import HiddenFields from './collections/HiddenFields'; import Hooks from './collections/Hooks'; @@ -26,10 +27,12 @@ import Select from './collections/Select'; import StrictPolicies from './collections/StrictPolicies'; import Validations from './collections/Validations'; import Uniques from './collections/Uniques'; +import Geolocation from './collections/Geolocation'; import BlocksGlobal from './globals/BlocksGlobal'; import NavigationArray from './globals/NavigationArray'; import GlobalWithStrictAccess from './globals/GlobalWithStrictAccess'; +import UnstoredMedia from './collections/UnstoredMedia'; export default buildConfig({ cookiePrefix: 'payload', @@ -62,6 +65,7 @@ export default buildConfig({ Code, Conditions, CustomComponents, + CustomID, File, DefaultValues, Blocks, @@ -81,6 +85,8 @@ export default buildConfig({ StrictPolicies, Validations, Uniques, + UnstoredMedia, + Geolocation, ], globals: [ NavigationArray, @@ -106,7 +112,7 @@ export default buildConfig({ defaultDepth: 2, graphQL: { maxComplexity: 1000, - disablePlaygroundInProduction: true, + disablePlaygroundInProduction: false, disable: false, }, // rateLimit: { diff --git a/docs/authentication/operations.mdx b/docs/authentication/operations.mdx index e2b11beda9..cd79405034 100644 --- a/docs/authentication/operations.mdx +++ b/docs/authentication/operations.mdx @@ -194,7 +194,7 @@ If successful, this operation will automatically renew the user's HTTP-only cook **Example REST API token refresh**: ```js -const res = await fetch('http://localhost:3000/api/[collection-slug]/refresh', { +const res = await fetch('http://localhost:3000/api/[collection-slug]/refresh-token', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index d0435770a1..05fc8c031c 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -17,7 +17,8 @@ It's often best practice to write your Collections in separate files and then im | **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. | | **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | | **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | -| **`admin`** | Admin-specific configuration. See below for [more detail](/docs/collections#admin). | +| **`description`**| Text or React component to display below the Collection label in the List view to give editors more information. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | | **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) | | **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) | | **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. | @@ -110,3 +111,21 @@ Hooks are a powerful way to extend collection functionality and execute your own ### Field types 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. + +### TypeScript + +You can import collection types as follows: + +```js +import { CollectionConfig } from 'payload/types'; + +// This is the type used for incoming collection configs. +// Only the bare minimum properties are marked as required. +``` + +```js +import { SanitizedCollectionConfig } from 'payload/types'; + +// This is the type used after an incoming collection config is fully sanitized. +// Generally, this is only used internally by Payload. +``` diff --git a/docs/configuration/globals.mdx b/docs/configuration/globals.mdx index de134517d1..c807b92cb8 100644 --- a/docs/configuration/globals.mdx +++ b/docs/configuration/globals.mdx @@ -17,6 +17,7 @@ As with Collection configs, it's often best practice to write your Globals in se | **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. | | **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | | **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. | +| **`description`**| Text or React component to display below the Global header to give editors more information. | | **`admin`** | Admin-specific configuration. See below for [more detail](/docs/configuration/globals#admin-options). | | **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) | | **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) | @@ -70,3 +71,21 @@ Globals also fully support a smaller subset of Hooks. To learn more, go to the [ ### Field types Globals 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. + +### TypeScript + +You can import global types as follows: + +```js +import { GlobalConfig } from 'payload/types'; + +// This is the type used for incoming global configs. +// Only the bare minimum properties are marked as required. +``` + +```js +import { SanitizedGlobalConfig } from 'payload/types'; + +// This is the type used after an incoming global config is fully sanitized. +// Generally, this is only used internally by Payload. +``` diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx index 5dc93f7c39..c18434afa5 100644 --- a/docs/configuration/overview.mdx +++ b/docs/configuration/overview.mdx @@ -145,3 +145,21 @@ If for any reason you need to re-use the built-in Payload `babel.config.js`, you ``` import { config } from 'payload/babel'; ``` + +### TypeScript + +You can import config types as follows: + +```js +import { Config } from 'payload/config'; + +// This is the type used for an incoming Payload config. +// Only the bare minimum properties are marked as required. +``` + +```js +import { SanitizedConfig } from 'payload/config'; + +// This is the type used after an incoming Payload config is fully sanitized. +// Generally, this is only used internally by Payload. +``` diff --git a/docs/fields/date.mdx b/docs/fields/date.mdx index 5ae80413d4..35f3947d54 100644 --- a/docs/fields/date.mdx +++ b/docs/fields/date.mdx @@ -6,42 +6,57 @@ desc: The Date field type stores a Date in the database. Learn how to use and cu keywords: date, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Date field type saves a Date in the database and provides the Admin panel with a customizable time picker interface. + + The Date field type saves a Date in the database and provides the Admin panel + with a customizable time picker interface. This field uses [`react-datepicker`](https://www.npmjs.com/package/react-datepicker) for the Admin panel component. ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. | -| **`label`** | Used as a field label in the Admin panel and to name the generated GraphQL type. | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`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. | -| **`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). | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. | +| **`label`** | Used as a field label in the Admin panel and to name the generated GraphQL type. | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`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. | +| **`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). | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ -### Admin config +### Admin Date Config -In addition to the default [field admin config](/docs/fields/overview#admin-config), you can customize all of the options that `react-datepicker` provisions for via the `date` property. +In addition to the default [field admin config](/docs/fields/overview#admin-config), you can customize the following fields that will adjust how the component displays in the admin panel via the `date` property. + +| Option | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`pickerAppearance`** | Determines the appearance of the datepicker: `dayAndTime` `timeOnly` `dayOnly`. Defaults to `dayAndTime`. | +| **`displayFormat`** | Determines how the date is presented. dayAndTime default to `MMM d, yyy h:mm a` timeOnly defaults to `h:mm a` and dayOnly defaults to `MMM d, yyy`. | +| **`placeholder`** | Placeholder text for the field. | +| **`monthsToShow`** | Number of months to display max is 2. Defaults to 1. | +| **`minDate`** | Passed directly to [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md). | +| **`maxDate`** | Passed directly to [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md). | +| **`minTime`** | Passed directly to [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md). | +| **`maxTime`** | Passed directly to [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md). | +| **`timeIntervals`** | Passed directly to [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md). Defaults to 30 minutes. | +| **`timeFormat`** | Passed directly to [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md). Defaults to `'h:mm aa'`. | + +_\* An asterisk denotes that a property is required._ Common use cases for customizing the `date` property are to restrict your field to only show time or day input—but lots more can be done. -[Check out the `react-datepicker` docs](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md) for more info. - ### Example `collections/ExampleCollection.js` + ```js { slug: 'example-collection', @@ -53,7 +68,7 @@ Common use cases for customizing the `date` property are to restrict your field defaultValue: '1988-11-05T8:00:00.000+05:00', admin: { date: { - // All `react-datepicker` options are supported + // All config options above should be placed here pickerAppearance: 'timeOnly', } } diff --git a/docs/fields/overview.mdx b/docs/fields/overview.mdx index 3ae8097e5a..0535631a4c 100644 --- a/docs/fields/overview.mdx +++ b/docs/fields/overview.mdx @@ -41,6 +41,7 @@ const Pages = { - [Email](/docs/fields/email) - validates the entry is a properly formatted email - [Group](/docs/fields/group) - nest fields within an object - [Number](/docs/fields/number) - field that enforces that its value be a number +- [Point](/docs/fields/point) - geometric coordinates for location data - [Radio](/docs/fields/radio) - radio button group, allowing only one value to be selected - [Relationship](/docs/fields/relationship) - assign relationships to other collections - [Rich Text](/docs/fields/rich-text) - fully extensible Rich Text editor @@ -83,6 +84,24 @@ Example: } ``` +### Customizable ID + +Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior. +Users are then required to provide a custom ID value when creating a record through the Admin UI or API. +Valid ID types are `number` and `text`. + +Example: +```js +{ + fields: [ + { + name: 'id', + type: 'number', + }, + ], +} +``` + ### Admin config In addition to each field's base configuration, you can define specific traits and properties for fields that only have effect on how they are rendered in the Admin panel. The following properties are available for all fields within the `admin` property: @@ -91,6 +110,7 @@ In addition to each field's base configuration, you can define specific traits a | ------------- | -------------| | `condition` | You can programmatically show / hide fields based on what other fields are doing. [Click here](#conditional-logic) for more info. | | `components` | All field components can be completely and easily swapped out for custom components that you define. [Click here](#custom-admin-components) for more info. | +| `description` | Helper text to display with the field to provide more information for the editor user. [Click here](#description) for more info. | | `position` | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. | | `width` | Restrict the width of a field. you can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. | | `readOnly` | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. | @@ -138,3 +158,54 @@ The `condition` function should return a boolean that will control if the field ### Custom components All Payload fields support the ability to swap in your own React components with ease. For more information, including examples, [click here](/docs/admin/components#fields). + +### Description + +A description can be configured three ways. +- As a string +- As a function that accepts an object containing the field's value, which returns a string +- As a React component that accepts value as a prop + +As shown above, you can simply provide a string that will show by the field, but there are use cases where you may want to create some dynamic feedback. By using a function or a component for the `description` property you can provide rich feedback in realtime the user interacts with the form. + +**Function Example:** + +```js +{ + fields: [ + { + name: 'message', + type: 'text', + maxLength: 20, + admin: { + description: ({ value }) => (`${typeof value === 'string' ? 20 - value.length : '20'} characters left`) + } + } + ] +} +``` +This example will display the number of characters allowed as the user types. + +**Component Example:** +```js +{ + fields: [ + { + name: 'message', + type: 'text', + maxLength: 20, + admin: { + description: + ({ value }) => ( +
+ Character count: + {' '} + { value?.length || 0 } +
+ ) + } + } + ] +} +``` +This component will count the number of characters entered. diff --git a/docs/fields/point.mdx b/docs/fields/point.mdx new file mode 100644 index 0000000000..14555020a5 --- /dev/null +++ b/docs/fields/point.mdx @@ -0,0 +1,54 @@ +--- +title: Point Field +label: Point +order: 95 +desc: The Point field type stores coordinates in the database. Learn how to use Point field for geolocation and geometry. + +keywords: point, geolocation, geospatial, geojson, 2dsphere, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express +--- + + + The Point field type saves a pair of coordinates in the database and assigns an index for location related queries. + + +The data structure in the database matches the GeoJSON structure to represent point. The Payload APIs simplifies the object data to only the [x, y] location. + +### Config + +| Option | Description | +| ---------------- | ----------- | +| **`name`** * | To be used as the property name when stored and retrieved from the database. | +| **`label`** | Used as a field label in the Admin panel and to name the generated GraphQL type. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`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. | +| **`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). | + +*\* An asterisk denotes that a property is required.* + +### Example + +`collections/ExampleCollection.js` +```js +{ + slug: 'example-collection', + fields: [ + { + name: 'location', + type: 'point', + label: 'Location', + }, + ] +} +``` + +### Querying + +In order to do query based on the distance to another point, you can use the `near` operator. When querying using the near operator, the returned documents will be sorted by nearest first. diff --git a/docs/fields/rich-text.mdx b/docs/fields/rich-text.mdx index 58d06364f6..f0a9d64705 100644 --- a/docs/fields/rich-text.mdx +++ b/docs/fields/rich-text.mdx @@ -58,6 +58,7 @@ The default `elements` available in Payload are: - `ol` - `ul` - [`relationship`](#relationship-element) +- [`upload`](#upload-element) **`leaves`** @@ -79,12 +80,16 @@ Set this property to `true` to hide this field's gutter within the admin panel. The built-in `relationship` element is a powerful way to reference other Documents directly within your Rich Text editor. +### Upload element + +Similar to the `relationship` element, the `upload` element is a user-friendly way to reference [Upload-enabled collections](/docs/upload/overview) with a UI specifically designed for media / image-based uploads. + Tip:
- Collections are automatically allowed to be selected within the Rich Text relationship by default. If you want to disable a collection from being able to be referenced in Rich Text fields, set the collection admin option of enableRichTextRelationship to false. + Collections are automatically allowed to be selected within the Rich Text relationship and upload elements by default. If you want to disable a collection from being able to be referenced in Rich Text fields, set the collection admin option of enableRichTextRelationship to false.
-Relationships are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize. +Relationship and Upload elements are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` or `upload` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize. ### Specifying which elements and leaves to allow diff --git a/docs/fields/row.mdx b/docs/fields/row.mdx index 32ad232bbe..ede63f2667 100644 --- a/docs/fields/row.mdx +++ b/docs/fields/row.mdx @@ -15,7 +15,7 @@ keywords: row, fields, config, configuration, documentation, Content Management | Option | Description | | ---------------- | ----------- | | **`fields`** * | Array of field types to nest within this Row. | -| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | +| **`admin`** | Admin-specific configuration excluding `description`, `readOnly`, and `hidden`. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | *\* An asterisk denotes that a property is required.* diff --git a/docs/hooks/collections.mdx b/docs/hooks/collections.mdx index ba5a9cf7b8..5049a59c23 100644 --- a/docs/hooks/collections.mdx +++ b/docs/hooks/collections.mdx @@ -8,19 +8,19 @@ keywords: hooks, collections, config, configuration, documentation, Content Mana Collections feature the ability to define the following hooks: -- [beforeOperation](#beforeOperation) -- [beforeValidate](#beforeValidate) -- [beforeChange](#beforeChange) -- [afterChange](#afterChange) -- [beforeRead](#beforeRead) -- [afterRead](#afterRead) -- [beforeDelete](#beforeDelete) -- [afterDelete](#afterDelete) +- [beforeOperation](#beforeoperation) +- [beforeValidate](#beforevalidate) +- [beforeChange](#beforechange) +- [afterChange](#afterchange) +- [beforeRead](#beforeread) +- [afterRead](#afterread) +- [beforeDelete](#beforedelete) +- [afterDelete](#afterdelete) Additionally, `auth`-enabled collections feature the following hooks: -- [afterLogin](#afterLogin) -- [afterForgotPassword](#afterForgotPassword) +- [afterLogin](#afterlogin) +- [afterForgotPassword](#afterforgotpassword) ## Config diff --git a/docs/local-api/overview.mdx b/docs/local-api/overview.mdx index 49e0089481..fefb3600dc 100644 --- a/docs/local-api/overview.mdx +++ b/docs/local-api/overview.mdx @@ -162,6 +162,11 @@ const result = await payload.update({ // a file directly through the Local API by providing // its full, absolute file path. filePath: path.resolve(__dirname, './path-to-image.jpg'), + + // If you are uploading a file and would like to replace + // the existing file instead of generating a new filename, + // you can set the following property to `true` + overwriteExistingFiles: true, }) ``` diff --git a/docs/plugins/overview.mdx b/docs/plugins/overview.mdx index 40c15c9b2a..fcff43b9ea 100644 --- a/docs/plugins/overview.mdx +++ b/docs/plugins/overview.mdx @@ -1,6 +1,6 @@ --- title: Plugins -label: Plugins +label: Overview order: 10 desc: Plugins provide a great way to modularize Payload functionalities into easy-to-use enhancements and extensions of your Payload apps. keywords: plugins, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, express @@ -76,7 +76,9 @@ export default config; #### When Plugins are initialized -Payload Plugins are executed _after_ the incoming config is validated, sanitized, and default options are merged in. +Payload Plugins are executed _after_ the incoming config is validated, but before it is sanitized and had default options merged in. + +After all plugins are executed, the full config with all plugins will be sanitized. ## Simple example diff --git a/docs/queries/overview.mdx b/docs/queries/overview.mdx index a4fddbca74..7545a47525 100644 --- a/docs/queries/overview.mdx +++ b/docs/queries/overview.mdx @@ -63,6 +63,7 @@ The above example demonstrates a simple query but you can get much more complex. | `in` | The value must be found within the provided comma-delimited list of values. | | `not_in` | The value must NOT be within the provided comma-delimited list of values. | | `exists` | Only return documents where the value either exists (`true`) or does not exist (`false`). | +| `near` | For distance related to a [point field]('/docs/fields/point') comma separated as `, , , `. | Tip:
diff --git a/docs/upload/overview.mdx b/docs/upload/overview.mdx index abcc7601fd..f465f3ac28 100644 --- a/docs/upload/overview.mdx +++ b/docs/upload/overview.mdx @@ -37,13 +37,14 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl #### Collection Upload Options -| Option | Description | -| ---------------------- | -------------| -| **`staticURL`** * | The base URL path to use to access you uploads. Example: `/media` | -| **`staticDir`** * | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. | -| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) | -| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) | -| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | +| Option | Description | +| ------------------------- | -------------| +| **`staticURL`** * | The base URL path to use to access you uploads. Example: `/media` | +| **`staticDir`** * | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. | +| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) | +| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) | +| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | +| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) | *An asterisk denotes that a property above is required.* @@ -66,6 +67,16 @@ const Media = { width: 768, height: 1024, crop: 'centre', + }, + { + name: 'tablet', + width: 1024, + // By specifying `null` or leaving a height undefined, + // the image will be sized to a certain width, + // but it will retain its original aspect ratio + // and calculate a height automatically. + height: null, + crop: 'centre', } ], adminThumbnail: 'thumbnail', @@ -113,6 +124,21 @@ The Payload Admin panel will also automatically display all available files, inc Behind the scenes, Payload relies on [`sharp`](https://sharp.pixelplumbing.com/api-resize#resize) to perform its image resizing. You can specify additional options for `sharp` to use while resizing your images. +##### Accessing the resized images in hooks + +All auto-resized images are exposed to be re-used in hooks and similar via an object that is bound to `req.payloadUploadSizes`. + +The object will have keys for each size generated, and each key will be set equal to a buffer containing the file data. + +### Disabling Local Upload Storage + +If you are using a plugin to send your files off to a third-party file storage host or CDN, like Amazon S3 or similar, you may not want to store your files locally at all. You can prevent Payload from writing files to disk by specifying `disableLocalStorage: true` on your collection's upload config. + + + Note:
+ This is a fairly advanced feature. If you do disable local file storage, by default, your admin panel's thumbnails will be broken as you will not have stored a file. It will be totally up to you to use either a plugin or your own hooks to store your files in a permanent manner, as well as provide your own admin thumbnail using upload.adminThumbnail. +
+ ### Admin thumbnails You can specify how Payload retrieves admin thumbnails for your upload-enabled Collections. This property accepts two different configurations: diff --git a/errors.d.ts b/errors.d.ts new file mode 100644 index 0000000000..ce2f651937 --- /dev/null +++ b/errors.d.ts @@ -0,0 +1 @@ +export * from './dist/errors/types'; diff --git a/package.json b/package.json index 9a37702f85..d120cd79bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "0.7.6", + "version": "0.10.6", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "SEE LICENSE IN license.md", "author": { @@ -41,7 +41,8 @@ "test:client": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.ts NODE_ENV=test jest --config=jest.react.config.js", "clean": "rimraf dist", "release": "release-it", - "release:rc": "release-it prepatch --config .release-it.rc" + "release:beta": "release-it prepatch --config .release-it.beta.json", + "lint": "eslint \"src/**/*.ts\"" }, "bugs": { "url": "https://github.com/payloadcms/payload" @@ -83,7 +84,7 @@ "@faceless-ui/modal": "^1.1.2", "@faceless-ui/scroll-info": "^1.2.3", "@faceless-ui/window-info": "^1.2.4", - "@payloadcms/config-provider": "0.0.23", + "@payloadcms/config-provider": "^0.1.0", "@types/mime": "^2.0.3", "@udecode/slate-plugins": "^0.71.9", "assert": "^2.0.0", @@ -97,6 +98,7 @@ "css-loader": "^5.0.1", "css-minimizer-webpack-plugin": "^1.1.5", "date-fns": "^2.14.0", + "deep-equal": "^2.0.5", "deepmerge": "^4.2.2", "dotenv": "^8.2.0", "express": "^4.17.1", @@ -127,7 +129,7 @@ "mkdirp": "^1.0.4", "mongoose": "^5.8.9", "mongoose-paginate-v2": "^1.3.6", - "node-sass": "^6.0.0", + "node-sass": "^6.0.1", "nodemailer": "^6.4.2", "object-to-formdata": "^4.1.0", "passport": "^0.4.1", @@ -159,13 +161,13 @@ "react-simple-code-editor": "^0.11.0", "react-toastify": "^6.1.0", "sanitize-filename": "^1.6.3", - "sass": "^1.29.0", + "sass": "^1.42.0", "sass-loader": "^10.1.0", "sharp": "^0.28.1", - "slate": "^0.59.0", - "slate-history": "^0.59.0", - "slate-hyperscript": "^0.59.0", - "slate-react": "^0.59.0", + "slate": "^0.66.2", + "slate-history": "^0.66.0", + "slate-hyperscript": "^0.66.0", + "slate-react": "^0.66.4", "style-loader": "^2.0.0", "terser-webpack-plugin": "^5.0.3", "ts-essentials": "^7.0.1", @@ -191,7 +193,7 @@ "@types/compression": "^1.7.0", "@types/connect-history-api-fallback": "^1.3.3", "@types/eslint": "^7.2.6", - "@types/express": "^4.17.9", + "@types/express": "^4.17.13", "@types/express-fileupload": "^1.1.5", "@types/express-graphql": "^0.9.0", "@types/express-rate-limit": "^5.1.0", @@ -262,7 +264,7 @@ "form-data": "^3.0.0", "graphql-request": "^3.4.0", "mongodb": "^3.6.2", - "mongodb-memory-server": "6.5.2", + "mongodb-memory-server": "^7.2.0", "nodemon": "^2.0.6", "optimize-css-assets-webpack-plugin": "^5.0.4", "passport-strategy": "^1.0.0", diff --git a/src/admin/components/elements/Button/index.tsx b/src/admin/components/elements/Button/index.tsx index e31f68e44f..ec859765d5 100644 --- a/src/admin/components/elements/Button/index.tsx +++ b/src/admin/components/elements/Button/index.tsx @@ -5,6 +5,7 @@ import { Props } from './types'; import plus from '../../icons/Plus'; import x from '../../icons/X'; import chevron from '../../icons/Chevron'; +import edit from '../../icons/Edit'; import './index.scss'; @@ -12,6 +13,7 @@ const icons = { plus, x, chevron, + edit, }; const baseClass = 'btn'; diff --git a/src/admin/components/elements/Button/types.ts b/src/admin/components/elements/Button/types.ts index 7af12354b9..01aa28dc35 100644 --- a/src/admin/components/elements/Button/types.ts +++ b/src/admin/components/elements/Button/types.ts @@ -9,7 +9,7 @@ export type Props = { children?: React.ReactNode, onClick?: (event: MouseEvent) => void, disabled?: boolean, - icon?: React.ReactNode | ['chevron' | 'x' | 'plus'], + icon?: React.ReactNode | ['chevron' | 'x' | 'plus' | 'edit'], iconStyle?: 'with-border' | 'without-border' | 'none', buttonStyle?: 'primary' | 'secondary' | 'transparent' | 'error' | 'none' | 'icon-label', round?: boolean, diff --git a/src/admin/components/elements/ColumnSelector/types.ts b/src/admin/components/elements/ColumnSelector/types.ts index a27578745b..6307b02f72 100644 --- a/src/admin/components/elements/ColumnSelector/types.ts +++ b/src/admin/components/elements/ColumnSelector/types.ts @@ -1,6 +1,6 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { - collection: CollectionConfig, + collection: SanitizedCollectionConfig, handleChange: (columns) => void, } diff --git a/src/admin/components/elements/DeleteDocument/types.ts b/src/admin/components/elements/DeleteDocument/types.ts index c547e160e8..5c353a5d2e 100644 --- a/src/admin/components/elements/DeleteDocument/types.ts +++ b/src/admin/components/elements/DeleteDocument/types.ts @@ -1,7 +1,7 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { - collection?: CollectionConfig, + collection?: SanitizedCollectionConfig, id?: string, title?: string, } diff --git a/src/admin/components/elements/FileDetails/types.ts b/src/admin/components/elements/FileDetails/types.ts index 8ba5af4f97..119b26edcb 100644 --- a/src/admin/components/elements/FileDetails/types.ts +++ b/src/admin/components/elements/FileDetails/types.ts @@ -1,7 +1,7 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { - collection: CollectionConfig + collection: SanitizedCollectionConfig doc: Record handleRemove?: () => void, } diff --git a/src/admin/components/elements/ListControls/types.ts b/src/admin/components/elements/ListControls/types.ts index 5121943d58..bef034d03e 100644 --- a/src/admin/components/elements/ListControls/types.ts +++ b/src/admin/components/elements/ListControls/types.ts @@ -1,10 +1,10 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { enableColumns?: boolean, enableSort?: boolean, setSort: (sort: string) => void, - collection: CollectionConfig, + collection: SanitizedCollectionConfig, handleChange: (newState) => void, } diff --git a/src/admin/components/elements/Nav/index.scss b/src/admin/components/elements/Nav/index.scss index 4d37f85fd6..5772ea8a08 100644 --- a/src/admin/components/elements/Nav/index.scss +++ b/src/admin/components/elements/Nav/index.scss @@ -130,7 +130,7 @@ backdrop-filter: saturate(180%) blur(5px); width: 100%; height: base(3); - z-index: $z-nav; + z-index: $z-modal; &__scroll { padding: 0; diff --git a/src/admin/components/elements/Popup/index.tsx b/src/admin/components/elements/Popup/index.tsx index 49b0fe01e6..4353e62640 100644 --- a/src/admin/components/elements/Popup/index.tsx +++ b/src/admin/components/elements/Popup/index.tsx @@ -12,6 +12,7 @@ const baseClass = 'popup'; const Popup: React.FC = (props) => { const { + className, render, size = 'small', color = 'light', @@ -87,6 +88,7 @@ const Popup: React.FC = (props) => { const classes = [ baseClass, + className, `${baseClass}--size-${size}`, `${baseClass}--color-${color}`, `${baseClass}--v-align-${verticalAlign}`, diff --git a/src/admin/components/elements/Popup/types.ts b/src/admin/components/elements/Popup/types.ts index 1d03935f35..e851f4f639 100644 --- a/src/admin/components/elements/Popup/types.ts +++ b/src/admin/components/elements/Popup/types.ts @@ -1,4 +1,5 @@ export type Props = { + className?: string render?: (any) => void, children?: React.ReactNode, horizontalAlign?: 'left' | 'center' | 'right', diff --git a/src/admin/components/elements/ReactSelect/index.tsx b/src/admin/components/elements/ReactSelect/index.tsx index 85393092e0..b872c354b0 100644 --- a/src/admin/components/elements/ReactSelect/index.tsx +++ b/src/admin/components/elements/ReactSelect/index.tsx @@ -7,6 +7,7 @@ import './index.scss'; const ReactSelect: React.FC = (props) => { const { + className, showError = false, options, onChange, @@ -15,6 +16,7 @@ const ReactSelect: React.FC = (props) => { } = props; const classes = [ + className, 'react-select', showError && 'react-select--error', ].filter(Boolean).join(' '); diff --git a/src/admin/components/elements/ReactSelect/types.ts b/src/admin/components/elements/ReactSelect/types.ts index d08a48db4e..9517b9a4c5 100644 --- a/src/admin/components/elements/ReactSelect/types.ts +++ b/src/admin/components/elements/ReactSelect/types.ts @@ -7,6 +7,7 @@ export type Value = { } export type Props = { + className?: string value?: Value | Value[], onChange?: (value: any) => void, disabled?: boolean, diff --git a/src/admin/components/elements/SortComplex/types.ts b/src/admin/components/elements/SortComplex/types.ts index b29c93f435..c171e21bf7 100644 --- a/src/admin/components/elements/SortComplex/types.ts +++ b/src/admin/components/elements/SortComplex/types.ts @@ -1,6 +1,6 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { handleChange: (controls: any) => void, - collection: CollectionConfig, + collection: SanitizedCollectionConfig, } diff --git a/src/admin/components/elements/Thumbnail/types.ts b/src/admin/components/elements/Thumbnail/types.ts index bad63ec4c0..d080799ef8 100644 --- a/src/admin/components/elements/Thumbnail/types.ts +++ b/src/admin/components/elements/Thumbnail/types.ts @@ -1,7 +1,7 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { doc: Record - collection: CollectionConfig + collection: SanitizedCollectionConfig size?: 'small' | 'medium' | 'large' | 'expand', } diff --git a/src/admin/components/elements/UploadCard/index.tsx b/src/admin/components/elements/UploadCard/index.tsx index 1e1ea25bcc..1f580ed4a5 100644 --- a/src/admin/components/elements/UploadCard/index.tsx +++ b/src/admin/components/elements/UploadCard/index.tsx @@ -9,6 +9,7 @@ const baseClass = 'upload-card'; const UploadCard: React.FC = (props) => { const { + className, onClick, doc, collection, @@ -16,6 +17,7 @@ const UploadCard: React.FC = (props) => { const classes = [ baseClass, + className, typeof onClick === 'function' && `${baseClass}--has-on-click`, ].filter(Boolean).join(' '); diff --git a/src/admin/components/elements/UploadCard/types.ts b/src/admin/components/elements/UploadCard/types.ts index b211f959c1..7ebfd460b3 100644 --- a/src/admin/components/elements/UploadCard/types.ts +++ b/src/admin/components/elements/UploadCard/types.ts @@ -1,7 +1,8 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { - collection: CollectionConfig, + className?: string + collection: SanitizedCollectionConfig, doc: Record onClick?: () => void, } diff --git a/src/admin/components/elements/UploadGallery/types.ts b/src/admin/components/elements/UploadGallery/types.ts index 770441c00e..14c9078a9f 100644 --- a/src/admin/components/elements/UploadGallery/types.ts +++ b/src/admin/components/elements/UploadGallery/types.ts @@ -1,7 +1,7 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; export type Props = { docs?: Record[], - collection: CollectionConfig, + collection: SanitizedCollectionConfig, onCardClick: (doc) => void, } diff --git a/src/admin/components/elements/ViewDescription/index.scss b/src/admin/components/elements/ViewDescription/index.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/admin/components/elements/ViewDescription/index.tsx b/src/admin/components/elements/ViewDescription/index.tsx new file mode 100644 index 0000000000..5872c7f449 --- /dev/null +++ b/src/admin/components/elements/ViewDescription/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Props, isComponent } from './types'; +import './index.scss'; + +const ViewDescription: React.FC = (props) => { + const { + description, + } = props; + + if (isComponent(description)) { + const Description = description; + return ; + } + + if (description) { + return ( +
+ {typeof description === 'function' ? description() : description} +
+ ); + } + + return null; +}; + +export default ViewDescription; diff --git a/src/admin/components/elements/ViewDescription/types.ts b/src/admin/components/elements/ViewDescription/types.ts new file mode 100644 index 0000000000..0c5404ab29 --- /dev/null +++ b/src/admin/components/elements/ViewDescription/types.ts @@ -0,0 +1,15 @@ +import React from 'react'; + +export type DescriptionFunction = () => string + +export type DescriptionComponent = React.ComponentType + +type Description = string | DescriptionFunction | DescriptionComponent + +export type Props = { + description?: Description +} + +export function isComponent(description: Description): description is DescriptionComponent { + return React.isValidElement(description); +} diff --git a/src/admin/components/elements/WhereBuilder/field-types.tsx b/src/admin/components/elements/WhereBuilder/field-types.tsx index 1cdf649d8e..6839f7829e 100644 --- a/src/admin/components/elements/WhereBuilder/field-types.tsx +++ b/src/admin/components/elements/WhereBuilder/field-types.tsx @@ -45,6 +45,18 @@ const numeric = [ }, ]; +const geo = [ + ...boolean, + { + label: 'exists', + value: 'exists', + }, + { + label: 'near', + value: 'near', + }, +]; + const like = { label: 'is like', value: 'like', @@ -79,6 +91,10 @@ const fieldTypeConditions = { component: 'Date', operators: [...base, ...numeric], }, + point: { + component: 'Point', + operators: [...geo], + }, upload: { component: 'Text', operators: [...base], diff --git a/src/admin/components/elements/WhereBuilder/types.ts b/src/admin/components/elements/WhereBuilder/types.ts index e659e24af9..35871ece70 100644 --- a/src/admin/components/elements/WhereBuilder/types.ts +++ b/src/admin/components/elements/WhereBuilder/types.ts @@ -1,10 +1,10 @@ -import { CollectionConfig } from '../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; import { Field } from '../../../../fields/config/types'; import { Operator } from '../../../../types'; export type Props = { handleChange: (controls: any) => void, - collection: CollectionConfig, + collection: SanitizedCollectionConfig, } export type FieldCondition = { diff --git a/src/admin/components/forms/FieldDescription/index.scss b/src/admin/components/forms/FieldDescription/index.scss new file mode 100644 index 0000000000..2a6f97e10a --- /dev/null +++ b/src/admin/components/forms/FieldDescription/index.scss @@ -0,0 +1,8 @@ +@import '../../../scss/styles.scss'; + +.field-description { + display: flex; + padding-top: base(.25); + padding-bottom: base(.25); + color: $color-gray; +} diff --git a/src/admin/components/forms/FieldDescription/index.tsx b/src/admin/components/forms/FieldDescription/index.tsx new file mode 100644 index 0000000000..0ec54ac769 --- /dev/null +++ b/src/admin/components/forms/FieldDescription/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Props, isComponent } from './types'; +import './index.scss'; + +const FieldDescription: React.FC = (props) => { + const { + description, + value, + } = props; + + + if (isComponent(description)) { + const Description = description; + return ; + } + + if (description) { + return ( +
+ {typeof description === 'function' ? description({ value }) : description} +
+ ); + } + + return null; +}; + +export default FieldDescription; diff --git a/src/admin/components/forms/FieldDescription/types.ts b/src/admin/components/forms/FieldDescription/types.ts new file mode 100644 index 0000000000..98e8c0bdd7 --- /dev/null +++ b/src/admin/components/forms/FieldDescription/types.ts @@ -0,0 +1,16 @@ +import React from 'react'; + +export type DescriptionFunction = (value: unknown) => string + +export type DescriptionComponent = React.ComponentType<{value: unknown}> + +type Description = string | DescriptionFunction | DescriptionComponent + +export type Props = { + description?: Description + value: unknown; +} + +export function isComponent(description: Description): description is DescriptionComponent { + return React.isValidElement(description); +} diff --git a/src/admin/components/forms/Form/buildStateFromSchema.ts b/src/admin/components/forms/Form/buildStateFromSchema.ts index b87980fb2d..d3e9758dcb 100644 --- a/src/admin/components/forms/Form/buildStateFromSchema.ts +++ b/src/admin/components/forms/Form/buildStateFromSchema.ts @@ -2,18 +2,12 @@ import ObjectID from 'bson-objectid'; import { Field as FieldSchema } from '../../../../fields/config/types'; import { Fields, Field, Data } from './types'; -const buildValidationPromise = async (fieldState: Field, field: FieldSchema, fullData: Data = {}, data: Data = {}) => { +const buildValidationPromise = async (fieldState: Field, field: FieldSchema) => { const validatedFieldState = fieldState; - let passesConditionalLogic = true; - - if (field?.admin?.condition) { - passesConditionalLogic = await field.admin.condition(fullData, data); - } - let validationResult: boolean | string = true; - if (passesConditionalLogic && typeof field.validate === 'function') { + if (typeof field.validate === 'function') { validationResult = await field.validate(fieldState.value, field); } @@ -29,7 +23,7 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = if (fieldSchema) { const validationPromises = []; - const structureFieldState = (field, data = {}) => { + const structureFieldState = (field, passesCondition, data = {}) => { const value = typeof data?.[field.name] !== 'undefined' ? data[field.name] : field.defaultValue; const fieldState = { @@ -37,15 +31,16 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = initialValue: value, valid: true, validate: field.validate, - condition: field?.admin?.condition, + condition: field.admin?.condition, + passesCondition, }; - validationPromises.push(buildValidationPromise(fieldState, field, fullData, data)); + validationPromises.push(buildValidationPromise(fieldState, field)); return fieldState; }; - const iterateFields = (fields: FieldSchema[], data: Data, path = '') => fields.reduce((state, field) => { + const iterateFields = (fields: FieldSchema[], data: Data, parentPassesCondition: boolean, path = '') => fields.reduce((state, field) => { let initialData = data; if (!field?.admin?.disabled) { @@ -53,6 +48,8 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = initialData = { [field.name]: field.defaultValue }; } + const passesCondition = Boolean((field?.admin?.condition ? field.admin.condition(fullData || {}, initialData || {}) : true) && parentPassesCondition); + if (field.name) { if (field.type === 'relationship' && initialData?.[field.name] === null) { initialData[field.name] = 'null'; @@ -75,7 +72,7 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = initialValue: row.id || new ObjectID().toHexString(), valid: true, }, - ...iterateFields(field.fields, row, rowPath), + ...iterateFields(field.fields, row, passesCondition, rowPath), }; }, {}), }; @@ -104,7 +101,7 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = initialValue: row.id || new ObjectID().toHexString(), valid: true, }, - ...(block?.fields ? iterateFields(block.fields, row, rowPath) : {}), + ...(block?.fields ? iterateFields(block.fields, row, passesCondition, rowPath) : {}), }; }, {}), }; @@ -120,13 +117,13 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = return { ...state, - ...iterateFields(field.fields, subFieldData, `${path}${field.name}.`), + ...iterateFields(field.fields, subFieldData, passesCondition, `${path}${field.name}.`), }; } return { ...state, - [`${path}${field.name}`]: structureFieldState(field, data), + [`${path}${field.name}`]: structureFieldState(field, passesCondition, data), }; } @@ -134,21 +131,21 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = if (field.type === 'row') { return { ...state, - ...iterateFields(field.fields, data, path), + ...iterateFields(field.fields, data, passesCondition, path), }; } // Handle normal fields return { ...state, - [`${path}${field.name}`]: structureFieldState(field, data), + [`${path}${field.name}`]: structureFieldState(field, passesCondition, data), }; } return state; }, {}); - const resultingState = iterateFields(fieldSchema, fullData); + const resultingState = iterateFields(fieldSchema, fullData, true); await Promise.all(validationPromises); return resultingState; } diff --git a/src/admin/components/forms/Form/fieldReducer.ts b/src/admin/components/forms/Form/fieldReducer.ts index 2d1939984e..0e8f9d79cc 100644 --- a/src/admin/components/forms/Form/fieldReducer.ts +++ b/src/admin/components/forms/Form/fieldReducer.ts @@ -1,5 +1,8 @@ +import equal from 'deep-equal'; import { unflatten, flatten } from 'flatley'; import flattenFilters from './flattenFilters'; +import getSiblingData from './getSiblingData'; +import reduceFieldsToValues from './reduceFieldsToValues'; import { Fields } from './types'; const unflattenRowsFromState = (state: Fields, path) => { @@ -36,7 +39,26 @@ const unflattenRowsFromState = (state: Fields, path) => { function fieldReducer(state: Fields, action): Fields { switch (action.type) { case 'REPLACE_STATE': { - return action.state; + const newState = {}; + + // Only update fields that have changed + // by comparing old value / initialValue to new + // .. + // This is a performance enhancement for saving + // large documents with hundreds of fields + + Object.entries(action.state).forEach(([path, field]) => { + const oldField = state[path]; + const newField = field; + + if (!equal(oldField, newField)) { + newState[path] = newField; + } else if (oldField) { + newState[path] = oldField; + } + }); + + return newState; } case 'REMOVE': { @@ -103,6 +125,39 @@ function fieldReducer(state: Fields, action): Fields { return newState; } + case 'MODIFY_CONDITION': { + const { path, result } = action; + + return Object.entries(state).reduce((newState, [fieldPath, field]) => { + if (fieldPath === path || fieldPath.indexOf(`${path}.`) === 0) { + let passesCondition = result; + + // If a condition is being set to true, + // Set all conditions to true + // Besides those who still fail their own conditions + + if (passesCondition && field.condition) { + passesCondition = field.condition(reduceFieldsToValues(state), getSiblingData(state, path)); + } + + return { + ...newState, + [fieldPath]: { + ...field, + passesCondition, + }, + }; + } + + return { + ...newState, + [fieldPath]: { + ...field, + }, + }; + }, {}); + } + default: { const newField = { value: action.value, @@ -114,6 +169,7 @@ function fieldReducer(state: Fields, action): Fields { stringify: action.stringify, validate: action.validate, condition: action.condition, + passesCondition: action.passesCondition, }; return { diff --git a/src/admin/components/forms/Form/index.tsx b/src/admin/components/forms/Form/index.tsx index 591789e3c7..b9de633140 100644 --- a/src/admin/components/forms/Form/index.tsx +++ b/src/admin/components/forms/Form/index.tsx @@ -67,32 +67,24 @@ const Form: React.FC = (props) => { const validatedFieldState = {}; let isValid = true; - const data = contextRef.current.getData(); - const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => { const validatedField = { ...field, valid: true, }; - const siblingData = contextRef.current.getSiblingData(path); + if (field.passesCondition !== false) { + let validationResult: boolean | string = true; - let passesConditionalLogic = true; + if (typeof field.validate === 'function') { + validationResult = await field.validate(field.value); + } - if (typeof field?.condition === 'function') { - passesConditionalLogic = await field.condition(data, siblingData); - } - - let validationResult: boolean | string = true; - - if (passesConditionalLogic && typeof field.validate === 'function') { - validationResult = await field.validate(field.value); - } - - if (typeof validationResult === 'string') { - validatedField.errorMessage = validationResult; - validatedField.valid = false; - isValid = false; + if (typeof validationResult === 'string') { + validatedField.errorMessage = validationResult; + validatedField.valid = false; + isValid = false; + } } validatedFieldState[path] = validatedField; diff --git a/src/admin/components/forms/Form/types.ts b/src/admin/components/forms/Form/types.ts index bc19f86af8..930e6492f1 100644 --- a/src/admin/components/forms/Form/types.ts +++ b/src/admin/components/forms/Form/types.ts @@ -10,6 +10,7 @@ export type Field = { ignoreWhileFlattening?: boolean stringify?: boolean condition?: Condition + passesCondition?: boolean } export type Fields = { diff --git a/src/admin/components/forms/Label/index.scss b/src/admin/components/forms/Label/index.scss index 98c4ceeb69..c6b9c3b1fc 100644 --- a/src/admin/components/forms/Label/index.scss +++ b/src/admin/components/forms/Label/index.scss @@ -3,7 +3,7 @@ label.field-label { display: flex; padding-bottom: base(.25); - color: $color-gray; + color: $color-dark-gray; .required { color: $color-red; diff --git a/src/admin/components/forms/field-types/Array/Array.tsx b/src/admin/components/forms/field-types/Array/Array.tsx index e242351681..a51001e277 100644 --- a/src/admin/components/forms/field-types/Array/Array.tsx +++ b/src/admin/components/forms/field-types/Array/Array.tsx @@ -11,6 +11,7 @@ import useFieldType from '../../useFieldType'; import Error from '../../Error'; import { array } from '../../../../../fields/validations'; import Banner from '../../../elements/Banner'; +import FieldDescription from '../../FieldDescription'; import { Props, RenderArrayProps } from './types'; import './index.scss'; @@ -30,6 +31,7 @@ const ArrayFieldType: React.FC = (props) => { permissions, admin: { readOnly, + description, condition, }, } = props; @@ -132,6 +134,7 @@ const ArrayFieldType: React.FC = (props) => { minRows={minRows} maxRows={maxRows} required={required} + description={description} /> ); }; @@ -156,6 +159,7 @@ const RenderArray = React.memo((props: RenderArrayProps) => { minRows, maxRows, required, + description, } = props; const hasMaxRows = maxRows && rows.length >= maxRows; @@ -173,6 +177,10 @@ const RenderArray = React.memo((props: RenderArrayProps) => {

{label}

+
{(provided) => ( diff --git a/src/admin/components/forms/field-types/Array/index.scss b/src/admin/components/forms/field-types/Array/index.scss index 1ef8fccc70..43b4c7a4c5 100644 --- a/src/admin/components/forms/field-types/Array/index.scss +++ b/src/admin/components/forms/field-types/Array/index.scss @@ -4,6 +4,14 @@ margin: base(2) 0; min-width: base(15); + &__header { + h3 { + margin-bottom: 0; + } + + margin-bottom: base(1); + } + &__error-wrap { position: relative; } diff --git a/src/admin/components/forms/field-types/Array/types.ts b/src/admin/components/forms/field-types/Array/types.ts index d9408fed57..067f45c811 100644 --- a/src/admin/components/forms/field-types/Array/types.ts +++ b/src/admin/components/forms/field-types/Array/types.ts @@ -1,5 +1,5 @@ import { Data } from '../../Form/types'; -import { ArrayField, Labels, Field } from '../../../../../fields/config/types'; +import { ArrayField, Labels, Field, Description } from '../../../../../fields/config/types'; import { FieldTypes } from '..'; import { FieldPermissions } from '../../../../../auth/types'; @@ -30,4 +30,5 @@ export type RenderArrayProps = { showError: boolean errorMessage: string rows: Data[] + description?: Description } diff --git a/src/admin/components/forms/field-types/Blocks/Blocks.tsx b/src/admin/components/forms/field-types/Blocks/Blocks.tsx index f168c8d904..7cc49cd83a 100644 --- a/src/admin/components/forms/field-types/Blocks/Blocks.tsx +++ b/src/admin/components/forms/field-types/Blocks/Blocks.tsx @@ -17,6 +17,7 @@ import Popup from '../../../elements/Popup'; import BlockSelector from './BlockSelector'; import { blocks as blocksValidator } from '../../../../../fields/validations'; import Banner from '../../../elements/Banner'; +import FieldDescription from '../../FieldDescription'; import { Props, RenderBlockProps } from './types'; import { DocumentPreferences } from '../../../../../preferences/types'; @@ -44,6 +45,7 @@ const Blocks: React.FC = (props) => { permissions, admin: { readOnly, + description, condition, }, } = props; @@ -181,6 +183,7 @@ const Blocks: React.FC = (props) => { minRows={minRows} maxRows={maxRows} required={required} + description={description} /> ); }; @@ -206,6 +209,7 @@ const RenderBlocks = React.memo((props: RenderBlockProps) => { minRows, maxRows, required, + description, } = props; const hasMaxRows = maxRows && rows.length >= maxRows; @@ -223,6 +227,10 @@ const RenderBlocks = React.memo((props: RenderBlockProps) => {

{label}

+
void + description?: Description } diff --git a/src/admin/components/forms/field-types/Checkbox/index.tsx b/src/admin/components/forms/field-types/Checkbox/index.tsx index c32f78c6b2..d49342318c 100644 --- a/src/admin/components/forms/field-types/Checkbox/index.tsx +++ b/src/admin/components/forms/field-types/Checkbox/index.tsx @@ -4,6 +4,7 @@ import withCondition from '../../withCondition'; import Error from '../../Error'; import { checkbox } from '../../../../../fields/validations'; import Check from '../../../icons/Check'; +import FieldDescription from '../../FieldDescription'; import { Props } from './types'; import './index.scss'; @@ -23,6 +24,7 @@ const Checkbox: React.FC = (props) => { readOnly, style, width, + description, condition, } = {}, } = props; @@ -87,6 +89,10 @@ const Checkbox: React.FC = (props) => { {label} + ); }; diff --git a/src/admin/components/forms/field-types/Code/Code.tsx b/src/admin/components/forms/field-types/Code/Code.tsx index 77f279cd2d..5c4f4b2acd 100644 --- a/src/admin/components/forms/field-types/Code/Code.tsx +++ b/src/admin/components/forms/field-types/Code/Code.tsx @@ -7,6 +7,7 @@ import useFieldType from '../../useFieldType'; import withCondition from '../../withCondition'; import Label from '../../Label'; import Error from '../../Error'; +import FieldDescription from '../../FieldDescription'; import { code } from '../../../../../fields/validations'; import { Props } from './types'; @@ -23,6 +24,7 @@ const Code: React.FC = (props) => { style, width, language, + description, condition, } = {}, label, @@ -94,6 +96,10 @@ const Code: React.FC = (props) => { pointerEvents: readOnly ? 'none' : 'auto', }} /> + ); }; diff --git a/src/admin/components/forms/field-types/DateTime/index.scss b/src/admin/components/forms/field-types/DateTime/index.scss index 6bcb34a082..e2dee8285d 100644 --- a/src/admin/components/forms/field-types/DateTime/index.scss +++ b/src/admin/components/forms/field-types/DateTime/index.scss @@ -3,6 +3,10 @@ .date-time-field { margin-bottom: $baseline; + &__error-wrap { + position: relative; + } + &--has-error { .react-datepicker__input-container input { background-color: lighten($color-red, 20%); diff --git a/src/admin/components/forms/field-types/DateTime/index.tsx b/src/admin/components/forms/field-types/DateTime/index.tsx index ae37e7ae3f..cebb44ebdd 100644 --- a/src/admin/components/forms/field-types/DateTime/index.tsx +++ b/src/admin/components/forms/field-types/DateTime/index.tsx @@ -5,6 +5,7 @@ import withCondition from '../../withCondition'; import useFieldType from '../../useFieldType'; import Label from '../../Label'; import Error from '../../Error'; +import FieldDescription from '../../FieldDescription'; import { date as dateValidation } from '../../../../../fields/validations'; import { Props } from './types'; @@ -25,6 +26,7 @@ const DateTime: React.FC = (props) => { style, width, date, + description, condition, } = {}, } = props; @@ -62,10 +64,12 @@ const DateTime: React.FC = (props) => { width, }} > - +
+ +