Compare commits

..

42 Commits

Author SHA1 Message Date
James
f361a44cca chore(release): v0.16.3 2022-05-04 11:20:47 -04:00
James
47c37e0153 fix: rare bug while merging locale data 2022-05-04 11:18:52 -04:00
James
5df3b35189 chore(release): v0.16.2 2022-05-02 13:44:31 -04:00
Elliot DeNolf
1e4a68f76e fix: checkbox defaultValues and more typing of sanitize (#550) 2022-05-02 13:42:56 -04:00
James
b3832e21c9 feat: exposes findMany argument to afterRead hooks to discern between find and findByID 2022-05-02 13:38:13 -04:00
James Mikrut
18489faceb feat: optimizes field operations
* wip: beforeChange field op pattern

* feat: optimizes field-level beforeChange

* feat: optimizes beforeValidate

* feat: optimizes afterChange

* feat: optimizes afterRead

* chore: comment accuracy
2022-05-02 12:46:52 -04:00
Dan Ribbens
69d328d15e docs: updated link 2022-04-29 23:29:46 -04:00
Dan Ribbens
738e8ab9b6 docs: preventing abuse of file uploads 2022-04-29 23:25:55 -04:00
Elliot DeNolf
e7349fea9a chore: add explicit release scripts 2022-04-29 20:21:01 -04:00
James
51a6790f26 chore(release): v0.16.1 2022-04-29 18:34:40 -04:00
James
515f20372e chore: passing tests 2022-04-29 18:30:40 -04:00
James
12fbe8368f Merge branch 'fix/localization-defaultvalues' of github.com:payloadcms/payload 2022-04-29 16:49:17 -04:00
Dan Ribbens
55b4dfb309 chore: test defaultValue with localization 2022-04-29 16:48:50 -04:00
James
fb7bb76674 Merge branch 'master' of github.com:payloadcms/payload 2022-04-29 16:48:24 -04:00
James
e46b942259 feat: exposes payload within server-side validation args 2022-04-29 16:48:15 -04:00
Dan Ribbens
e4affd4bf9 docs: updated links 2022-04-29 14:40:49 -04:00
Dan Ribbens
16398d3438 chore(release): v0.16.0 2022-04-29 13:19:34 -04:00
Dan Ribbens
e8503232ba chore: changes eslint rules no-console and cleanup 2022-04-29 12:36:21 -04:00
Dan Ribbens
bf48fdf189 fix: file upload safely handles missing mimeTypes (#540)
* fix: file upload safely handles missing mimeTypes

* feat: uploaded files that do not have mimeTypes are given one based on file extension
2022-04-29 10:36:58 -04:00
Dan Ribbens
834f4c2700 feat: allow subfield readOnly to override parent readOnly (#546) 2022-04-29 10:36:38 -04:00
Dan Ribbens
e297eb9090 feat: allows defaultValue to accept async function to calculate defaultValue (#547)
* feat: field default values with async functions

* docs: improves field default value

* chore: simplifies async defaultValue

* chore: api test coverage for default value functions

* chore: WIP defaultValue async arrays

* chore: refactors and simplifies buildStateFromSchema

* chore: improves tests for defaultValues

Co-authored-by: James <james@trbl.design>
2022-04-29 10:36:10 -04:00
James
1f394bef72 chore(release): v0.15.13 2022-04-26 10:21:55 -04:00
James
1cdd5b96b3 chore: ensures array fields update modified state 2022-04-26 10:19:39 -04:00
James
2d14ab1217 chore(release): v0.15.12 2022-04-25 21:02:31 -04:00
James
8bdbd0dd41 fix: ensures adding array / block rows modifies form state 2022-04-25 20:59:33 -04:00
James
800be4c9a0 chore(release): v0.15.11 2022-04-24 19:37:39 -04:00
James
b99ec060ca fix: improperly typed access control 2022-04-24 19:35:58 -04:00
James
d5f4c030b4 chore(release): v0.15.10 2022-04-24 19:19:41 -04:00
James
4de92e3924 Merge branch 'master' of github.com:payloadcms/payload 2022-04-24 19:16:48 -04:00
James
3b70560e25 fix: block form-data bug 2022-04-24 19:16:39 -04:00
Dan Ribbens
d88c89fb05 chore: update discord invite link 2022-04-24 10:24:37 -04:00
Dan Ribbens
24d6d8e5f9 chore: add public discord link (#541) 2022-04-22 14:35:39 -04:00
Elliot DeNolf
9a9b28113a test: implement cypress test suite (#527) 2022-04-20 23:12:02 -04:00
James
ec84ffbee2 chore(release): v0.15.9 2022-04-20 17:25:20 -04:00
James
3c1dfb88df fix: intermittent blocks UI issue 2022-04-20 17:20:05 -04:00
James
4a6b79b231 chore(release): v0.15.8 2022-04-20 13:53:52 -04:00
James
9e2ed56ef0 chore: migrates to React 18 2022-04-20 11:29:09 -04:00
Alessio Gravili
9e324be057 fix: ensure relationTo is valid in upload fields (#533)
* Error handling

* Improve error message

* Re-position error handling

* Move error checking to sanitize
2022-04-20 09:21:14 -04:00
James Mikrut
b7f47c9bb1 chore/ts dep compat (#535)
* chore: ensures typescript dependency compatibility

* chore: updates dependencies

* chore: updates window-info breakpoints
2022-04-20 09:16:36 -04:00
Dan Ribbens
8a997c82be chore: github action runs demo generate types (#530) 2022-04-17 10:59:32 -04:00
Dan Ribbens
3dcd8a24cb fix: richtext editor input height (#529) 2022-04-17 10:56:33 -04:00
Dan Ribbens
203ce2c2f9 chore: update changelog 2022-04-12 11:50:27 -04:00
141 changed files with 5083 additions and 3185 deletions

View File

@@ -46,15 +46,23 @@ module.exports = {
'@typescript-eslint/no-use-before-define': 'off',
},
},
{
files: ['*.e2e.ts'],
rules: {
'@typescript-eslint/no-use-before-define': 'off',
'jest/expect-expect': 'off',
},
},
],
rules: {
'no-sparse-arrays': 'off',
'import/no-extraneous-dependencies': ['error', { packageDir: './' }],
'react/jsx-filename-extension': [2, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
'import/prefer-default-export': 'off',
'react/prop-types': 'off',
'react/require-default-props': 'off',
'react/no-unused-prop-types': 'off',
'no-console': 'warn',
'no-sparse-arrays': 'off',
'no-underscore-dangle': 'off',
'no-use-before-define': 'off',
'arrow-body-style': 0,

View File

@@ -33,6 +33,7 @@ jobs:
- run: yarn build
- run: yarn test:client
- run: yarn test:int # In-memory db + api tests
- run: yarn demo:generate:types
env:
CI: true
install_npm:

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@ coverage
package-lock.json
dist
.idea
cypress/videos
cypress/screenshots
# Created by https://www.gitignore.io/api/node,macos,windows,webstorm,sublimetext,visualstudiocode

View File

@@ -1,5 +1,91 @@
## [0.16.3](https://github.com/payloadcms/payload/compare/v0.16.2...v0.16.3) (2022-05-04)
### Bug Fixes
* rare bug while merging locale data ([47c37e0](https://github.com/payloadcms/payload/commit/47c37e015300be4f9d5d4387f26a0adb39b8379c))
## [0.16.2](https://github.com/payloadcms/payload/compare/v0.16.1...v0.16.2) (2022-05-02)
### Bug Fixes
* checkbox defaultValues and more typing of sanitize ([#550](https://github.com/payloadcms/payload/issues/550)) ([1e4a68f](https://github.com/payloadcms/payload/commit/1e4a68f76eeaab58ced0cc500223a1b86d66668e))
### Features
* exposes findMany argument to afterRead hooks to discern between find and findByID ([b3832e2](https://github.com/payloadcms/payload/commit/b3832e21c91fa5d52067cfc24a0b4f8aa6e178ec))
* optimizes field operations ([18489fa](https://github.com/payloadcms/payload/commit/18489facebe5d7b0abc87dcc30fae28510b6bb19))
## [0.16.1](https://github.com/payloadcms/payload/compare/v0.16.0...v0.16.1) (2022-04-29)
### Features
* exposes payload within server-side validation args ([e46b942](https://github.com/payloadcms/payload/commit/e46b94225957bba7758a0a2c22776c44a2d2d633))
# [0.16.0](https://github.com/payloadcms/payload/compare/v0.15.13...v0.16.0) (2022-04-29)
### Bug Fixes
* file upload safely handles missing mimeTypes ([#540](https://github.com/payloadcms/payload/issues/540)) ([bf48fdf](https://github.com/payloadcms/payload/commit/bf48fdf18961a2e57bcc5aae73de4c569e97e42b))
### Features
* allow subfield readOnly to override parent readOnly ([#546](https://github.com/payloadcms/payload/issues/546)) ([834f4c2](https://github.com/payloadcms/payload/commit/834f4c270020bf32852c00a3abbb908853689006))
* allows defaultValue to accept async function to calculate defaultValue ([#547](https://github.com/payloadcms/payload/issues/547)) ([e297eb9](https://github.com/payloadcms/payload/commit/e297eb90907d933524d220255d5f8dc4276358c5))
## [0.15.13](https://github.com/payloadcms/payload/compare/v0.15.12...v0.15.13) (2022-04-26)
## [0.15.12](https://github.com/payloadcms/payload/compare/v0.15.11...v0.15.12) (2022-04-26)
### Bug Fixes
* ensures adding array / block rows modifies form state ([8bdbd0d](https://github.com/payloadcms/payload/commit/8bdbd0dd418cd665441703fa4fd87becafd26170))
## [0.15.11](https://github.com/payloadcms/payload/compare/v0.15.10...v0.15.11) (2022-04-24)
### Bug Fixes
* improperly typed access control ([b99ec06](https://github.com/payloadcms/payload/commit/b99ec060cacf7a05c20ba0a05dd6ef6ab60df304))
## [0.15.10](https://github.com/payloadcms/payload/compare/v0.15.9...v0.15.10) (2022-04-24)
### Bug Fixes
* block form-data bug ([3b70560](https://github.com/payloadcms/payload/commit/3b70560e2566de5294eb15945120ffd6f1f5f1c4))
## [0.15.9](https://github.com/payloadcms/payload/compare/v0.15.8...v0.15.9) (2022-04-20)
### Bug Fixes
* intermittent blocks UI issue ([3c1dfb8](https://github.com/payloadcms/payload/commit/3c1dfb88df8651b26cb1dbc102a34cd0aad722bc))
## [0.15.8](https://github.com/payloadcms/payload/compare/v0.15.7...v0.15.8) (2022-04-20)
### Bug Fixes
* ensure relationTo is valid in upload fields ([#533](https://github.com/payloadcms/payload/issues/533)) ([9e324be](https://github.com/payloadcms/payload/commit/9e324be0577447965ee2f87c3a3943cd4f0c0a1c))
* richtext editor input height ([#529](https://github.com/payloadcms/payload/issues/529)) ([3dcd8a2](https://github.com/payloadcms/payload/commit/3dcd8a24cb8cbb77aae82a1f841429e7149e3182))
## [0.15.7](https://github.com/payloadcms/payload/compare/v0.15.6...v0.15.7) (2022-04-12)
### Bug Fixes
* checkbox validation error positioning ([9af89b6](https://github.com/payloadcms/payload/commit/9af89b6c03bc4e82a0c3e353f0d53ec14a847ee2))
### Features
* sanitize defaultValue to false when field is required ([6f84c0a](https://github.com/payloadcms/payload/commit/6f84c0a86943e9d99edde21b1d448e7ece3dd83c))
## [0.15.6](https://github.com/payloadcms/payload/compare/v0.15.5...v0.15.6) (2022-04-06)

View File

@@ -2,14 +2,18 @@
<p align="center">A self-hosted, TypeScript / JavaScript headless CMS & application framework built with Express, MongoDB and React.</p>
<p align="center">
<a href="https://github.com/payloadcms/payload/actions">
<img src="https://github.com/payloadcms/payload/workflows/build/badge.svg">
<img src="https://github.com/payloadcms/payload/workflows/build/badge.svg" />
</a>
<a href="https://www.npmjs.com/package/payload">
<img alt="npm" src="https://img.shields.io/npm/v/payload">
<img alt="npm" src="https://img.shields.io/npm/v/payload" />
</a>
<a href="https://twitter.com/intent/tweet?text=Payload%20-%20A%20self-hosted%2C%20headless%20JavaScript%20CMS%20%26%20application%20framework&url=https%3A%2F%2Fgithub.com%2Fpayloadcms%2Fpayload">
<img alt="Tweet Payload" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social">
<img alt="Tweet Payload" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social" />
</a>
<a href="https://discord.com/invite/r6sCXqVk3v">
<img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord" />
</a>
</p>

8
cypress.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://on.cypress.io/cypress.schema.json",
"testFiles": "**/*e2e.ts",
"ignoreTestFiles": "**/examples/*spec.js",
"viewportWidth": 1440,
"viewportHeight": 900,
"baseUrl": "http://localhost:3000"
}

16
cypress/cypress.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
export { };
declare global {
namespace Cypress {
interface Chainable<Subject> {
visitAdmin(): Chainable<any>
/**
* Login
*
* Creates user if not exists
*/
login(): Chainable<any>
apiLogin(): Chainable<any>
}
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,43 @@
import { adminURL } from './common/constants';
import { credentials } from './common/credentials';
describe('Collections', () => {
const collectionName = 'Admins';
before(() => {
cy.apiLogin();
});
beforeEach(() => {
cy.visitAdmin();
});
it('can view collection', () => {
cy.contains(collectionName).click();
cy.get('.collection-list__wrap')
.should('be.visible');
cy.get('.collection-list__header')
.contains(collectionName)
.should('be.visible');
cy.get('.table')
.contains(credentials.email)
.should('be.visible');
});
it('can create new', () => {
cy.contains(collectionName).click();
cy.contains('Create New').click();
cy.url().should('eq', `${adminURL}/collections/${collectionName.toLowerCase()}/create`);
});
it('can create new - plus button', () => {
cy.contains(collectionName)
.get('.card__actions')
.first()
.click();
cy.url().should('eq', `${adminURL}/collections/${collectionName.toLowerCase()}/create`);
});
});

View File

@@ -0,0 +1 @@
export const adminURL = 'http://localhost:3000/admin';

View File

@@ -0,0 +1,4 @@
export const credentials = {
email: 'test@test.com',
password: 'test123',
};

View File

@@ -0,0 +1,89 @@
/* eslint-disable jest/expect-expect */
import { adminURL } from './common/constants';
import { credentials } from './common/credentials';
const viewportSizes: Cypress.ViewportPreset[] = ['macbook-15', 'iphone-x', 'ipad-2'];
describe.skip('Payload Login', () => {
beforeEach(() => {
cy.clearCookies();
});
after(() => {
cy.apiLogin();
});
viewportSizes.forEach((viewportSize) => {
describe(`Login (${viewportSize})`, () => {
beforeEach(() => {
cy.visit(adminURL);
});
it('success', () => {
cy.viewport(viewportSize);
cy.get('#email').type(credentials.email);
cy.get('#password').type(credentials.password);
cy.get('form')
.contains('form', 'Login')
.should('be.visible')
.submit();
cy.get('.template-default')
.find('h3.dashboard__label')
.should('have.length', 2); // TODO: Should assert label content
cy.url().should('eq', adminURL);
});
it('bad Password', () => {
cy.viewport(viewportSize);
cy.visit(adminURL);
cy.get('#email').type(credentials.email);
cy.get('#password').type('badpassword');
cy.get('form')
.contains('form', 'Login')
.should('be.visible')
.submit();
cy.get('.Toastify')
.contains('The email or password provided is incorrect.')
.should('be.visible');
});
it('bad Password - Retry Success', () => {
cy.viewport(viewportSize);
cy.visit(adminURL);
cy.get('#email').type(credentials.email);
cy.get('#password').type('badpassword');
cy.get('form')
.contains('form', 'Login')
.should('be.visible')
.submit();
cy.get('.Toastify')
.contains('The email or password provided is incorrect.')
.should('be.visible');
// Dismiss notification
cy.wait(500);
cy.get('.Toastify__toast-body').click();
cy.wait(200);
cy.get('.Toastify__toast-body').should('not.be.visible');
cy.url().should('eq', `${adminURL}/login`);
cy.get('#password').clear().type(credentials.password);
cy.get('form')
.contains('form', 'Login')
.should('be.visible')
.submit();
cy.get('.template-default')
.find('h3.dashboard__label')
.should('have.length', 2); // TODO: Should assert label content
cy.url().should('eq', adminURL);
});
});
});
});

22
cypress/plugins/index.js Normal file
View File

@@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@@ -0,0 +1,54 @@
import { adminURL } from '../integration/common/constants';
import { credentials } from '../integration/common/credentials';
Cypress.Commands.add('visitAdmin', () => {
cy.visit(adminURL);
});
Cypress.Commands.add('login', () => {
cy.clearCookies();
cy.visit(adminURL);
cy.get('#email').type(credentials.email);
cy.get('#password').type(credentials.password);
cy.get('body')
.then((body) => {
if (body.find('.dashboard__card-list').length) {
cy.get('.dashboard__card-list')
.should('be.visible');
}
if (body.find('#confirm-password').length) {
cy.get('#confirm-password').type(credentials.password);
cy.get('.rs__indicators').first()
.click();
cy.get('.rs__menu').first().contains('admin')
.click();
cy.get('form')
.contains('form', 'Create')
.should('be.visible')
.submit();
}
if (body.find('form').length) {
cy.get('form')
.contains('form', 'Login')
.should('be.visible')
.submit();
}
cy.get('.dashboard__card-list')
.should('be.visible');
});
});
Cypress.Commands.add('apiLogin', () => {
cy.api({
url: '/api/admins/login',
method: 'POST',
body: credentials,
failOnStatusCode: true,
}).should(({ status }) => {
cy.wrap(status).should('equal', 200);
});
});

25
cypress/support/index.ts Normal file
View File

@@ -0,0 +1,25 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import '@bahmutov/cy-api';
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.Cookies.defaults({
preserve: 'payload-token',
});

15
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"es5",
"dom"
],
"types": [
"cypress"
]
},
"include": [
"**/*.ts"
]
}

View File

@@ -112,7 +112,8 @@ const DefaultValues: CollectionConfig = {
label: 'Group',
name: 'group',
defaultValue: {
nestedText1: 'neat',
nestedText2: 'nested default text 2',
nestedText3: 'neat',
},
fields: [
{
@@ -122,12 +123,16 @@ const DefaultValues: CollectionConfig = {
name: 'nestedText1',
label: 'Nested Text 1',
type: 'text',
defaultValue: 'nested default text 1',
}, {
defaultValue: 'this should take priority',
},
{
name: 'nestedText2',
label: 'Nested Text 2',
type: 'text',
defaultValue: 'nested default text 2',
},
{
name: 'nestedText3',
type: 'text',
},
],
},
@@ -143,6 +148,7 @@ const DefaultValues: CollectionConfig = {
defaultValue: [
{
arrayText1: 'Get out',
arrayText2: 'Get in',
},
],
fields: [
@@ -281,6 +287,42 @@ const DefaultValues: CollectionConfig = {
children: [{ text: 'Cookin now' }],
}],
},
{
type: 'array',
name: 'asyncArray',
defaultValue: () => {
return [{ child: 'ok' }];
},
fields: [
{
name: 'child',
type: 'text',
defaultValue: () => {
return 'async child';
},
},
],
},
{
name: 'asyncText',
type: 'text',
defaultValue: async (): Promise<string> => {
return new Promise((resolve) => setTimeout(() => {
resolve('asyncFunction');
}, 50));
},
},
{
name: 'function',
type: 'text',
defaultValue: (args): string => {
const { locale } = args;
if (locale === 'en') {
return 'function';
}
return '';
},
},
],
timestamps: true,
};

View File

@@ -21,13 +21,21 @@ const HiddenFields: CollectionConfig = {
hidden: true,
},
required: true,
defaultValue: 'should be hidden from admin, visible in API',
},
{
name: 'hiddenAPI',
type: 'text',
label: 'Hidden on API',
hidden: true,
required: true, // this should not matter
required: true,
hooks: {
beforeValidate: [
({ value }) => {
return value || 'should be hidden from API';
},
],
},
},
],
};

View File

@@ -45,11 +45,12 @@ const Hooks: CollectionConfig = {
],
afterRead: [
((operation) => {
const { doc } = operation;
const { doc, findMany } = operation;
doc.afterReadHook = true;
doc.findMany = findMany;
return doc;
}) as AfterReadHook<Hook & { afterReadHook: boolean }>,
}) as AfterReadHook<Hook & { afterReadHook: boolean, findMany: boolean }>,
],
afterChange: [
((operation) => {

View File

@@ -421,6 +421,7 @@ export interface DefaultValueTest {
group?: {
nestedText1?: string;
nestedText2?: string;
nestedText3?: string;
};
array?: {
arrayText1?: string;
@@ -474,6 +475,12 @@ export interface DefaultValueTest {
richText?: {
[k: string]: unknown;
}[];
asyncArray?: {
child?: string;
id?: string;
}[];
asyncText?: string;
function?: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -615,7 +622,7 @@ export interface RelationshipA {
export interface RelationshipB {
id: string;
title?: string;
disableRelation?: boolean;
disableRelation: boolean;
post?: (string | RelationshipA)[];
postManyRelationships?:
| {

View File

@@ -9,7 +9,7 @@ expressApp.use('/static', express.static(path.resolve(__dirname, 'client/static'
payload.init({
secret: 'SECRET_KEY',
mongoURL: 'mongodb://localhost/payload',
mongoURL: process.env.MONGO_URL || 'mongodb://localhost/payload',
express: expressApp,
email: {
fromName: 'Payload',

View File

@@ -31,7 +31,7 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. |
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |

View File

@@ -32,7 +32,7 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-level 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 response or the Admin panel. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |

View File

@@ -22,7 +22,7 @@ keywords: checkbox, fields, config, configuration, documentation, Content Manage
| **`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, will default to false if field is also `required`. |
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |

View File

@@ -28,7 +28,7 @@ This field uses `prismjs` for syntax highlighting and `react-simple-code-editor`
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -25,7 +25,7 @@ This field uses [`react-datepicker`](https://www.npmjs.com/package/react-datepic
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -23,7 +23,7 @@ keywords: email, fields, config, configuration, documentation, Content Managemen
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -22,7 +22,7 @@ keywords: group, fields, config, configuration, documentation, Content Managemen
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -25,7 +25,7 @@ keywords: number, fields, config, configuration, documentation, Content Manageme
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -7,7 +7,8 @@ keywords: overview, fields, config, configuration, documentation, Content Manage
---
<Banner type="info">
Fields are the building blocks of Payload. Collections and Globals both use Fields to define the shape of the data that they store. Payload offers a wide variety of field types - both simple and complex.
Fields are the building blocks of Payload. Collections and Globals both use Fields to define the shape of the data
that they store. Payload offers a wide variety of field types - both simple and complex.
</Banner>
Fields are defined as an array on Collections and Globals via the `fields` key. They define the shape of the data that will be stored as well as automatically construct the corresponding Admin UI.
@@ -75,6 +76,7 @@ There are two arguments available to custom validation functions.
| `operation` | Will be "create" or "update" depending on the UI action or API call |
| `id` | The value of the collection `id`, will be `undefined` on create request |
| `user` | The currently authenticated user object |
| `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. |
Example:
```js
@@ -227,6 +229,38 @@ const customFieldWithCondition = {
```
### Default values
Fields can be prefilled with starting values using the `defaultValue` property. This is used in the admin UI and also on the backend as API requests will be populated with missing or undefined field values. You can assign the defaultValue directly in the field configuration or supply a function for dynamic behavior. Values assigned during a create request on the server are added before validation occurs.
Functions are called with an optional argument object containing:
- `user` - the authenticated user object
- `locale` - the currently selected locale string
Here is an example of a defaultValue function that uses both:
```js
const translation: {
en: 'Written by',
es: 'escrito por',
};
const field = {
name: 'attribution',
type: 'text',
admin: {
// highlight-start
defaultValue: ({ user, locale }) => (`${translation[locale]} ${user.name}`)
// highlight-end
}
};
```
<Banner type="success">
You can use async defaultValue functions to fill fields with data from API requests.
</Banner>
### Description
A description can be configured three ways.
@@ -246,7 +280,8 @@ As shown above, you can simply provide a string that will show by the field, but
type: 'text',
maxLength: 20,
admin: {
description: ({ value }) => (`${typeof value === 'string' ? 20 - value.length : '20'} characters left`)
description: ({ value }) =>
(`${typeof value === 'string' ? 20 - value.length : '20'} characters left`)
}
}
]

View File

@@ -26,7 +26,7 @@ The data structure in the database matches the GeoJSON structure to represent po
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -23,7 +23,7 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -33,7 +33,7 @@ keywords: relationship, fields, config, configuration, documentation, Content Ma
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |

View File

@@ -27,7 +27,7 @@ The Admin component is built on the powerful [`slatejs`](https://docs.slatejs.or
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -25,7 +25,7 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |

View File

@@ -25,7 +25,7 @@ keywords: text, fields, config, configuration, documentation, Content Management
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -25,7 +25,7 @@ keywords: textarea, fields, config, configuration, documentation, Content Manage
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |

View File

@@ -37,7 +37,7 @@ keywords: upload, images media, fields, config, configuration, documentation, Co
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |

View File

@@ -133,6 +133,7 @@ const afterReadHook = async ({
doc, // full document data
req, // full express request
query, // JSON formatted query
findMany, // boolean to denote if this hook is running against finding one, or finding many
}) => {
return doc;
}

View File

@@ -56,12 +56,13 @@ Field Hooks receive one `args` argument that contains the following properties:
| Option | Description |
| ----------------- | -------------|
| **`value`** | The value of the field. |
| **`data`** | The data passed to update the document within `create` and `update` operations, and the full document itself in the `afterRead` hook. |
| **`siblingData`** | The sibling data passed to a field that the hook is running against. |
| **`originalDoc`** | The full original document in `update` operations. |
| **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many within the `afterRead` hook. |
| **`operation`** | A string relating to which operation the field type is currently executing within. Useful within `beforeValidate`, `beforeChange`, and `afterChange` hooks to differentiate between `create` and `update` operations. |
| **`originalDoc`** | The full original document in `update` operations. |
| **`req`** | The Express `request` object. It is mocked for Local API operations. |
| **`siblingData`** | The sibling data passed to a field that the hook is running against. |
| **`value`** | The value of the field. |
#### Return value

View File

@@ -96,6 +96,7 @@ Runs as the last step before a global is returned. Flattens locales, hides prote
const afterReadHook = async ({
doc, // full document data
req, // full express request
findMany, // boolean to denote if this hook is running against finding one, or finding many (useful in versions)
}) => {...}
```

View File

@@ -10,11 +10,11 @@ keywords: abuse, production, config, configuration, documentation, Content Manag
Payload has built-in security best practices that can be configured to your application-specific needs.
#### Limit Failed Login Attempts
### Limit Failed Login Attempts
Set the max number of failed login attempts before a user account is locked out for a period of time. Set the `maxLoginAttempts` on the collections that feature Authentication to a reasonable but low number for your users to get in. Use the `lockTime` to set a number in milliseconds from the time a user fails their last allowed attempt that a user must wait to try again.
#### Rate Limiting Requests
### Rate Limiting Requests
To prevent DDoS, brute-force, and similar attacks, you can set IP-based rate limits so that once a certain threshold of requests has been hit by a single IP, further requests from the same IP will be ignored. The Payload config `rateLimit` property accepts an object with the following properties:
@@ -30,21 +30,32 @@ To prevent DDoS, brute-force, and similar attacks, you can set IP-based rate lim
Very commonly, NodeJS apps are served behind `nginx` reverse proxies and similar. If you use rate-limiting while you're behind a proxy, <strong>all</strong> IP addresses from everyone that uses your API will appear as if they are from a local origin (127.0.0.1), and your users will get rate-limited very quickly without cause. If you plan to host your app behind a proxy, make sure you set <strong>trustProxy</strong> to <strong>true</strong>.
</Banner>
#### Max Depth
### Max Depth
Querying a collection and automatically including related documents via `depth` incurs a performance cost. Also, it's possible that your configs may have circular relationships, meaning scenarios where an infinite amount of relationships might populate back and forth until your server times out and crashes. You can prevent any potential of depth-related issues by setting a `maxDepth` property on your Payload config.. The maximum allowed depth should be as small as possible without interrupting dev experience, and it defaults to `10`.
#### Cross-Site Request Forgery (CSRF)
### Cross-Site Request Forgery (CSRF)
CSRF prevention will verify the authenticity of each request to your API to prevent a malicious action from another site from authorized users. See how to configure CSRF [here](/docs/authentication/overview#csrf-protection).
#### Cross Origin Resource Sharing (CORS)
### Cross Origin Resource Sharing (CORS)
To securely allow headless operation you will need to configure the allowed origins for requests to be able to use the Payload API. You can see how to set CORS as well as other payload configuration settings [here](/docs/configuration/overview)
### Limiting GraphQL Complexity
Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way specify complexity limits which are based on a complexity score that is calculated for each request.
Any GraphQL request that is calculated to be too expensive is rejected. On the Payload config, in `graphQL` you can set the `maxComplexity` value as an integer. For reference, the default complexity value for each added field is 1, and all `relationship` and `upload` fields are assigned a value of 10.
If you do not need GraphQL it is advised that you disable it altogether with the Payload config by setting `graphQL.disable: true`. Should you wish to enable GraphQL again, you can remove this property or set it `false`, any time. By turning it off, Payload will bypass creating schemas from your collections and will not register the express route.
### Malicious File Uploads
Payload does not execute uploaded files on the server, but depending on your setup it may be used to transmit and store potentially dangerous files. If your configuration allows file uploads there is the potential that a bad actor uploads a malicious file that is then served to other users. Consider the following ways to mitigate the risks.
First, enable email [verification](/docs/authentication/config#email-verification) when users are allowed to register new accounts and add other bot prevention services.
Review that `create` and `update` access on file upload collections are as restrictive as your application needs allow. Consider limiting `read` access of uploaded user's files and how you might limit user uploaded files from being served outside of Payload.
You can also add a [3rd party library](https://github.com/Cisco-Talos/clamav) to scan files in a [hook](/docs/hooks/collections) or have antivirus software in place.

View File

@@ -64,7 +64,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 `<longitude>, <latitude>, <maxDistance in meters (nullable)>, <minDistance in meters (nullable)>`. |
| `near` | For distance related to a [point field](/docs/fields/point) comma separated as `<longitude>, <latitude>, <maxDistance in meters (nullable)>, <minDistance in meters (nullable)>`. |
<Banner type="success">
<strong>Tip</strong>:<br/>

View File

@@ -65,7 +65,7 @@ Globals cannot be created or deleted, so there are only two REST endpoints opene
## Preferences
In addition to the dynamically generated endpoints above Payload also has REST endpoints to manage the admin user [preferences](/docs/admin#preferences) for data specific to the authenticated user.
In addition to the dynamically generated endpoints above Payload also has REST endpoints to manage the admin user [preferences](/docs/admin/overview#preferences) for data specific to the authenticated user.
| Method | Path | Description |
| -------- | --------------------------- | ----------------------- |

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "0.15.7",
"version": "0.16.3",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "SEE LICENSE IN license.md",
"author": {
@@ -41,8 +41,14 @@
"pretest": "yarn build",
"test:int": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.ts NODE_ENV=test DISABLE_LOGGING=true jest --forceExit --runInBand",
"test:client": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.ts NODE_ENV=test jest --config=jest.react.config.js",
"cy:open": "cypress open",
"cy:run": "cypress run",
"test:e2e": "DISABLE_LOGGING=true MONGO_URL=mongodb://localhost/payload-integration start-server-and-test dev http-get://localhost:3000/admin cy:run",
"clean": "rimraf dist",
"release": "release-it",
"release:patch": "release-it patch",
"release:minor": "release-it minor",
"release:major": "release-it major",
"release:beta": "release-it prepatch --config .release-it.beta.json",
"lint": "eslint \"src/**/*.ts\""
},
@@ -83,12 +89,10 @@
"@babel/register": "^7.11.5",
"@date-io/date-fns": "^2.10.6",
"@faceless-ui/collapsibles": "^1.0.0",
"@faceless-ui/modal": "^1.1.4",
"@faceless-ui/modal": "^1.1.7",
"@faceless-ui/scroll-info": "^1.2.3",
"@faceless-ui/window-info": "^1.2.4",
"@payloadcms/config-provider": "^1.0.0",
"@types/mime": "^2.0.3",
"@types/passport-local-mongoose": "^6.1.0",
"@faceless-ui/window-info": "^2.0.2",
"@payloadcms/config-provider": "^1.0.1",
"babel-jest": "^26.3.0",
"babel-loader": "^8.1.0",
"body-parser": "^1.19.0",
@@ -139,7 +143,7 @@
"passport-headerapikey": "^1.2.1",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^6.0.1",
"passport-local-mongoose": "^7.0.0",
"path-browserify": "^1.0.1",
"pino": "^6.4.1",
"pino-pretty": "^4.3.0",
@@ -184,6 +188,7 @@
"webpack-hot-middleware": "^2.25.0"
},
"devDependencies": {
"@bahmutov/cy-api": "^2.1.3",
"@release-it/conventional-changelog": "^2.0.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^13.0.1",
@@ -211,6 +216,7 @@
"@types/json-schema": "^7.0.9",
"@types/jsonwebtoken": "^8.5.0",
"@types/method-override": "^0.0.31",
"@types/mime": "^2.0.3",
"@types/mini-css-extract-plugin": "^1.2.1",
"@types/minimist": "^1.2.1",
"@types/mkdirp": "^1.0.1",
@@ -230,10 +236,10 @@
"@types/prop-types": "^15.7.3",
"@types/qs": "^6.9.5",
"@types/qs-middleware": "^1.0.1",
"@types/react": "^17.0.0",
"@types/react": "^18.0.5",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-datepicker": "^3.1.1",
"@types/react-dom": "^17.0.0",
"@types/react-dom": "^18.0.1",
"@types/react-helmet": "^6.1.0",
"@types/react-router-dom": "^5.1.6",
"@types/react-select": "^3.0.26",
@@ -252,6 +258,7 @@
"babel-plugin-ignore-html-and-css-imports": "^0.1.0",
"copyfiles": "^2.4.0",
"cross-env": "^7.0.2",
"cypress": "9.2.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.0",
"eslint-plugin-jest": "^23.16.0",
@@ -268,6 +275,7 @@
"release-it": "^14.2.2",
"rimraf": "^3.0.2",
"serve-static": "^1.14.2",
"start-server-and-test": "^1.14.0",
"typescript": "^4.1.2"
},
"files": [

View File

@@ -16,4 +16,5 @@ export type RenderedTypeProps = {
className?: string
onClick?: onClick
to: string
children?: React.ReactNode
}

View File

@@ -9,6 +9,7 @@ export type Props = {
}
export type RenderedTypeProps = {
children: React.ReactNode
className?: string,
to: string,
onClick?: () => void,

View File

@@ -155,7 +155,6 @@ const Popup: React.FC<Props> = (props) => {
className={`${baseClass}__wrap`}
// TODO: color ::after with bg color
>
<div
className={`${baseClass}__scroll`}
style={{

View File

@@ -2,7 +2,7 @@ import { CSSProperties } from 'react';
export type Props = {
className?: string
render?: (any) => void,
render?: (any) => React.ReactNode,
children?: React.ReactNode,
verticalAlign?: 'top' | 'bottom'
horizontalAlign?: 'left' | 'center' | 'right',

View File

@@ -4,7 +4,7 @@ export type Options = OptionsType<Value> | GroupedOptionsType<Value>;
export type Value = {
label: string
value: string
value: string | null
options?: Options
}

View File

@@ -32,7 +32,7 @@ const UploadCard: React.FC<Props> = (props) => {
collection={collection}
/>
<div className={`${baseClass}__filename`}>
{doc?.filename}
{typeof doc?.filename === 'string' ? doc?.filename : '[Untitled]'}
</div>
</div>
);

View File

@@ -1,6 +1,6 @@
import React, { useReducer, useState, useCallback, useEffect } from 'react';
import { useConfig } from '@payloadcms/config-provider';
import { Props, Option, ValueWithRelation } from './types';
import { Props, Option, ValueWithRelation, GetResults } from './types';
import optionsReducer from './optionsReducer';
import useDebounce from '../../../../../hooks/useDebounce';
import ReactSelect from '../../../ReactSelect';
@@ -38,7 +38,7 @@ const RelationshipField: React.FC<Props> = (props) => {
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection });
}, [collections, hasMultipleRelations]);
const getResults = useCallback(async ({
const getResults = useCallback<GetResults>(async ({
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
lastLoadedPage: lastLoadedPageArg,
search: searchArg,

View File

@@ -33,3 +33,5 @@ export type ValueWithRelation = {
relationTo: string
value: string
}
export type GetResults = (args: { lastFullyLoadedRelation?: number, lastLoadedPage?: number, search?: string }) => Promise<void>

View File

@@ -80,7 +80,6 @@ const DraggableSection: React.FC<Props> = (props) => {
<HiddenInput
name={`${parentPath}.${rowIndex}.id`}
value={id}
modifyForm={false}
/>
<SectionTitle
label={label}

View File

@@ -1,17 +0,0 @@
import buildStateFromSchema from './buildStateFromSchema';
describe('Form - buildStateFromSchema', () => {
it('populates default value - normal fields', async () => {
const defaultValue = 'Default';
const fieldSchema = [
{
name: 'text',
type: 'text',
label: 'Text',
defaultValue,
},
];
const state = await buildStateFromSchema({ fieldSchema });
expect(state.text.value).toBe(defaultValue);
});
});

View File

@@ -1,190 +0,0 @@
import ObjectID from 'bson-objectid';
import { User } from '../../../../auth';
import {
Field as FieldSchema,
fieldAffectsData,
FieldAffectingData,
fieldIsPresentationalOnly,
ValidateOptions,
} from '../../../../fields/config/types';
import { Fields, Field, Data } from './types';
const buildValidationPromise = async (fieldState: Field, options: ValidateOptions<unknown, unknown, unknown>) => {
const validatedFieldState = fieldState;
let validationResult: boolean | string = true;
if (typeof fieldState.validate === 'function') {
validationResult = await fieldState.validate(fieldState.value, options);
}
if (typeof validationResult === 'string') {
validatedFieldState.errorMessage = validationResult;
validatedFieldState.valid = false;
} else {
validatedFieldState.valid = true;
}
};
type Args = {
fieldSchema: FieldSchema[]
data?: Data,
siblingData?: Data,
user?: User,
id?: string | number,
operation?: 'create' | 'update'
}
const buildStateFromSchema = async (args: Args): Promise<Fields> => {
const {
fieldSchema,
data: fullData = {},
user,
id,
operation,
} = args;
if (fieldSchema) {
const validationPromises = [];
const structureFieldState = (field, passesCondition, data = {}) => {
const value = typeof data?.[field.name] !== 'undefined' ? data[field.name] : field.defaultValue;
const fieldState = {
value,
initialValue: value,
valid: true,
validate: field.validate,
condition: field.admin?.condition,
passesCondition,
};
validationPromises.push(buildValidationPromise(fieldState, {
...field,
fullData,
user,
siblingData: data,
id,
operation,
}));
return fieldState;
};
const iterateFields = (fields: FieldSchema[], data: Data, parentPassesCondition: boolean, path = '') => fields.reduce((state, field) => {
let initialData = data;
if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) {
if (fieldAffectsData(field) && field.defaultValue && typeof initialData?.[field.name] === 'undefined') {
initialData = { [field.name]: field.defaultValue };
}
const passesCondition = Boolean((field?.admin?.condition ? field.admin.condition(fullData || {}, initialData || {}) : true) && parentPassesCondition);
if (fieldAffectsData(field)) {
if (field.type === 'relationship' && initialData?.[field.name] === null) {
initialData[field.name] = 'null';
}
if (field.type === 'array' || field.type === 'blocks') {
if (Array.isArray(initialData?.[field.name])) {
const rows = initialData[field.name] as Data[];
if (field.type === 'array') {
return {
...state,
...rows.reduce((rowState, row, i) => {
const rowPath = `${path}${field.name}.${i}.`;
return {
...rowState,
[`${rowPath}id`]: {
value: row.id,
initialValue: row.id || new ObjectID().toHexString(),
valid: true,
},
...iterateFields(field.fields, row, passesCondition, rowPath),
};
}, {}),
};
}
if (field.type === 'blocks') {
return {
...state,
...rows.reduce((rowState, row, i) => {
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
const rowPath = `${path}${field.name}.${i}.`;
return {
...rowState,
[`${rowPath}blockType`]: {
value: row.blockType,
initialValue: row.blockType,
valid: true,
},
[`${rowPath}blockName`]: {
value: row.blockName,
initialValue: row.blockName,
valid: true,
},
[`${rowPath}id`]: {
value: row.id,
initialValue: row.id || new ObjectID().toHexString(),
valid: true,
},
...(block?.fields ? iterateFields(block.fields, row, passesCondition, rowPath) : {}),
};
}, {}),
};
}
}
return state;
}
// Handle non-array-based nested fields (group, etc)
if (field.type === 'group') {
const subFieldData = initialData?.[field.name] as Data;
return {
...state,
...iterateFields(field.fields, subFieldData, passesCondition, `${path}${field.name}.`),
};
}
return {
...state,
[`${path}${field.name}`]: structureFieldState(field, passesCondition, data),
};
}
// Handle field types that do not use names (row, etc)
if (field.type === 'row') {
return {
...state,
...iterateFields(field.fields, data, passesCondition, path),
};
}
const namedField = field as FieldAffectingData;
// Handle normal fields
return {
...state,
[`${path}${namedField.name}`]: structureFieldState(field, passesCondition, data),
};
}
return state;
}, {});
const resultingState = iterateFields(fieldSchema, fullData, true);
await Promise.all(validationPromises);
return resultingState;
}
return {};
};
export default buildStateFromSchema;

View File

@@ -0,0 +1,210 @@
/* eslint-disable no-param-reassign */
import ObjectID from 'bson-objectid';
import { User } from '../../../../../auth';
import { NonPresentationalField, fieldAffectsData, fieldHasSubFields } from '../../../../../fields/config/types';
import getValueWithDefault from '../../../../../fields/getDefaultValue';
import { Fields, Field, Data } from '../types';
import { iterateFields } from './iterateFields';
type Args = {
field: NonPresentationalField
locale: string
user: User
state: Fields
path: string
passesCondition: boolean
fieldPromises: Promise<void>[]
id: string | number
operation: 'create' | 'update'
data: Data
fullData: Data
}
export const addFieldStatePromise = async ({
field,
locale,
user,
state,
path,
passesCondition,
fullData,
data,
fieldPromises,
id,
operation,
}: Args): Promise<void> => {
if (fieldAffectsData(field)) {
const fieldState: Field = {
valid: true,
value: undefined,
initialValue: undefined,
validate: field.validate,
condition: field.admin?.condition,
passesCondition,
};
const valueWithDefault = await getValueWithDefault({ value: data[field.name], defaultValue: field.defaultValue, locale, user });
data[field.name] = valueWithDefault;
let validationResult: boolean | string = true;
if (typeof fieldState.validate === 'function') {
validationResult = await fieldState.validate(data[field.name], {
...field,
data: fullData,
user,
siblingData: data,
id,
operation,
});
}
if (typeof validationResult === 'string') {
fieldState.errorMessage = validationResult;
fieldState.valid = false;
} else {
fieldState.valid = true;
}
switch (field.type) {
case 'array': {
const arrayValue = Array.isArray(valueWithDefault) ? valueWithDefault : [];
arrayValue.forEach((row, i) => {
const rowPath = `${path}${field.name}.${i}.`;
state[`${rowPath}id`] = {
value: row.id,
initialValue: row.id || new ObjectID().toHexString(),
valid: true,
};
iterateFields({
state,
fields: field.fields,
data: row,
parentPassesCondition: passesCondition,
path: rowPath,
user,
fieldPromises,
fullData,
id,
locale,
operation,
});
});
// Add values to field state
fieldState.value = arrayValue.length;
fieldState.initialValue = arrayValue.length;
if (arrayValue.length > 0) {
fieldState.disableFormData = true;
}
// Add field to state
state[`${path}${field.name}`] = fieldState;
break;
}
case 'blocks': {
const blocksValue = Array.isArray(valueWithDefault) ? valueWithDefault : [];
blocksValue.forEach((row, i) => {
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
const rowPath = `${path}${field.name}.${i}.`;
if (block) {
state[`${rowPath}id`] = {
value: row.id,
initialValue: row.id || new ObjectID().toHexString(),
valid: true,
};
state[`${rowPath}blockType`] = {
value: row.blockType,
initialValue: row.blockType,
valid: true,
};
state[`${rowPath}blockName`] = {
value: row.blockName,
initialValue: row.blockName,
valid: true,
};
iterateFields({
state,
fields: block.fields,
data: row,
fullData,
parentPassesCondition: passesCondition,
path: rowPath,
user,
locale,
operation,
fieldPromises,
id,
});
}
});
// Add values to field state
fieldState.value = blocksValue.length;
fieldState.initialValue = blocksValue.length;
if (blocksValue.length > 0) {
fieldState.disableFormData = true;
}
// Add field to state
state[`${path}${field.name}`] = fieldState;
break;
}
case 'group': {
iterateFields({
state,
id,
operation,
fieldPromises,
fields: field.fields,
data: data?.[field.name],
fullData,
parentPassesCondition: passesCondition,
path: `${path}${field.name}.`,
locale,
user,
});
break;
}
default: {
fieldState.value = valueWithDefault;
fieldState.initialValue = valueWithDefault;
// Add field to state
state[`${path}${field.name}`] = fieldState;
break;
}
}
} else if (fieldHasSubFields(field)) {
// Handle field types that do not use names (row, etc)
iterateFields({
state,
fields: field.fields,
data,
parentPassesCondition: passesCondition,
path,
user,
fieldPromises,
fullData,
id,
locale,
operation,
});
}
};

View File

@@ -0,0 +1,53 @@
import buildStateFromSchema from './index';
describe('Form - buildStateFromSchema', () => {
const defaultValue = 'Default';
it('populates default value - normal fields', async () => {
const fieldSchema = [
{
name: 'text',
type: 'text',
label: 'Text',
defaultValue,
},
];
const state = await buildStateFromSchema({ fieldSchema });
expect(state.text.value).toBe(defaultValue);
});
it('field value overrides defaultValue - normal fields', async () => {
const value = 'value';
const data = { text: value };
const fieldSchema = [
{
name: 'text',
type: 'text',
label: 'Text',
defaultValue,
},
];
const state = await buildStateFromSchema({ fieldSchema, data });
expect(state.text.value).toBe(value);
});
it('populates default value from a function - normal fields', async () => {
const user = { email: 'user@example.com' };
const locale = 'en';
const fieldSchema = [
{
name: 'text',
type: 'text',
label: 'Text',
defaultValue: (args) => {
if (!args.locale) {
return 'missing locale';
}
if (!args.user) {
return 'missing user';
}
return 'Default';
},
},
];
const state = await buildStateFromSchema({ fieldSchema, user, locale });
expect(state.text.value).toBe(defaultValue);
});
});

View File

@@ -0,0 +1,53 @@
import { User } from '../../../../../auth';
import { Field as FieldSchema } from '../../../../../fields/config/types';
import { Fields, Data } from '../types';
import { iterateFields } from './iterateFields';
type Args = {
fieldSchema: FieldSchema[]
data?: Data,
siblingData?: Data,
user?: User,
id?: string | number,
operation?: 'create' | 'update'
locale: string
}
const buildStateFromSchema = async (args: Args): Promise<Fields> => {
const {
fieldSchema,
data: fullData = {},
user,
id,
operation,
locale,
} = args;
if (fieldSchema) {
const fieldPromises = [];
const state: Fields = {};
iterateFields({
state,
fields: fieldSchema,
id,
locale,
operation,
path: '',
user,
fieldPromises,
data: fullData,
fullData,
parentPassesCondition: true,
});
await Promise.all(fieldPromises);
return state;
}
return {};
};
export default buildStateFromSchema;

View File

@@ -0,0 +1,57 @@
import { User } from '../../../../../auth';
import {
Field as FieldSchema,
fieldIsPresentationalOnly,
} from '../../../../../fields/config/types';
import { Fields, Data } from '../types';
import { addFieldStatePromise } from './addFieldStatePromise';
type Args = {
state: Fields
fields: FieldSchema[]
data: Data
fullData: Data
parentPassesCondition: boolean
path: string
user: User
locale: string
fieldPromises: Promise<void>[]
id: string | number
operation: 'create' | 'update'
}
export const iterateFields = ({
fields,
data,
parentPassesCondition,
path = '',
fullData,
user,
locale,
operation,
fieldPromises,
id,
state,
}: Args): void => {
fields.forEach((field) => {
const initialData = data;
if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) {
const passesCondition = Boolean((field?.admin?.condition ? field.admin.condition(fullData || {}, initialData || {}) : true) && parentPassesCondition);
fieldPromises.push(addFieldStatePromise({
fullData,
id,
locale,
operation,
path,
state,
user,
fieldPromises,
field,
passesCondition,
data,
}));
}
});
};

View File

@@ -18,7 +18,7 @@ const unflattenRowsFromState = (state: Fields, path) => {
// Add value to rowsFromStateObject and delete it from remaining state
Object.keys(state).forEach((key) => {
if (key.indexOf(`${path}.`) === 0) {
if (!state[key].ignoreWhileFlattening) {
if (!state[key].disableFormData) {
const name = key.replace(pathPrefixToRemove, '');
rowsFromStateObject[name] = state[key];
rowsFromStateObject[name].initialValue = rowsFromStateObject[name].value;
@@ -167,7 +167,6 @@ function fieldReducer(state: Fields, action): Fields {
valid: action.valid,
errorMessage: action.errorMessage,
disableFormData: action.disableFormData,
ignoreWhileFlattening: action.ignoreWhileFlattening,
initialValue: action.initialValue,
validate: action.validate,
condition: action.condition,

View File

@@ -20,7 +20,7 @@ import wait from '../../../../utilities/wait';
import { Field } from '../../../../fields/config/types';
import buildInitialState from './buildInitialState';
import errorMessages from './errorMessages';
import { Context as FormContextType, Props, SubmitOptions } from './types';
import { Context as FormContextType, GetDataByPath, Props, SubmitOptions } from './types';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FormWatchContext } from './context';
import buildStateFromSchema from './buildStateFromSchema';
import { useOperation } from '../../utilities/OperationProvider';
@@ -298,7 +298,7 @@ const Form: React.FC<Props> = (props) => {
const getField = useCallback((path: string) => contextRef.current.fields[path], [contextRef]);
const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), [contextRef]);
const getSiblingData = useCallback((path: string) => getSiblingDataFunc(contextRef.current.fields, path), [contextRef]);
const getDataByPath = useCallback((path: string) => getDataByPathFunc(contextRef.current.fields, path), [contextRef]);
const getDataByPath = useCallback<GetDataByPath>((path: string) => getDataByPathFunc(contextRef.current.fields, path), [contextRef]);
const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]);
const createFormData = useCallback((overrides: any = {}) => {
@@ -326,10 +326,10 @@ const Form: React.FC<Props> = (props) => {
}, [contextRef]);
const reset = useCallback(async (fieldSchema: Field[], data: unknown) => {
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation });
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation, locale });
contextRef.current = { ...initContextState } as FormContextType;
dispatchFields({ type: 'REPLACE_STATE', state });
}, [id, user, operation]);
}, [id, user, operation, locale]);
contextRef.current.dispatchFields = dispatchFields;
contextRef.current.submit = submit;
@@ -382,11 +382,6 @@ const Form: React.FC<Props> = (props) => {
baseClass,
].filter(Boolean).join(' ');
if (log) {
// eslint-disable-next-line no-console
console.log(fields);
}
return (
<form
noValidate

View File

@@ -8,7 +8,6 @@ export type Field = {
valid: boolean
validate?: Validate
disableFormData?: boolean
ignoreWhileFlattening?: boolean
condition?: Condition
passesCondition?: boolean
}

View File

@@ -73,7 +73,7 @@ const RenderFields: React.FC<Props> = (props) => {
let { admin: { readOnly } = {} } = field;
if (readOnlyOverride) readOnly = true;
if (readOnlyOverride && readOnly !== false) readOnly = true;
if ((isFieldAffectingData && permissions?.[field?.name]?.read?.permission !== false) || !isFieldAffectingData) {
if (isFieldAffectingData && permissions?.[field?.name]?.[operation]?.permission === false) {

View File

@@ -9,6 +9,7 @@ import reducer from '../rowReducer';
import { useForm } from '../../Form/context';
import buildStateFromSchema from '../../Form/buildStateFromSchema';
import useField from '../../useField';
import { useLocale } from '../../../utilities/Locale';
import Error from '../../Error';
import { array } from '../../../../../fields/validations';
import Banner from '../../../elements/Banner';
@@ -56,6 +57,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const formContext = useForm();
const { user } = useAuth();
const { id } = useDocumentInfo();
const locale = useLocale();
const operation = useOperation();
const { dispatchFields } = formContext;
@@ -77,21 +79,21 @@ const ArrayFieldType: React.FC<Props> = (props) => {
path,
validate: memoizedValidate,
disableFormData,
ignoreWhileFlattening: true,
condition,
});
const addRow = useCallback(async (rowIndex) => {
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user });
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
dispatchRows({ type: 'ADD', rowIndex });
setValue(value as number + 1);
}, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user]);
}, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user, locale]);
const removeRow = useCallback((rowIndex) => {
dispatchRows({ type: 'REMOVE', rowIndex });
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
}, [dispatchRows, dispatchFields, path]);
setValue(value as number - 1);
}, [dispatchRows, dispatchFields, path, value, setValue]);
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
@@ -111,7 +113,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
}, [formContext, path]);
useEffect(() => {
setValue(rows?.length || 0);
setValue(rows?.length || 0, true);
if (rows?.length === 0) {
setDisableFormData(false);

View File

@@ -3,6 +3,7 @@ import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { useAuth } from '@payloadcms/config-provider';
import { usePreferences } from '../../../utilities/Preferences';
import { useLocale } from '../../../utilities/Locale';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
import reducer from '../rowReducer';
@@ -60,6 +61,7 @@ const Blocks: React.FC<Props> = (props) => {
const formContext = useForm();
const { user } = useAuth();
const { id } = useDocumentInfo();
const locale = useLocale();
const operation = useOperation();
const { dispatchFields } = formContext;
@@ -78,19 +80,18 @@ const Blocks: React.FC<Props> = (props) => {
path,
validate: memoizedValidate,
disableFormData,
ignoreWhileFlattening: true,
condition,
});
const addRow = useCallback(async (rowIndex, blockType) => {
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user });
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType });
dispatchRows({ type: 'ADD', rowIndex, blockType });
setValue(value as number + 1);
}, [path, setValue, value, blocks, dispatchFields, operation, id, user]);
}, [path, setValue, value, blocks, dispatchFields, operation, id, user, locale]);
const removeRow = useCallback((rowIndex) => {
dispatchRows({ type: 'REMOVE', rowIndex });
@@ -104,7 +105,7 @@ const Blocks: React.FC<Props> = (props) => {
}, [dispatchRows, dispatchFields, path]);
const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => {
dispatchRows({ type: 'SET_COLLAPSE', rowID, collapsed });
dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed });
if (preferencesKey) {
const preferences: DocumentPreferences = await getPreference(preferencesKey);
@@ -142,13 +143,16 @@ const Blocks: React.FC<Props> = (props) => {
// Get preferences, and once retrieved,
// Reset rows with preferences included
useEffect(() => {
const fetchPreferences = async () => {
const preferences = preferencesKey ? await getPreference<DocumentPreferences>(preferencesKey) : undefined;
const data = formContext.getDataByPath(path);
dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed });
};
const data = formContext.getDataByPath(path);
fetchPreferences();
if (Array.isArray(data)) {
const fetchPreferences = async () => {
const preferences = preferencesKey ? await getPreference<DocumentPreferences>(preferencesKey) : undefined;
dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed });
};
fetchPreferences();
}
}, [formContext, path, preferencesKey, getPreference]);
// Set row count on mount and when form context is reset
@@ -158,7 +162,7 @@ const Blocks: React.FC<Props> = (props) => {
}, [formContext, path]);
useEffect(() => {
setValue(rows?.length || 0);
setValue(rows?.length || 0, true);
if (rows?.length === 0) {
setDisableFormData(false);

View File

@@ -8,7 +8,7 @@ const HiddenInput: React.FC<Props> = (props) => {
name,
path: pathFromProps,
value: valueFromProps,
modifyForm = true,
disableModifyingForm = true,
} = props;
const path = pathFromProps || name;
@@ -19,9 +19,9 @@ const HiddenInput: React.FC<Props> = (props) => {
useEffect(() => {
if (valueFromProps !== undefined) {
setValue(valueFromProps, modifyForm);
setValue(valueFromProps, disableModifyingForm);
}
}, [valueFromProps, setValue, modifyForm]);
}, [valueFromProps, setValue, disableModifyingForm]);
return (
<input

View File

@@ -2,5 +2,5 @@ export type Props = {
name: string
path?: string
value: unknown
modifyForm?: boolean
disableModifyingForm?: false
}

View File

@@ -61,7 +61,7 @@ const Relationship: React.FC<Props> = (props) => {
const { getData, getSiblingData } = useWatchForm();
const formProcessing = useFormProcessing();
const hasMultipleRelations = Array.isArray(relationTo);
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: 'null', label: 'None' }]);
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: null, label: 'None' }]);
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1);
const [lastLoadedPage, setLastLoadedPage] = useState(1);
const [errorLoading, setErrorLoading] = useState('');
@@ -313,6 +313,7 @@ const Relationship: React.FC<Props> = (props) => {
].filter(Boolean).join(' ');
const valueToRender = (findOptionsByValue() || value) as Value;
if (valueToRender?.value === 'null') valueToRender.value = null;
return (
<div

View File

@@ -271,6 +271,7 @@ const RichText: React.FC<Props> = (props) => {
ref={editorRef}
>
<Editable
className={`${baseClass}__input`}
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder={placeholder}

View File

@@ -15,6 +15,7 @@ import Submit from '../../../../../../Submit';
import { Field } from '../../../../../../../../../fields/config/types';
import './index.scss';
import { useLocale } from '../../../../../../../utilities/Locale';
const baseClass = 'edit-upload-modal';
@@ -31,6 +32,7 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
const editor = useSlateStatic();
const [initialState, setInitialState] = useState({});
const { user } = useAuth();
const locale = useLocale();
const handleUpdateEditData = useCallback((fields) => {
const newNode = {
@@ -49,12 +51,12 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
useEffect(() => {
const awaitInitialState = async () => {
const state = await buildStateFromSchema({ fieldSchema, data: element?.fields, user, operation: 'update' });
const state = await buildStateFromSchema({ fieldSchema, data: element?.fields, user, operation: 'update', locale });
setInitialState(state);
};
awaitInitialState();
}, [fieldSchema, element.fields, user]);
}, [fieldSchema, element.fields, user, locale]);
return (
<Modal

View File

@@ -8,6 +8,7 @@ import RenderFields from '../../../RenderFields';
import FormSubmit from '../../../Submit';
import Upload from '../../../../views/collections/Edit/Upload';
import { NegativeFieldGutterProvider } from '../../../FieldTypeGutter/context';
import ViewDescription from '../../../../elements/ViewDescription';
import { Props } from './types';
import './index.scss';
@@ -72,7 +73,9 @@ const AddUploadModal: React.FC<Props> = (props) => {
/>
</div>
{description && (
<div className={`${baseClass}__sub-header`}>{description}</div>
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
<Upload

View File

@@ -12,11 +12,12 @@ import UploadGallery from '../../../../elements/UploadGallery';
import { Props } from './types';
import PerPage from '../../../../elements/PerPage';
import formatFields from '../../../../views/collections/List/formatFields';
import './index.scss';
import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
import { useWatchForm } from '../../../Form/context';
import ViewDescription from '../../../../elements/ViewDescription';
import './index.scss';
const baseClass = 'select-existing-upload-modal';
@@ -117,7 +118,9 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
/>
</div>
{description && (
<div className={`${baseClass}__sub-header`}>{description}</div>
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
<ListControls

View File

@@ -14,7 +14,6 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
validate,
enableDebouncedValue,
disableFormData,
ignoreWhileFlattening,
condition,
} = options;
@@ -66,7 +65,6 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
const fieldToDispatch = {
path,
disableFormData,
ignoreWhileFlattening,
initialValue,
validate,
condition,
@@ -105,7 +103,6 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
getData,
getSiblingData,
id,
ignoreWhileFlattening,
initialValue,
operation,
path,
@@ -116,10 +113,10 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
// Method to return from `useField`, used to
// update internal field values from field component(s)
// as fast as they arrive. NOTE - this method is NOT debounced
const setValue = useCallback((e, modifyForm = true) => {
const setValue = useCallback((e, disableModifyingForm = false) => {
const val = (e && e.target) ? e.target.value : e;
if ((!ignoreWhileFlattening && !modified) && modifyForm) {
if (!modified && !disableModifyingForm) {
if (typeof setModified === 'function') {
setModified(true);
}
@@ -128,7 +125,6 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
}, [
setModified,
modified,
ignoreWhileFlattening,
]);
useEffect(() => {
@@ -143,7 +139,7 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
const valueToSend = enableDebouncedValue ? debouncedValue : internalValue;
useEffect(() => {
if (field?.value !== valueToSend && valueToSend !== undefined) {
if ((field?.value !== valueToSend && valueToSend !== undefined) || disableFormData !== field?.disableFormData) {
sendField(valueToSend);
}
}, [
@@ -151,6 +147,7 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
valueToSend,
sendField,
field,
disableFormData,
]);
return {

View File

@@ -64,12 +64,12 @@ const AccountView: React.FC = () => {
useEffect(() => {
const awaitInitialState = async () => {
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, operation: 'update', id, user });
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, operation: 'update', id, user, locale });
setInitialState(state);
};
awaitInitialState();
}, [dataToRender, fields, id, user]);
}, [dataToRender, fields, id, user, locale]);
return (
<NegativeFieldGutterProvider allow>

View File

@@ -45,9 +45,9 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const onSave = useCallback(async (json) => {
getVersions();
const state = await buildStateFromSchema({ fieldSchema: fields, data: json.result, operation: 'update', user });
const state = await buildStateFromSchema({ fieldSchema: fields, data: json.result, operation: 'update', user, locale });
setInitialState(state);
}, [getVersions, fields, user]);
}, [getVersions, fields, user, locale]);
const [{ data, isLoading }] = usePayloadAPI(
`${serverURL}${api}/globals/${slug}`,
@@ -66,12 +66,12 @@ const GlobalView: React.FC<IndexProps> = (props) => {
useEffect(() => {
const awaitInitialState = async () => {
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: 'update' });
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: 'update', locale });
setInitialState(state);
};
awaitInitialState();
}, [dataToRender, fields, user]);
}, [dataToRender, fields, user, locale]);
const globalPermissions = permissions?.globals?.[slug];

View File

@@ -33,7 +33,7 @@ const CompareVersion: React.FC<Props> = (props) => {
const getResults = useCallback(async ({
lastLoadedPage: lastLoadedPageArg,
} = {}) => {
}) => {
const query: {
[key: string]: unknown
where: Where

View File

@@ -50,10 +50,10 @@ const EditView: React.FC<IndexProps> = (props) => {
if (!isEditing) {
history.push(`${admin}/collections/${collection.slug}/${json?.doc?.id}`);
} else {
const state = await buildStateFromSchema({ fieldSchema: collection.fields, data: json.doc, user, id, operation: 'update' });
const state = await buildStateFromSchema({ fieldSchema: collection.fields, data: json.doc, user, id, operation: 'update', locale });
setInitialState(state);
}
}, [admin, collection, history, isEditing, getVersions, user, id]);
}, [admin, collection, history, isEditing, getVersions, user, id, locale]);
const [{ data, isLoading, isError }] = usePayloadAPI(
(isEditing ? `${serverURL}${api}/${slug}/${id}` : null),
@@ -96,13 +96,16 @@ const EditView: React.FC<IndexProps> = (props) => {
}, [setStepNav, isEditing, pluralLabel, dataToRender, slug, useAsTitle, admin]);
useEffect(() => {
if (isLoading) {
return;
}
const awaitInitialState = async () => {
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: isEditing ? 'update' : 'create', id });
const state = await buildStateFromSchema({ fieldSchema: fields, data: dataToRender, user, operation: isEditing ? 'update' : 'create', id, locale });
setInitialState(state);
};
awaitInitialState();
}, [dataToRender, fields, isEditing, id, user]);
}, [dataToRender, fields, isEditing, id, user, locale, isLoading]);
if (isError) {
return (

View File

@@ -20,7 +20,7 @@ const DefaultCell: React.FC<Props> = (props) => {
const { routes: { admin } } = useConfig();
let WrapElement: React.ComponentType | string = 'span';
let WrapElement: React.ComponentType<any> | string = 'span';
const wrapElementProps: {
to?: string

View File

@@ -2,7 +2,7 @@
// @ts-ignore - need to do this because this file doesn't actually exist
import config from 'payload-config';
import React from 'react';
import { render } from 'react-dom';
import { createRoot } from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import { ScrollInfoProvider } from '@faceless-ui/scroll-info';
import { WindowInfoProvider } from '@faceless-ui/window-info';
@@ -22,10 +22,10 @@ const Index = () => (
<React.Fragment>
<ConfigProvider config={config}>
<WindowInfoProvider breakpoints={{
xs: 400,
s: 768,
m: 1024,
l: 1440,
xs: '(max-width: 400px)',
s: '(max-width: 768px)',
m: '(max-width: 1024px)',
l: '(max-width: 1440px)',
}}
>
<ScrollInfoProvider>
@@ -61,7 +61,9 @@ const Index = () => (
</React.Fragment>
);
render(<Index />, document.getElementById('app'));
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<Index />);
// Needed for Hot Module Replacement
if (typeof (module.hot) !== 'undefined') {

View File

@@ -9,6 +9,7 @@ import { Field, fieldHasSubFields, fieldAffectsData } from '../../fields/config/
import { User } from '../types';
import { Collection } from '../../collections/config/types';
import { Payload } from '../..';
import { afterRead } from '../../fields/hooks/afterRead';
export type Result = {
user?: User,
@@ -171,15 +172,12 @@ async function login(this: Payload, incomingArgs: Arguments): Promise<Result> {
// afterRead - Fields
// /////////////////////////////////////
user = await this.performFieldOperations(collectionConfig, {
user = await afterRead({
depth,
req,
id: user.id,
data: user,
hook: 'afterRead',
operation: 'read',
doc: user,
entityConfig: collectionConfig,
overrideAccess,
flattenLocales: true,
req,
showHiddenFields,
});

View File

@@ -1,15 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DeepRequired } from 'ts-essentials';
import { PaginateModel, PassportLocalModel } from 'mongoose';
import { PaginateModel } from 'mongoose';
import { GraphQLType } from 'graphql';
import { Access, GeneratePreviewURL } from '../../config/types';
import { Access, GeneratePreviewURL, EntityDescription } from '../../config/types';
import { Field } from '../../fields/config/types';
import { PayloadRequest } from '../../express/types';
import { IncomingAuthType, Auth } from '../../auth/types';
import { IncomingUploadType, Upload } from '../../uploads/types';
import { IncomingCollectionVersions, SanitizedCollectionVersions } from '../../versions/types';
export interface CollectionModel extends PaginateModel<any>, PassportLocalModel<any> {
type Register<T = any> = (doc: T, password: string) => T;
interface PassportLocalModel {
register: Register
authenticate: any
}
export interface CollectionModel extends PaginateModel<any>, PassportLocalModel {
buildQuery: (query: unknown, locale?: string) => Record<string, unknown>
}
@@ -87,6 +94,7 @@ export type AfterReadHook<T extends TypeWithID = any> = (args: {
doc: T;
req: PayloadRequest;
query?: { [key: string]: any };
findMany?: boolean
}) => any;
export type BeforeDeleteHook = (args: {
@@ -128,7 +136,7 @@ export type CollectionAdminOptions = {
/**
* Custom description for collection
*/
description?: string | (() => string) | React.FC;
description?: EntityDescription;
disableDuplicate?: boolean;
/**
* Hide the API URL within the Edit view

View File

@@ -12,6 +12,10 @@ import { Document } from '../../types';
import { Payload } from '../..';
import { fieldAffectsData } from '../../fields/config/types';
import uploadFile from '../../uploads/uploadFile';
import { beforeChange } from '../../fields/hooks/beforeChange';
import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
collection: Collection
@@ -99,12 +103,13 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
// beforeValidate - Fields
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data = await beforeValidate({
data,
req,
hook: 'beforeValidate',
doc: {},
entityConfig: collectionConfig,
operation: 'create',
overrideAccess,
req,
});
// /////////////////////////////////////
@@ -139,13 +144,13 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
// beforeChange - Fields
// /////////////////////////////////////
const resultWithLocales = await this.performFieldOperations(collectionConfig, {
const resultWithLocales = await beforeChange({
data,
hook: 'beforeChange',
doc: {},
docWithLocales: {},
entityConfig: collectionConfig,
operation: 'create',
req,
overrideAccess,
unflattenLocales: true,
skipValidation: shouldSaveDraft,
});
@@ -214,14 +219,12 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
// afterRead - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
result = await afterRead({
depth,
req,
data: result,
hook: 'afterRead',
operation: 'create',
doc: result,
entityConfig: collectionConfig,
overrideAccess,
flattenLocales: true,
req,
showHiddenFields,
});
@@ -242,14 +245,12 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
// afterChange - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
data: result,
hook: 'afterChange',
result = await afterChange({
data,
doc: result,
entityConfig: collectionConfig,
operation: 'create',
req,
depth,
overrideAccess,
showHiddenFields,
});
// /////////////////////////////////////

View File

@@ -10,6 +10,7 @@ import { Document, Where } from '../../types';
import { hasWhereAccessResult } from '../../auth/types';
import { FileData } from '../../uploads/types';
import fileExists from '../../uploads/fileExists';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
depth?: number
@@ -173,14 +174,12 @@ async function deleteQuery(incomingArgs: Arguments): Promise<Document> {
// afterRead - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
result = await afterRead({
depth,
req,
data: result,
hook: 'afterRead',
operation: 'delete',
doc: result,
entityConfig: collectionConfig,
overrideAccess,
flattenLocales: true,
req,
showHiddenFields,
});

View File

@@ -9,6 +9,7 @@ import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
import { buildSortParam } from '../../mongoose/buildSortParam';
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable';
import { AccessResult } from '../../config/types';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
collection: Collection
@@ -169,20 +170,15 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
result = {
...result,
docs: await Promise.all(result.docs.map(async (data) => this.performFieldOperations(
collectionConfig,
{
depth,
data,
req,
id: data.id,
hook: 'afterRead',
operation: 'read',
overrideAccess,
flattenLocales: true,
showHiddenFields,
},
))),
docs: await Promise.all(result.docs.map(async (doc) => afterRead<T>({
depth,
doc,
entityConfig: collectionConfig,
overrideAccess,
req,
showHiddenFields,
findMany: true,
}))),
};
// /////////////////////////////////////
@@ -197,7 +193,7 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook;
docRef = await hook({ req, query, doc }) || doc;
docRef = await hook({ req, query, doc, findMany: true }) || doc;
}, Promise.resolve());
return docRef;

View File

@@ -9,6 +9,7 @@ import executeAccess from '../../auth/executeAccess';
import { Where } from '../../types';
import { hasWhereAccessResult } from '../../auth/types';
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
collection: Collection
@@ -149,16 +150,13 @@ async function findByID<T extends TypeWithID = any>(this: Payload, incomingArgs:
// afterRead - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
depth,
req,
id,
data: result,
hook: 'afterRead',
operation: 'read',
result = await afterRead({
currentDepth,
doc: result,
depth,
entityConfig: collectionConfig,
overrideAccess,
flattenLocales: true,
req,
showHiddenFields,
});

View File

@@ -9,6 +9,7 @@ import executeAccess from '../../auth/executeAccess';
import { Where } from '../../types';
import { hasWhereAccessResult } from '../../auth/types';
import { TypeWithVersion } from '../../versions/types';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
collection: Collection
@@ -113,16 +114,13 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(this: Payload
// afterRead - Fields
// /////////////////////////////////////
result.version = await this.performFieldOperations(collectionConfig, {
depth,
req,
id,
data: result.version,
hook: 'afterRead',
operation: 'read',
result.version = await afterRead({
currentDepth,
depth,
doc: result.version,
entityConfig: collectionConfig,
overrideAccess,
flattenLocales: true,
req,
showHiddenFields,
});

View File

@@ -9,6 +9,7 @@ import { buildSortParam } from '../../mongoose/buildSortParam';
import { PaginatedDocs } from '../../mongoose/types';
import { TypeWithVersion } from '../../versions/types';
import { Payload } from '../../index';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
collection: Collection
@@ -131,21 +132,15 @@ async function findVersions<T extends TypeWithVersion<T> = any>(this: Payload, a
...result,
docs: await Promise.all(result.docs.map(async (data) => ({
...data,
version: await this.performFieldOperations(
collectionConfig,
{
depth,
data: data.version,
req,
id: data.version.id,
hook: 'afterRead',
operation: 'read',
overrideAccess,
flattenLocales: true,
showHiddenFields,
isVersion: true,
},
),
version: await afterRead({
depth,
doc: data.version,
entityConfig: collectionConfig,
overrideAccess,
req,
showHiddenFields,
findMany: true,
}),
}))),
};
@@ -161,7 +156,7 @@ async function findVersions<T extends TypeWithVersion<T> = any>(this: Payload, a
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook;
docRef.version = await hook({ req, query, doc: doc.version }) || doc.version;
docRef.version = await hook({ req, query, doc: doc.version, findMany: true }) || doc.version;
}, Promise.resolve());
return docRef;

View File

@@ -8,6 +8,8 @@ import { Payload } from '../../index';
import { hasWhereAccessResult } from '../../auth/types';
import { Where } from '../../types';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { afterChange } from '../../fields/hooks/afterChange';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
collection: Collection
@@ -114,15 +116,12 @@ async function restoreVersion<T extends TypeWithID = any>(this: Payload, args: A
// afterRead - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
id: parentDocID,
result = await afterRead({
depth,
doc: result,
entityConfig: collectionConfig,
req,
data: result,
hook: 'afterRead',
operation: 'update',
overrideAccess,
flattenLocales: true,
showHiddenFields,
});
@@ -143,15 +142,12 @@ async function restoreVersion<T extends TypeWithID = any>(this: Payload, args: A
// afterChange - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
result = await afterChange({
data: result,
hook: 'afterChange',
doc: result,
entityConfig: collectionConfig,
operation: 'update',
req,
id: parentDocID,
depth,
overrideAccess,
showHiddenFields,
});
// /////////////////////////////////////

View File

@@ -12,6 +12,10 @@ import { saveCollectionVersion } from '../../versions/saveCollectionVersion';
import uploadFile from '../../uploads/uploadFile';
import cleanUpFailedVersion from '../../versions/cleanUpFailedVersion';
import { ensurePublishedCollectionVersion } from '../../versions/ensurePublishedCollectionVersion';
import { beforeChange } from '../../fields/hooks/beforeChange';
import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange';
import { afterRead } from '../../fields/hooks/afterRead';
export type Arguments = {
collection: Collection
@@ -110,15 +114,12 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
docWithLocales = JSON.stringify(docWithLocales);
docWithLocales = JSON.parse(docWithLocales);
const originalDoc = await this.performFieldOperations(collectionConfig, {
id,
const originalDoc = await afterRead({
depth: 0,
doc: docWithLocales,
entityConfig: collectionConfig,
req,
data: docWithLocales,
hook: 'afterRead',
operation: 'update',
overrideAccess: true,
flattenLocales: true,
showHiddenFields,
});
@@ -139,14 +140,14 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
// beforeValidate - Fields
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data = await beforeValidate({
data,
req,
doc: originalDoc,
entityConfig: collectionConfig,
id,
originalDoc,
hook: 'beforeValidate',
operation: 'update',
overrideAccess,
req,
});
// // /////////////////////////////////////
@@ -183,16 +184,14 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
// beforeChange - Fields
// /////////////////////////////////////
let result = await this.performFieldOperations(collectionConfig, {
let result = await beforeChange({
data,
req,
id,
originalDoc,
hook: 'beforeChange',
operation: 'update',
overrideAccess,
unflattenLocales: true,
doc: originalDoc,
docWithLocales,
entityConfig: collectionConfig,
id,
operation: 'update',
req,
skipValidation: shouldSaveDraft,
});
@@ -266,8 +265,8 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
: error;
}
result = JSON.stringify(result);
result = JSON.parse(result);
const resultString = JSON.stringify(result);
result = JSON.parse(resultString);
// custom id type reset
result.id = result._id;
@@ -279,15 +278,12 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
// afterRead - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
id,
result = await afterRead({
depth,
doc: result,
entityConfig: collectionConfig,
req,
data: result,
hook: 'afterRead',
operation: 'update',
overrideAccess,
flattenLocales: true,
showHiddenFields,
});
@@ -308,15 +304,12 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
// afterChange - Fields
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
data: result,
hook: 'afterChange',
result = await afterChange({
data,
doc: result,
entityConfig: collectionConfig,
operation: 'update',
req,
id,
depth,
overrideAccess,
showHiddenFields,
});
// /////////////////////////////////////

View File

@@ -0,0 +1,109 @@
import getConfig from '../../config/load';
import { email, password } from '../../mongoose/testCredentials';
require('isomorphic-fetch');
const { serverURL: url } = getConfig();
let token = null;
let headers = null;
describe('DefaultValue - REST', () => {
beforeAll(async (done) => {
const response = await fetch(`${url}/api/admins/login`, {
body: JSON.stringify({
email,
password,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'post',
});
const data = await response.json();
({ token } = data);
headers = {
Authorization: `JWT ${token}`,
'Content-Type': 'application/json',
};
done();
});
describe('DefaultValues', () => {
let document;
beforeAll(async (done) => {
const result = await fetch(`${url}/api/default-values`, {
body: JSON.stringify({}),
headers,
method: 'post',
});
const data = await result.json();
document = data.doc;
done();
});
it('should create with defaultValues saved', async () => {
expect(document.id).toBeDefined();
expect(document.function).toStrictEqual('function');
expect(document.asyncText).toStrictEqual('asyncFunction');
expect(document.array[0].arrayText1).toStrictEqual('Get out');
expect(document.group.nestedText1).toStrictEqual('this should take priority');
expect(document.group.nestedText2).toStrictEqual('nested default text 2');
expect(document.group.nestedText3).toStrictEqual('neat');
});
it('should not overwrite other locales when updating', async () => {
const slug = 'updated';
const esSlug = 'spanish';
const createResult = await fetch(`${url}/api/default-values`, {
body: JSON.stringify({
text: 'unique',
slug: 'unique',
}),
headers,
method: 'post',
});
const createData = await createResult.json();
const { id } = createData.doc;
const enResult = await fetch(`${url}/api/default-values/${id}?locale=en`, {
body: JSON.stringify({
slug,
}),
headers,
method: 'put',
});
const enData = await enResult.json();
const esResult = await fetch(`${url}/api/default-values/${id}?locale=es`, {
body: JSON.stringify({
slug: esSlug,
}),
headers,
method: 'put',
});
const esData = await esResult.json();
const allResult = await fetch(`${url}/api/default-values/${id}?locale=all`, {
headers,
method: 'get',
});
const allData = await allResult.json();
expect(createData.doc.slug).toStrictEqual('unique');
expect(enData.doc.slug).toStrictEqual(slug);
expect(esData.doc.slug).toStrictEqual(esSlug);
expect(allData.slug.en).toStrictEqual(slug);
expect(allData.slug.es).toStrictEqual(esSlug);
});
});
});

View File

@@ -99,6 +99,12 @@ describe('Collections - REST', () => {
const getResponseData = await getResponse.json();
expect(getResponse.status).toBe(200);
expect(getResponseData.afterReadHook).toStrictEqual(true);
expect(getResponseData.findMany).toBeUndefined();
const getManyResponse = await fetch(`${url}/api/hooks`);
const getManyResponseData = await getManyResponse.json();
expect(getManyResponseData.docs[0].findMany).toStrictEqual(true);
});
it('afterChange', async () => {

View File

@@ -5,7 +5,7 @@ import { Options } from 'express-fileupload';
import { Configuration } from 'webpack';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import GraphQL from 'graphql';
import { ConnectionOptions } from 'mongoose';
import { ConnectOptions } from 'mongoose';
import React from 'react';
import { LoggerOptions } from 'pino';
import { Payload } from '..';
@@ -63,7 +63,7 @@ export function hasTransportOptions(emailConfig: EmailOptions): emailConfig is E
export type InitOptions = {
express?: Express;
mongoURL: string;
mongoOptions?: ConnectionOptions;
mongoOptions?: ConnectOptions;
secret: string;
license?: string;
email?: EmailOptions;
@@ -78,7 +78,7 @@ export type AccessResult = boolean | Where;
/**
* Access function
*/
export type Access = (args?: any) => AccessResult;
export type Access = (args?: any) => AccessResult | Promise<AccessResult>;
export type AdminView = React.ComponentType<{ user: User, canAccessAdmin: boolean }>
@@ -187,3 +187,5 @@ export type SanitizedConfig = Omit<DeepRequired<Config>, 'collections' | 'global
globals: SanitizedGlobalConfig[]
paths: { [key: string]: string };
}
export type EntityDescription = string | (() => string) | React.ComponentType

View File

@@ -1,73 +0,0 @@
import { Payload } from '..';
import { HookName, FieldAffectingData } from './config/types';
import relationshipPopulationPromise from './relationshipPopulationPromise';
import { Operation } from '../types';
import { PayloadRequest } from '../express/types';
type Arguments = {
data: Record<string, unknown>
fullData: Record<string, unknown>
originalDoc: Record<string, unknown>
field: FieldAffectingData
operation: Operation
overrideAccess: boolean
req: PayloadRequest
id: string | number
relationshipPopulations: (() => Promise<void>)[]
depth: number
currentDepth: number
hook: HookName
payload: Payload
showHiddenFields: boolean
}
const accessPromise = async ({
data,
fullData,
field,
operation,
overrideAccess,
req,
id,
relationshipPopulations,
depth,
currentDepth,
hook,
payload,
showHiddenFields,
originalDoc,
}: Arguments): Promise<void> => {
const resultingData = data;
let accessOperation;
if (hook === 'afterRead') {
accessOperation = 'read';
} else if (hook === 'beforeValidate') {
if (operation === 'update') accessOperation = 'update';
if (operation === 'create') accessOperation = 'create';
}
if (field.access && field.access[accessOperation]) {
const result = overrideAccess ? true : await field.access[accessOperation]({ req, id, siblingData: data, data: fullData, doc: originalDoc });
if (!result) {
delete resultingData[field.name];
}
}
if ((field.type === 'relationship' || field.type === 'upload') && hook === 'afterRead') {
relationshipPopulations.push(relationshipPopulationPromise({
showHiddenFields,
data,
field,
depth,
currentDepth,
req,
overrideAccess,
payload,
}));
}
};
export default accessPromise;

View File

@@ -1,36 +1,40 @@
import sanitizeFields from './sanitize';
import { MissingFieldType, InvalidFieldRelationship } from '../../errors';
import { Block } from './types';
import { ArrayField, Block, BlockField, CheckboxField, Field, TextField } from './types';
describe('sanitizeFields', () => {
it('should throw on missing type field', () => {
const fields = [{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const fields: Field[] = [{
label: 'some-collection',
name: 'Some Collection',
}];
expect(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
sanitizeFields(fields, []);
}).toThrow(MissingFieldType);
});
describe('auto-labeling', () => {
it('should populate label if missing', () => {
const fields = [{
const fields: Field[] = [{
name: 'someField',
type: 'text',
}];
const sanitizedField = sanitizeFields(fields, [])[0];
const sanitizedField = sanitizeFields(fields, [])[0] as TextField;
expect(sanitizedField.name).toStrictEqual('someField');
expect(sanitizedField.label).toStrictEqual('Some Field');
expect(sanitizedField.type).toStrictEqual('text');
});
it('should allow auto-label override', () => {
const fields = [{
const fields: Field[] = [{
name: 'someField',
type: 'text',
label: 'Do not label',
}];
const sanitizedField = sanitizeFields(fields, [])[0];
const sanitizedField = sanitizeFields(fields, [])[0] as TextField;
expect(sanitizedField.name).toStrictEqual('someField');
expect(sanitizedField.label).toStrictEqual('Do not label');
expect(sanitizedField.type).toStrictEqual('text');
@@ -38,19 +42,19 @@ describe('sanitizeFields', () => {
describe('opt-out', () => {
it('should allow label opt-out', () => {
const fields = [{
const fields: Field[] = [{
name: 'someField',
type: 'text',
label: false,
}];
const sanitizedField = sanitizeFields(fields, [])[0];
const sanitizedField = sanitizeFields(fields, [])[0] as TextField;
expect(sanitizedField.name).toStrictEqual('someField');
expect(sanitizedField.label).toStrictEqual(false);
expect(sanitizedField.type).toStrictEqual('text');
});
it('should allow label opt-out for arrays', () => {
const fields = [{
const arrayField: ArrayField = {
name: 'items',
type: 'array',
label: false,
@@ -60,15 +64,15 @@ describe('sanitizeFields', () => {
type: 'text',
},
],
}];
const sanitizedField = sanitizeFields(fields, [])[0];
};
const sanitizedField = sanitizeFields([arrayField], [])[0] as ArrayField;
expect(sanitizedField.name).toStrictEqual('items');
expect(sanitizedField.label).toStrictEqual(false);
expect(sanitizedField.type).toStrictEqual('array');
expect(sanitizedField.labels).toBeUndefined();
});
it('should allow label opt-out for blocks', () => {
const fields = [{
const fields: Field[] = [{
name: 'noLabelBlock',
type: 'blocks',
label: false,
@@ -84,7 +88,7 @@ describe('sanitizeFields', () => {
},
],
}];
const sanitizedField = sanitizeFields(fields, [])[0];
const sanitizedField = sanitizeFields(fields, [])[0] as BlockField;
expect(sanitizedField.name).toStrictEqual('noLabelBlock');
expect(sanitizedField.label).toStrictEqual(false);
expect(sanitizedField.type).toStrictEqual('blocks');
@@ -94,7 +98,7 @@ describe('sanitizeFields', () => {
it('should label arrays with plural and singular', () => {
const fields = [{
const fields: Field[] = [{
name: 'items',
type: 'array',
fields: [
@@ -104,7 +108,7 @@ describe('sanitizeFields', () => {
},
],
}];
const sanitizedField = sanitizeFields(fields, [])[0];
const sanitizedField = sanitizeFields(fields, [])[0] as ArrayField;
expect(sanitizedField.name).toStrictEqual('items');
expect(sanitizedField.label).toStrictEqual('Items');
expect(sanitizedField.type).toStrictEqual('array');
@@ -112,7 +116,7 @@ describe('sanitizeFields', () => {
});
it('should label blocks with plural and singular', () => {
const fields = [{
const fields: Field[] = [{
name: 'specialBlock',
type: 'blocks',
blocks: [
@@ -122,7 +126,7 @@ describe('sanitizeFields', () => {
},
],
}];
const sanitizedField = sanitizeFields(fields, [])[0];
const sanitizedField = sanitizeFields(fields, [])[0] as BlockField;
expect(sanitizedField.name).toStrictEqual('specialBlock');
expect(sanitizedField.label).toStrictEqual('Special Block');
expect(sanitizedField.type).toStrictEqual('blocks');
@@ -134,7 +138,7 @@ describe('sanitizeFields', () => {
describe('relationships', () => {
it('should not throw on valid relationship', () => {
const validRelationships = ['some-collection'];
const fields = [{
const fields: Field[] = [{
type: 'relationship',
label: 'my-relationship',
name: 'My Relationship',
@@ -147,7 +151,7 @@ describe('sanitizeFields', () => {
it('should not throw on valid relationship - multiple', () => {
const validRelationships = ['some-collection', 'another-collection'];
const fields = [{
const fields: Field[] = [{
type: 'relationship',
label: 'my-relationship',
name: 'My Relationship',
@@ -169,12 +173,9 @@ describe('sanitizeFields', () => {
relationTo: 'some-collection',
}],
};
const fields = [{
const fields: Field[] = [{
name: 'layout',
label: 'Layout Blocks',
labels: {
singular: 'Block',
},
type: 'blocks',
blocks: [relationshipBlock],
}];
@@ -185,7 +186,7 @@ describe('sanitizeFields', () => {
it('should throw on invalid relationship', () => {
const validRelationships = ['some-collection'];
const fields = [{
const fields: Field[] = [{
type: 'relationship',
label: 'my-relationship',
name: 'My Relationship',
@@ -198,7 +199,7 @@ describe('sanitizeFields', () => {
it('should throw on invalid relationship - multiple', () => {
const validRelationships = ['some-collection', 'another-collection'];
const fields = [{
const fields: Field[] = [{
type: 'relationship',
label: 'my-relationship',
name: 'My Relationship',
@@ -220,12 +221,9 @@ describe('sanitizeFields', () => {
relationTo: 'not-valid',
}],
};
const fields = [{
const fields: Field[] = [{
name: 'layout',
label: 'Layout Blocks',
labels: {
singular: 'Block',
},
type: 'blocks',
blocks: [relationshipBlock],
}];
@@ -233,5 +231,21 @@ describe('sanitizeFields', () => {
sanitizeFields(fields, validRelationships);
}).toThrow(InvalidFieldRelationship);
});
it('should defaultValue of checkbox to false if required and undefined', () => {
const fields: Field[] = [{
type: 'checkbox',
name: 'My Checkbox',
required: true,
}];
const sanitizedField = sanitizeFields(fields, [])[0] as CheckboxField;
expect(sanitizedField.defaultValue).toStrictEqual(false);
});
it('should return empty field array if no fields', () => {
const sanitizedFields = sanitizeFields([], []);
expect(sanitizedFields).toStrictEqual([]);
});
});
});

View File

@@ -3,26 +3,26 @@ import { MissingFieldType, InvalidFieldRelationship } from '../../errors';
import { baseBlockFields } from '../baseFields/baseBlockFields';
import validations from '../validations';
import { baseIDField } from '../baseFields/baseIDField';
import { fieldAffectsData } from './types';
import { Field, fieldAffectsData } from './types';
const sanitizeFields = (fields, validRelationships: string[]) => {
const sanitizeFields = (fields: Field[], validRelationships: string[]): Field[] => {
if (!fields) return [];
return fields.map((unsanitizedField) => {
const field = { ...unsanitizedField };
const field: Field = { ...unsanitizedField };
if (!field.type) throw new MissingFieldType(field);
// Auto-label
if (field.name && typeof field.label !== 'string' && field.label !== false) {
if ('name' in field && field.name && typeof field.label !== 'string' && field.label !== false) {
field.label = toWords(field.name);
}
if (field.type === 'checkbox' && typeof field.defaulValue === 'undefined' && field.required === true) {
if (field.type === 'checkbox' && typeof field.defaultValue === 'undefined' && field.required === true) {
field.defaultValue = false;
}
if (field.type === 'relationship') {
if (field.type === 'relationship' || field.type === 'upload') {
const relationships = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo];
relationships.forEach((relationship: string) => {
if (!validRelationships.includes(relationship)) {
@@ -59,9 +59,9 @@ const sanitizeFields = (fields, validRelationships: string[]) => {
if (!field.admin) field.admin = {};
if (field.fields) field.fields = sanitizeFields(field.fields, validRelationships);
if ('fields' in field && field.fields) field.fields = sanitizeFields(field.fields, validRelationships);
if (field.blocks) {
if ('blocks' in field && field.blocks) {
field.blocks = field.blocks.map((block) => {
const unsanitizedBlock = { ...block };
unsanitizedBlock.labels = !unsanitizedBlock.labels ? formatLabels(unsanitizedBlock.slug) : unsanitizedBlock.labels;

View File

@@ -58,7 +58,10 @@ export const idField = baseField.keys({
export const text = baseField.keys({
type: joi.string().valid('text').required(),
name: joi.string().required(),
defaultValue: joi.string(),
defaultValue: joi.alternatives().try(
joi.string(),
joi.func(),
),
minLength: joi.number(),
maxLength: joi.number(),
admin: baseAdminFields.keys({
@@ -70,7 +73,10 @@ export const text = baseField.keys({
export const number = baseField.keys({
type: joi.string().valid('number').required(),
name: joi.string().required(),
defaultValue: joi.number(),
defaultValue: joi.alternatives().try(
joi.number(),
joi.func(),
),
min: joi.number(),
max: joi.number(),
admin: baseAdminFields.keys({
@@ -83,7 +89,10 @@ export const number = baseField.keys({
export const textarea = baseField.keys({
type: joi.string().valid('textarea').required(),
name: joi.string().required(),
defaultValue: joi.string(),
defaultValue: joi.alternatives().try(
joi.string(),
joi.func(),
),
minLength: joi.number(),
maxLength: joi.number(),
admin: baseAdminFields.keys({
@@ -95,7 +104,10 @@ export const textarea = baseField.keys({
export const email = baseField.keys({
type: joi.string().valid('email').required(),
name: joi.string().required(),
defaultValue: joi.string(),
defaultValue: joi.alternatives().try(
joi.string(),
joi.func(),
),
minLength: joi.number(),
maxLength: joi.number(),
admin: baseAdminFields.keys({
@@ -107,7 +119,10 @@ export const email = baseField.keys({
export const code = baseField.keys({
type: joi.string().valid('code').required(),
name: joi.string().required(),
defaultValue: joi.string(),
defaultValue: joi.alternatives().try(
joi.string(),
joi.func(),
),
admin: baseAdminFields.keys({
language: joi.string(),
}),
@@ -127,6 +142,7 @@ export const select = baseField.keys({
defaultValue: joi.alternatives().try(
joi.string().allow(''),
joi.array().items(joi.string().allow('')),
joi.func(),
),
});
@@ -140,7 +156,10 @@ export const radio = baseField.keys({
label: joi.string().required(),
}),
)).required(),
defaultValue: joi.string().allow(''),
defaultValue: joi.alternatives().try(
joi.string().allow(''),
joi.func(),
),
admin: baseAdminFields.keys({
layout: joi.string().valid('vertical', 'horizontal'),
}),
@@ -160,7 +179,10 @@ export const group = baseField.keys({
type: joi.string().valid('group').required(),
name: joi.string().required(),
fields: joi.array().items(joi.link('#field')),
defaultValue: joi.object(),
defaultValue: joi.alternatives().try(
joi.object(),
joi.func(),
),
admin: baseAdminFields.keys({
hideGutter: joi.boolean().default(false),
description: joi.string(),
@@ -177,7 +199,10 @@ export const array = baseField.keys({
singular: joi.string(),
plural: joi.string(),
}),
defaultValue: joi.array().items(joi.object()),
defaultValue: joi.alternatives().try(
joi.array().items(joi.object()),
joi.func(),
),
});
export const upload = baseField.keys({
@@ -194,13 +219,19 @@ export const upload = baseField.keys({
export const checkbox = baseField.keys({
type: joi.string().valid('checkbox').required(),
name: joi.string().required(),
defaultValue: joi.boolean(),
defaultValue: joi.alternatives().try(
joi.boolean(),
joi.func(),
),
});
export const point = baseField.keys({
type: joi.string().valid('point').required(),
name: joi.string().required(),
defaultValue: joi.array().items(joi.number()).max(2).min(2),
defaultValue: joi.alternatives().try(
joi.array().items(joi.number()).max(2).min(2),
joi.func(),
),
});
export const relationship = baseField.keys({
@@ -216,6 +247,9 @@ export const relationship = baseField.keys({
joi.object(),
joi.func(),
),
defaultValue: joi.alternatives().try(
joi.func(),
),
});
export const blocks = baseField.keys({
@@ -239,13 +273,19 @@ export const blocks = baseField.keys({
fields: joi.array().items(joi.link('#field')),
}),
),
defaultValue: joi.array().items(joi.object()),
defaultValue: joi.alternatives().try(
joi.array().items(joi.object()),
joi.func(),
),
});
export const richText = baseField.keys({
type: joi.string().valid('richText').required(),
name: joi.string().required(),
defaultValue: joi.array().items(joi.object()),
defaultValue: joi.alternatives().try(
joi.array().items(joi.object()),
joi.func(),
),
admin: baseAdminFields.keys({
placeholder: joi.string(),
elements: joi.array().items(
@@ -282,7 +322,10 @@ export const richText = baseField.keys({
export const date = baseField.keys({
type: joi.string().valid('date').required(),
name: joi.string().required(),
defaultValue: joi.string(),
defaultValue: joi.alternatives().try(
joi.string(),
joi.func(),
),
admin: baseAdminFields.keys({
placeholder: joi.string(),
date: joi.object({

View File

@@ -7,21 +7,23 @@ import { PayloadRequest } from '../../express/types';
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';
import { Description } from '../../admin/components/forms/FieldDescription/types';
import { User } from '../../auth';
import { Payload } from '../..';
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
value?: P,
originalDoc?: T,
data?: Partial<T>,
siblingData: Partial<S>
findMany?: boolean
originalDoc?: T,
operation?: 'create' | 'read' | 'update' | 'delete',
req: PayloadRequest
siblingData: Partial<S>
value?: P,
}
export type FieldHook<T extends TypeWithID = any, P = any, S = any> = (args: FieldHookArgs<T, P, S>) => Promise<P> | P;
export type FieldAccess<T extends TypeWithID = any, P = any> = (args: {
req: PayloadRequest
id?: string
id?: string | number
data?: Partial<T>
siblingData?: Partial<P>
doc?: T
@@ -67,6 +69,7 @@ export type ValidateOptions<T, S, F> = {
id?: string | number
user?: Partial<User>
operation?: Operation
payload?: Payload
} & F;
export type Validate<T = any, S = any, F = any> = (value?: T, options?: ValidateOptions<F, S, Partial<F>>) => string | true | Promise<string | true>;
@@ -181,9 +184,9 @@ export type UIField = {
width?: string
condition?: Condition
components?: {
Filter?: React.ComponentType;
Cell?: React.ComponentType;
Field: React.ComponentType;
Filter?: React.ComponentType<any>;
Cell?: React.ComponentType<any>;
Field: React.ComponentType<any>;
}
}
type: 'ui';
@@ -226,6 +229,10 @@ export type ValueWithRelation = {
value: string | number
}
export function valueIsValueWithRelation(value: unknown): value is ValueWithRelation {
return typeof value === 'object' && 'relationTo' in value && 'value' in value;
}
export type RelationshipValue = (string | number)
| (string | number)[]
| ValueWithRelation

View File

@@ -0,0 +1,23 @@
import { User } from '../auth';
type Args = {
value?: unknown,
defaultValue: unknown,
user: User,
locale: string | undefined,
};
const getValueWithDefault = async ({ value, defaultValue, locale, user }: Args): Promise<unknown> => {
if (typeof value !== 'undefined') {
return value;
}
if (defaultValue && typeof defaultValue === 'function') {
return defaultValue({ locale, user });
}
return defaultValue;
return undefined;
};
export default getValueWithDefault;

View File

@@ -1,94 +0,0 @@
import { PayloadRequest } from '../express/types';
import { Operation } from '../types';
import { HookName, FieldAffectingData, FieldHook } from './config/types';
type Arguments = {
data: Record<string, unknown>
field: FieldAffectingData
hook: HookName
req: PayloadRequest
operation: Operation
fullOriginalDoc: Record<string, unknown>
fullData: Record<string, unknown>
flattenLocales: boolean
isVersion: boolean
}
type ExecuteHookArguments = {
currentHook: FieldHook
value: unknown
} & Arguments;
const executeHook = async ({
currentHook,
fullOriginalDoc,
fullData,
data,
operation,
req,
value,
}: ExecuteHookArguments) => {
let hookedValue = await currentHook({
value,
originalDoc: fullOriginalDoc,
data: fullData,
siblingData: data,
operation,
req,
});
if (typeof hookedValue === 'undefined') {
hookedValue = value;
}
return hookedValue;
};
const hookPromise = async (args: Arguments): Promise<void> => {
const {
field,
hook,
req,
flattenLocales,
data,
} = args;
if (field.hooks && field.hooks[hook]) {
await field.hooks[hook].reduce(async (priorHook, currentHook) => {
await priorHook;
const shouldRunHookOnAllLocales = hook === 'afterRead'
&& field.localized
&& (req.locale === 'all' || !flattenLocales)
&& typeof data[field.name] === 'object';
if (shouldRunHookOnAllLocales) {
const hookPromises = Object.entries(data[field.name]).map(([locale, value]) => (async () => {
const hookedValue = await executeHook({
...args,
currentHook,
value,
});
if (hookedValue !== undefined) {
data[field.name][locale] = hookedValue;
}
})());
await Promise.all(hookPromises);
} else {
const hookedValue = await executeHook({
...args,
value: data[field.name],
currentHook,
});
if (hookedValue !== undefined) {
data[field.name] = hookedValue;
}
}
}, Promise.resolve());
}
};
export default hookPromise;

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