Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f361a44cca | ||
|
|
47c37e0153 | ||
|
|
5df3b35189 | ||
|
|
1e4a68f76e | ||
|
|
b3832e21c9 | ||
|
|
18489faceb | ||
|
|
69d328d15e | ||
|
|
738e8ab9b6 | ||
|
|
e7349fea9a | ||
|
|
51a6790f26 | ||
|
|
515f20372e | ||
|
|
12fbe8368f | ||
|
|
55b4dfb309 | ||
|
|
fb7bb76674 | ||
|
|
e46b942259 | ||
|
|
e4affd4bf9 | ||
|
|
16398d3438 | ||
|
|
e8503232ba | ||
|
|
bf48fdf189 | ||
|
|
834f4c2700 | ||
|
|
e297eb9090 | ||
|
|
1f394bef72 | ||
|
|
1cdd5b96b3 | ||
|
|
2d14ab1217 | ||
|
|
8bdbd0dd41 | ||
|
|
800be4c9a0 | ||
|
|
b99ec060ca | ||
|
|
d5f4c030b4 | ||
|
|
4de92e3924 | ||
|
|
3b70560e25 | ||
|
|
d88c89fb05 | ||
|
|
24d6d8e5f9 | ||
|
|
9a9b28113a | ||
|
|
ec84ffbee2 | ||
|
|
3c1dfb88df | ||
|
|
4a6b79b231 | ||
|
|
9e2ed56ef0 | ||
|
|
9e324be057 | ||
|
|
b7f47c9bb1 | ||
|
|
8a997c82be | ||
|
|
3dcd8a24cb | ||
|
|
203ce2c2f9 |
10
.eslintrc.js
10
.eslintrc.js
@@ -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,
|
||||
|
||||
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
86
CHANGELOG.md
86
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -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
8
cypress.json
Normal 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
16
cypress/cypress.d.ts
vendored
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal 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"
|
||||
}
|
||||
43
cypress/integration/collections.e2e.ts
Normal file
43
cypress/integration/collections.e2e.ts
Normal 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`);
|
||||
});
|
||||
});
|
||||
1
cypress/integration/common/constants.ts
Normal file
1
cypress/integration/common/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const adminURL = 'http://localhost:3000/admin';
|
||||
4
cypress/integration/common/credentials.ts
Normal file
4
cypress/integration/common/credentials.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const credentials = {
|
||||
email: 'test@test.com',
|
||||
password: 'test123',
|
||||
};
|
||||
89
cypress/integration/login.e2e.ts
Normal file
89
cypress/integration/login.e2e.ts
Normal 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
22
cypress/plugins/index.js
Normal 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
|
||||
}
|
||||
54
cypress/support/commands.ts
Normal file
54
cypress/support/commands.ts
Normal 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
25
cypress/support/index.ts
Normal 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
15
cypress/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom"
|
||||
],
|
||||
"types": [
|
||||
"cypress"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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?:
|
||||
| {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}) => {...}
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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 |
|
||||
| -------- | --------------------------- | ----------------------- |
|
||||
|
||||
26
package.json
26
package.json
@@ -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": [
|
||||
|
||||
@@ -16,4 +16,5 @@ export type RenderedTypeProps = {
|
||||
className?: string
|
||||
onClick?: onClick
|
||||
to: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type Props = {
|
||||
}
|
||||
|
||||
export type RenderedTypeProps = {
|
||||
children: React.ReactNode
|
||||
className?: string,
|
||||
to: string,
|
||||
onClick?: () => void,
|
||||
|
||||
@@ -155,7 +155,6 @@ const Popup: React.FC<Props> = (props) => {
|
||||
className={`${baseClass}__wrap`}
|
||||
// TODO: color ::after with bg color
|
||||
>
|
||||
|
||||
<div
|
||||
className={`${baseClass}__scroll`}
|
||||
style={{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -4,7 +4,7 @@ export type Options = OptionsType<Value> | GroupedOptionsType<Value>;
|
||||
|
||||
export type Value = {
|
||||
label: string
|
||||
value: string
|
||||
value: string | null
|
||||
options?: Options
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,3 +33,5 @@ export type ValueWithRelation = {
|
||||
relationTo: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type GetResults = (args: { lastFullyLoadedRelation?: number, lastLoadedPage?: number, search?: string }) => Promise<void>
|
||||
|
||||
@@ -80,7 +80,6 @@ const DraggableSection: React.FC<Props> = (props) => {
|
||||
<HiddenInput
|
||||
name={`${parentPath}.${rowIndex}.id`}
|
||||
value={id}
|
||||
modifyForm={false}
|
||||
/>
|
||||
<SectionTitle
|
||||
label={label}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,6 @@ export type Field = {
|
||||
valid: boolean
|
||||
validate?: Validate
|
||||
disableFormData?: boolean
|
||||
ignoreWhileFlattening?: boolean
|
||||
condition?: Condition
|
||||
passesCondition?: boolean
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,5 +2,5 @@ export type Props = {
|
||||
name: string
|
||||
path?: string
|
||||
value: unknown
|
||||
modifyForm?: boolean
|
||||
disableModifyingForm?: false
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -271,6 +271,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
ref={editorRef}
|
||||
>
|
||||
<Editable
|
||||
className={`${baseClass}__input`}
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const CompareVersion: React.FC<Props> = (props) => {
|
||||
|
||||
const getResults = useCallback(async ({
|
||||
lastLoadedPage: lastLoadedPageArg,
|
||||
} = {}) => {
|
||||
}) => {
|
||||
const query: {
|
||||
[key: string]: unknown
|
||||
where: Where
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
109
src/collections/tests/defaultValue.spec.ts
Normal file
109
src/collections/tests/defaultValue.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
23
src/fields/getDefaultValue.ts
Normal file
23
src/fields/getDefaultValue.ts
Normal 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;
|
||||
@@ -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
Reference in New Issue
Block a user