Compare commits

...

125 Commits

Author SHA1 Message Date
Elliot DeNolf
94de5c6c24 chore(release): richtext-slate/1.0.7 [skip ci] 2023-10-23 13:21:00 -04:00
Elliot DeNolf
5d4ef620b1 chore(release): richtext-lexical/0.1.15 [skip ci] 2023-10-23 13:20:48 -04:00
Elliot DeNolf
cac33ac275 chore(release): payload/2.0.12 [skip ci] 2023-10-23 13:18:50 -04:00
Elliot DeNolf
219aa3b2f3 fix: remove duplicate removal of temp upload file (#3818) 2023-10-23 13:01:18 -04:00
Elliot DeNolf
dd0ff50621 chore: sync pnpm-lock.yaml 2023-10-23 00:27:51 -04:00
Elliot DeNolf
591c0a0786 Merge pull request #3677 from payloadcms/chore/plugin-stripe
chore: imports stripe plugin
2023-10-23 00:25:50 -04:00
Elliot DeNolf
a197161390 chore: sync pnpm-lock.yaml 2023-10-23 00:24:15 -04:00
Elliot DeNolf
ae5b397bc8 test(plugin-stripe): stub tests 2023-10-23 00:11:19 -04:00
Elliot DeNolf
e6b2d3d1fc chore(plugin-stripe): format 2023-10-22 17:40:01 -04:00
Elliot DeNolf
6b19525a65 chore(plugin-stripe): eslint fix 2023-10-22 17:39:30 -04:00
Elliot DeNolf
c86ae0a9d2 chore(plugin-stripe): cleanup after import 2023-10-22 17:38:56 -04:00
Alessio Gravili
9277b306de fix(richtext-lexical): infinite re-rendering after draft save (#3709) 2023-10-22 01:41:58 +02:00
Alessio Gravili
156eae2551 fix(richtext-lexical): do not update Block node if form data is the same 2023-10-22 00:21:09 +02:00
Alessio Gravili
e197e0316f fix(richtext-*): hasMany relationships not populated correctly 2023-10-21 23:58:47 +02:00
Alessio Gravili
863b79348b feat(richtext-lexical): linebreak html converter 2023-10-21 23:30:47 +02:00
Alessio Gravili
30ff65d0b4 Merge pull request #3805 from payloadcms/fix/lexical-use-client
fix(richtext-lexical): missing use client markers required for next.js compatibility
2023-10-21 23:25:09 +02:00
Alessio Gravili
3185771551 fix(richtext-lexical): missing use client markers required for next.js compatibility 2023-10-21 23:21:12 +02:00
Jarrod Flesch
ea83c3f3a2 fix: simplify how the search input and query params are connected (#3797) 2023-10-21 10:04:54 -04:00
Alessio Gravili
072f7febd2 chore(richtext-lexical): do not capitalize H2 and H3 headings 2023-10-21 14:56:40 +02:00
Alessio Gravili
b5c7bbed93 fix(richtext-lexical): defaultValue property didn't fit into field schema 2023-10-21 14:52:26 +02:00
Alessio Gravili
931f6ff519 fix(richtext-lexical): Field Description shows up twice (#3793) 2023-10-21 14:51:11 +02:00
Alessio Gravili
0af36af16c feat(richtext-lexical): HTML Serializer (#3685)
* chore(richtext-lexical): add jsdocs for afterReadPromise in GraphQL

* feat(richtext-lexical): HTML Serializer

* chore(richtext-lexical): adjust comment

* chore(richtext-lexical): change the way the html serializer works

* chore: working html converter field, improve various exports

* feat: link and heading html serializers

* fix: populationPromises not being added properly

* feat: allow html serializers to be async

* feat: upload html serializer

* feat: text format => html

* feat: lists => html

* feat: Quote => html

* chore: improve Checklist => html conversion, by passing in the full parent to converters
2023-10-21 14:37:59 +02:00
Alessio Gravili
f6adbae0c7 feat: collection, global and field props for hooks, fix request context initialization, add context to global hooks (#3780)
* feat: pass collection, global and field props to collection, global and field hooks - where applicable

* fix: initial request context not set for all operations

* chore: add tests which check the collection prop for collection hooks

* feat: add context to props of global hooks

* chore: add global tests for global and field props

* chore: int tests: use JSON instead of object hashes
2023-10-21 11:40:57 +02:00
Jacob Fletcher
67d61df563 fix: standardizes layout of document fields (#3798) 2023-10-20 22:22:41 -04:00
Jacob Fletcher
01380bebe5 fix(templates/website): missing env vars (#3799) 2023-10-20 18:09:43 -04:00
Elliot DeNolf
5719b1b39a test: remove declare module from tests 2023-10-20 16:46:14 -04:00
Jarrod Flesch
f259645488 fix: issue where dragging unsortable item would crash the page (#3789) 2023-10-20 16:14:08 -04:00
Elliot DeNolf
eec60d5883 chore: rename .eslintrc.cjs -> .js 2023-10-20 15:50:25 -04:00
Elliot DeNolf
d4548d73d5 ci: checkout code for filters to work with non-PRs 2023-10-20 15:14:20 -04:00
Elliot DeNolf
ccc7c51c90 ci: detect code changes by dir, add builds for templates (#3724)
* ci: add simple website template build

* ci: copy .env.example

* ci: add mongo

* ci: add other templates via matrix

* ci: move templates to separate workflow

* ci: implement paths-filter

* chore: trigger template filter

* chore: refine filters

* chore: adjust needs_build

* chore: undo trigger
2023-10-20 14:57:47 -04:00
Kalon Robson
ec5e35ff71 chore(templates): fix e-commerce template jsx component type error (#3717) 2023-10-20 13:37:21 -04:00
Kalon Robson
55b9bf40df chore(templates): fix website template unused ts-expect-error (#3652) 2023-10-20 13:37:02 -04:00
Elliot DeNolf
5282673746 chore(templates): revert unintentional change 2023-10-20 11:58:35 -04:00
Elliot DeNolf
298ca0b7ae chore(templates): copy yarn.lock in Dockerfile (#3787) 2023-10-20 10:52:00 -04:00
Elliot DeNolf
71407e19e2 chore(deps): bump sharp for CVE-2023-4863 (#3786) 2023-10-20 10:40:59 -04:00
Jarrod Flesch
09078bdb40 fix(examples): ensure next middleware is built, removes unnecessary alias (#3771) 2023-10-20 07:47:07 -04:00
Elliot DeNolf
e84f5ded28 chore: update changelog 2023-10-19 17:34:33 -04:00
Elliot DeNolf
a475b9b28b chore(release): live-preview-react/0.1.5 [skip ci] 2023-10-19 16:42:58 -04:00
Elliot DeNolf
baad7d3360 chore(release): live-preview/0.1.5 [skip ci] 2023-10-19 16:42:33 -04:00
Jacob Fletcher
7fcb972dfa fix(live-preview): blocks field (#3753) 2023-10-19 16:40:16 -04:00
Elliot DeNolf
01245b07f8 chore(release): richtext-lexical/0.1.14 [skip ci] 2023-10-19 16:14:06 -04:00
Elliot DeNolf
d2f45343da chore(release): db-postgres/0.1.10 [skip ci] 2023-10-19 16:13:55 -04:00
Elliot DeNolf
5ba95df674 chore(release): db-mongodb/1.0.4 [skip ci] 2023-10-19 16:13:46 -04:00
Jacob Fletcher
b0a62442e5 Merge remote-tracking branch 'plugin-stripe/main' into chore/plugin-stripe 2023-10-15 01:42:42 -04:00
Elliot DeNolf
927a1ab049 chore(release): plugin-nested-stripe/0.0.15 2023-10-13 10:57:48 -04:00
Elliot DeNolf
f23ae28d45 chore(deps): add payload 2.0 to peer deps 2023-10-13 10:56:46 -04:00
Elliot DeNolf
36740b70d4 0.0.15-beta.0 2023-10-06 11:49:53 -04:00
PatrikKozak
5d1677a84e Merge pull request #29 from payloadcms/chore/alias-pattern
chore: improves alias pattern
2023-10-06 10:55:29 -04:00
Patrik Kozak
4fab26db9d chore: updates local dev aliases 2023-10-05 22:29:51 -04:00
Patrik Kozak
56cf767e18 chore: improves alias pattern 2023-10-05 15:37:01 -04:00
Jacob Fletcher
0a45389a25 0.0.14 2023-08-04 13:28:23 -04:00
Jacob Fletcher
2abdce31f8 feat: allows turning off rest proxy (#22) 2023-08-04 13:27:48 -04:00
Jacob Fletcher
0221394c06 Merge pull request #19 from payloadcms/chore/eslint
chore: eslint and prettier
2023-05-18 14:29:18 -04:00
Jacob Fletcher
741ab0487d bumps demo to payload v1.8.2 2023-05-18 14:28:46 -04:00
Jacob Fletcher
8a513ba7af chore: eslint and prettier 2023-05-18 14:28:41 -04:00
Jacob Fletcher
dfb9a93547 0.0.13 2023-05-01 17:40:57 -04:00
Jacob Fletcher
a2e336470a Merge pull request #18 from payloadcms/fix/deletion-hook
fix: safely retrieves stripe resource before deletion #17
2023-05-01 17:40:20 -04:00
Jacob Fletcher
f6994e57dd fix: safely retrieves stripe resource before deletion #17 2023-05-01 17:36:40 -04:00
Jacob Fletcher
bfe8de3fd6 0.0.12 2023-03-29 13:27:09 -04:00
Jacob Fletcher
bd16e9fb53 chore: conditionally logs webhook events #15 (#16) 2023-03-29 13:25:27 -04:00
Jacob Fletcher
8ddbb67f07 0.0.11 2023-02-02 14:28:23 -05:00
Jacob Fletcher
60c14557ff Merge pull request #14 from payloadcms/chore/13
chore: handles stripe test keys
2023-01-04 16:53:56 -05:00
Jacob Fletcher
a38b43dc4f chore: handles stripe test keys 2023-01-04 16:53:18 -05:00
Jacob Fletcher
12e85f654e Merge pull request #12 from payloadcms/fix/7
fix: auto-generates password for webhook-created users
2023-01-04 10:18:26 -05:00
Jacob Fletcher
c02463be69 fix: auto-generates password for webhook-created users 2023-01-04 10:16:41 -05:00
Jacob Fletcher
1b6d0cf4da Merge pull request #11 from payloadcms/feat/type-exports
fix: properly exports types
2023-01-04 09:58:03 -05:00
Jacob Fletcher
e59e6ed65e fix: properly exports types 2023-01-04 09:44:15 -05:00
Jacob Fletcher
d6a11921e0 Merge pull request #10 from Velua/patch-2
feat: accepts generic in StripeWebhookHandler
2023-01-04 08:57:25 -05:00
John Williamson
573c8de380 accept generic 2022-12-29 17:11:33 +10:00
Jacob Fletcher
e7ac1819ce 0.0.10 2022-10-19 14:51:29 -04:00
Jacob Fletcher
288ff2b094 feat: inforces stripeArgs array 2022-10-19 14:51:11 -04:00
Jacob Fletcher
aca534ec59 0.0.9 2022-10-19 14:27:39 -04:00
Jacob Fletcher
a8951cb741 fix: safely passes args through stripe proxy method handler 2022-10-19 14:26:39 -04:00
Jarrod Flesch
7f9dd2b4e1 fix: readme example ContentType to Content-Type 2022-10-18 17:02:26 -04:00
Jacob Fletcher
07b970027d fix: demo subscription sync 2022-10-12 16:14:41 -04:00
Jacob Fletcher
71f6542341 chore: adds ui link to demo subscriptions 2022-10-11 11:13:46 -04:00
Jacob Fletcher
c90830f961 chore: syncs subscription status 2022-10-11 11:10:34 -04:00
Jacob Fletcher
d46d2c0595 0.0.8 2022-10-11 09:58:12 -04:00
Jacob Fletcher
16d6c26387 feat: adds priceJSON sync to demo 2022-10-11 09:55:15 -04:00
Jacob Fletcher
32df3067e1 fix: migrates from afterChange to beforeChange hook 2022-10-10 17:27:49 -04:00
Jacob Fletcher
3a7440dcb9 fix: uses proper key-value pairs in to-stripe hooks and renames fieldName to fieldPath 2022-10-10 17:11:10 -04:00
Jacob Fletcher
417f4b7aa9 fix: allows dot notation in sync config 2022-10-10 16:24:36 -04:00
Jacob Fletcher
822aec0a5c chore: renames fieldName and stripeProperty 2022-10-10 15:24:05 -04:00
Jacob Fletcher
455622fa57 0.0.7 2022-10-07 14:20:00 -04:00
Jacob Fletcher
f93316e588 chore: renames resource to stripeResourceType 2022-10-07 14:05:29 -04:00
Jacob Fletcher
b7e65d1024 feat: adds direct link to stripe resources #4 2022-10-07 13:38:40 -04:00
Jacob Fletcher
b5728104dd 0.0.6 2022-09-30 13:09:52 -04:00
Jacob Fletcher
604197bb98 chore: mocks server modules in demo 2022-09-30 13:05:43 -04:00
Jacob Fletcher
6b30a9702b 0.0.5 2022-09-30 11:21:54 -04:00
Jacob Fletcher
ab974ee587 fix: build errors 2022-09-30 11:20:47 -04:00
Jacob Fletcher
3a9efb21e0 chore: custom webhooks 2022-09-30 11:12:45 -04:00
Jacob Fletcher
2dd395f718 chore: improves logs 2022-09-29 15:27:03 -04:00
Jacob Fletcher
2df28355cf chore: syncs demo products 2022-09-29 12:33:24 -04:00
Jacob Fletcher
7607c17041 chore: custom subscriptionCreatedOrUpdated webhook 2022-09-29 12:33:07 -04:00
Jacob Fletcher
f81b4d3a1b feat: detects nested webhooks events 2022-09-28 12:24:53 -04:00
Jacob Fletcher
8305b65b98 chore: renames object to resource 2022-09-27 17:00:47 -04:00
Jacob Fletcher
275d15cfdc chore: renames isSyncedToStripe to skipSync 2022-09-27 16:28:26 -04:00
Jacob Fletcher
c09667edfc chore: general housekeeping 2022-09-27 16:26:49 -04:00
Jacob Fletcher
2cbb14f8dd feat: abstracts webhooks 2022-09-27 14:43:21 -04:00
Jacob Fletcher
936c125a42 fix: auto-sync hooks 2022-09-27 10:55:30 -04:00
Jacob Fletcher
5a8cdef103 wip: auto-sync 2022-09-26 18:18:58 -04:00
Jacob Fletcher
26bc1b46c1 chore: bumps to payload v1.1.4 2022-09-26 10:32:45 -04:00
Jacob Fletcher
639a832600 feat: supports collection config 2022-09-23 16:22:07 -04:00
Jacob Fletcher
ba4d751831 feat: configures working sync 2022-09-23 14:49:48 -04:00
Jacob Fletcher
32a0972855 feat: webhooks catch-all 2022-09-21 11:09:05 -04:00
Jacob Fletcher
d354610978 0.0.4 2022-09-20 13:20:04 -04:00
Jacob Fletcher
97bd414d3d chore: updates README.md 2022-09-20 13:19:16 -04:00
Jacob Fletcher
9f396598a0 chore: pluralizes stripeWebhooksEndpointSecret 2022-09-20 13:06:54 -04:00
Jacob Fletcher
c2e20277ec chore: aliases server modules 2022-09-20 13:04:09 -04:00
Jacob Fletcher
7e6f35f380 chore: removes proxy from demo hooks 2022-09-19 17:28:51 -04:00
Jacob Fletcher
750646b3b8 chore: updates README 2022-09-19 12:40:40 -04:00
Jacob Fletcher
eef80a8239 0.0.3 2022-08-24 16:24:38 -04:00
Jacob Fletcher
339fb96b7d fix: type error in demo 2022-08-24 16:24:04 -04:00
Jacob Fletcher
fe8254c73d fix: exports stripeProxy 2022-08-24 16:23:47 -04:00
Jacob Fletcher
aef868f471 0.0.2 2022-08-24 15:44:40 -04:00
Jacob Fletcher
8e02db10ae chore: updates README.md 2022-08-24 15:44:24 -04:00
Jacob Fletcher
44dd66cb72 feat: builds customers sync demo 2022-08-24 15:44:13 -04:00
Jacob Fletcher
713c6738aa feat: abstracts stripe proxy from route 2022-08-24 15:43:26 -04:00
Jacob Fletcher
f70a7b80fc chore: stripe rest api error handling 2022-08-24 11:34:22 -04:00
Jacob Fletcher
32665d11c5 0.0.1 2022-08-24 08:18:52 -04:00
Jacob Fletcher
1ed4c096a3 fix: type errors 2022-08-24 08:18:43 -04:00
Jacob Fletcher
339ab3a838 fix: express type errors 2022-08-18 15:36:23 -04:00
Jacob Fletcher
cc9f9dd704 feat: opens stripe rest 2022-08-18 14:17:31 -04:00
Jacob Fletcher
c13acfe47a feat: initial working draft 2022-08-17 18:11:59 -04:00
Jacob Fletcher
715e13b78e chore: scaffolds plugin 2022-08-17 13:58:41 -04:00
250 changed files with 11572 additions and 1207 deletions

View File

@@ -7,7 +7,36 @@ on:
branches: ['main']
jobs:
changes:
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
needs_build: ${{ steps.filter.outputs.needs_build }}
templates: ${{ steps.filter.outputs.templates }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
needs_build:
- 'packages/**'
- 'test/**'
- 'pnpm-lock.yaml'
- 'package.json'
templates:
- 'templates/**'
- name: Log all filter results
run: |
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
echo "templates: ${{ steps.filter.outputs.templates }}"
core-build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
steps:
@@ -246,3 +275,34 @@ jobs:
- name: Test ${{ matrix.pkg }}
run: pnpm --filter ${{ matrix.pkg }} run test
if: matrix.pkg != 'create-payload-app' # degit doesn't work within GitHub Actions
templates:
needs: changes
if: ${{ needs.changes.outputs.templates == 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
template: [blank, website, ecommerce]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: 6.0
- name: Build Website
run: |
cd templates/${{ matrix.template }}
cp .env.example .env
yarn install
yarn build

View File

@@ -1,3 +1,25 @@
## [2.0.11](https://github.com/payloadcms/payload/compare/v2.0.10...v2.0.11) (2023-10-19)
### Features
* add ability to opt out of type gen declare statement ([#3765](https://github.com/payloadcms/payload/pull/3765))
### Bug Fixes
* corrects versions collection casing ([#3739](https://github.com/payloadcms/payload/pull/3739))
* updates req after file resize ([#3754](https://github.com/payloadcms/payload/pull/3754))
* correctly renders focal point when crop is set to false ([#3759](https://github.com/payloadcms/payload/pull/3759))
* account for many slug types in generate types ([#3698](https://github.com/payloadcms/payload/pull/3698))
* handle graphQL: false on globals when building policy type ([#3729](https://github.com/payloadcms/payload/pull/3729))
* renders id as fallback title in DeleteDocument ([#3745](https://github.com/payloadcms/payload/pull/3745))
* properly handles hideAPIURL ([#3721](https://github.com/payloadcms/payload/pull/3721))
* filesRequiredOnCreate typing, tests, linting ([#3737](https://github.com/payloadcms/payload/pull/3737))
* **webpack-bundler:** corrects payload alias ([#3769](https://github.com/payloadcms/payload/pull/3769))
* **bundler-webpack:** better node_modules resolution ([#3744](https://github.com/payloadcms/payload/pull/3744))
* **db-postgres:** block and array inserts error ([#3714](https://github.com/payloadcms/payload/pull/3714))
* **live-preview:** properly handles uploads and hasOne monomorphic relationships ([#3719](https://github.com/payloadcms/payload/pull/3719))
## [2.0.10](https://github.com/payloadcms/payload/compare/v2.0.9...v2.0.10) (2023-10-17)
### Features

View File

@@ -62,7 +62,7 @@ All field-level hooks are formatted to accept the same arguments, although some
Field Hooks receive one `args` argument that contains the following properties:
| Option | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`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. |
| **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many within the `afterRead` hook. |
@@ -73,6 +73,10 @@ Field Hooks receive one `args` argument that contains the following properties:
| **`req`** | The Express `request` object. It is mocked for Local API operations. |
| **`value`** | The value of the field. |
| **`previousValue`** | The previous value of the field, before changes were applied, only in `afterChange` hooks. |
| **`context`** | Context passed to this hook. More info can be found under [Context](/docs/hooks/context) |
| **`field`** | The field which the hook is running against. |
| **`collection`** | The collection which the field belongs to. If the field belongs to a global, this will be null. |
| **`global`** | The global which the field belongs to. If the field belongs to a collection, this will be null. |
#### Return value

View File

@@ -21,16 +21,6 @@ export default buildConfig({
components: {
beforeLogin: [BeforeLogin],
},
webpack: config => ({
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
dotenv: path.resolve(__dirname, './dotenv.js'),
},
},
}),
},
editor: slateEditor({}),
db: mongooseAdapter({

View File

@@ -29,7 +29,7 @@ const start = async (): Promise<void> => {
app.listen(PORT, async () => {
payload.logger.info(`Next.js is now building...`)
// @ts-expect-error
await nextBuild(path.join(__dirname, '../'))
await nextBuild(path.join(__dirname, '..'))
process.exit()
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.0.3",
"version": "1.0.4",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.9",
"version": "0.1.10",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "0.1.4",
"version": "0.1.5",
"description": "The official live preview React SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "0.1.4",
"version": "0.1.5",
"description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -6,7 +6,7 @@ export type MergeLiveDataArgs<T> = {
apiRoute?: string
depth: number
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: T
incomingData: Partial<T>
initialData: T
serverURL: string
}

View File

@@ -53,34 +53,36 @@ export const traverseFields = <T>({
case 'blocks':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((row, i) => {
const matchedBlock = fieldJSON.blocks[row.blockType]
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType]
const hasExistingRow =
// Compare the index and id to determine if this block already exists in the result
// If so, we want to use the existing block as the base, otherwise take the incoming block
// Either way, we will traverse the fields of the block to populate relationships
const isExistingBlock =
Array.isArray(result[fieldName]) &&
typeof result[fieldName][i] === 'object' &&
result[fieldName][i] !== null &&
result[fieldName][i].blockType === row.blockType
result[fieldName][i].id === incomingBlock.id
const newRow = hasExistingRow
? { ...result[fieldName][i] }
: {
blockType: matchedBlock.slug,
}
const block = isExistingBlock ? result[fieldName][i] : incomingBlock
traverseFields({
apiRoute,
depth,
fieldSchema: matchedBlock.fields,
incomingData: row,
fieldSchema: incomingBlockJSON.fields,
incomingData: incomingBlock,
populationPromises,
result: newRow,
result: block,
serverURL,
})
return newRow
return block
})
} else {
result[fieldName] = []
}
break
case 'tabs':

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.0.11",
"version": "2.0.12",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -130,7 +130,7 @@
"sass": "1.69.4",
"scheduler": "0.23.0",
"scmp": "2.1.0",
"sharp": "0.31.3",
"sharp": "0.32.6",
"swc-loader": "0.2.3",
"terser-webpack-plugin": "5.3.9",
"ts-essentials": "7.0.3",

View File

@@ -1,12 +1,12 @@
@import '../../../../scss/styles.scss';
@import '../../../scss/styles.scss';
.global-default-edit {
.document-fields {
width: 100%;
display: flex;
--doc-sidebar-width: 325px;
&--has-sidebar {
.global-default-edit {
.document-fields {
&__edit {
[dir='ltr'] & {
top: 0;
@@ -46,10 +46,6 @@
flex-grow: 1;
}
&__auth {
margin-bottom: var(--base);
}
&__sidebar-wrap {
position: sticky;
top: var(--doc-controls-height);
@@ -62,9 +58,6 @@
width: 100%;
height: 100%;
overflow-y: auto;
}
&__sidebar-sticky-wrap {
display: flex;
flex-direction: column;
min-height: 100%;
@@ -88,7 +81,7 @@
display: block;
&--has-sidebar {
.global-default-edit {
.document-fields {
&__main {
width: 100%;
}
@@ -124,8 +117,6 @@
width: 100%;
height: initial;
border-left: 0;
margin-top: calc(var(--base) / 2);
width: var(--doc-sidebar-width);
}
&__form {
@@ -136,6 +127,7 @@
padding-top: 0;
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
padding-bottom: 0;
gap: base(0.5);
[dir='ltr'] & {

View File

@@ -0,0 +1,87 @@
import React from 'react'
import type { CollectionPermission, GlobalPermission } from '../../../../auth'
import type { FieldWithPath } from '../../../../fields/config/types'
import type { Description } from '../../forms/FieldDescription/types'
import RenderFields from '../../forms/RenderFields'
import { filterFields } from '../../forms/RenderFields/filterFields'
import { fieldTypes } from '../../forms/field-types'
import { Gutter } from '../Gutter'
import ViewDescription from '../ViewDescription'
import './index.scss'
const baseClass = 'document-fields'
export const DocumentFields: React.FC<{
AfterFields?: React.FC
BeforeFields?: React.FC
description?: Description
fields: FieldWithPath[]
hasSavePermission: boolean
permissions: CollectionPermission | GlobalPermission
}> = (props) => {
const { AfterFields, BeforeFields, description, fields, hasSavePermission, permissions } = props
const sidebarFields = filterFields({
fieldSchema: fields,
fieldTypes,
filter: (field) => field?.admin?.position === 'sidebar',
permissions: permissions.fields,
readOnly: !hasSavePermission,
})
const hasSidebar = sidebarFields && sidebarFields.length > 0
return (
<React.Fragment>
<div
className={[
baseClass,
hasSidebar ? `${baseClass}--has-sidebar` : `${baseClass}--no-sidebar`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__main`}>
<Gutter className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
{description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
{BeforeFields && <BeforeFields />}
<RenderFields
className={`${baseClass}__fields`}
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) =>
!field.admin.position ||
(field.admin.position && field.admin.position !== 'sidebar')
}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
{AfterFields && <AfterFields />}
</Gutter>
</div>
{hasSidebar && (
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldTypes={fieldTypes}
fields={sidebarFields}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
</div>
</div>
)}
</div>
</React.Fragment>
)
}

View File

@@ -62,6 +62,7 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
data = await collection.admin.hooks.beforeDuplicate({
collection,
data,
locale,
})
@@ -108,6 +109,7 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
localizedDoc = await collection.admin.hooks.beforeDuplicate({
collection,
data: localizedDoc,
locale,
})

View File

@@ -1,13 +1,11 @@
import { useWindowInfo } from '@faceless-ui/window-info'
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import AnimateHeight from 'react-animate-height'
import { useTranslation } from 'react-i18next'
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { Props } from './types'
import { fieldAffectsData } from '../../../../fields/config/types'
import flattenFields from '../../../../utilities/flattenTopLevelFields'
import { getTranslation } from '../../../../utilities/getTranslation'
import Chevron from '../../icons/Chevron'
import { useSearchParams } from '../../utilities/SearchParams'
@@ -27,22 +25,12 @@ import './index.scss'
const baseClass = 'list-controls'
const getUseAsTitle = (collection: SanitizedCollectionConfig) => {
const {
admin: { useAsTitle },
fields,
} = collection
const topLevelFields = flattenFields(fields)
return topLevelFields.find((field) => fieldAffectsData(field) && field.name === useAsTitle)
}
/**
* The ListControls component is used to render the controls (search, filter, where)
* for a collection's list view. You can find those directly above the table which lists
* the collection's documents.
*/
const ListControls: React.FC<Props> = (props) => {
export const ListControls: React.FC<Props> = (props) => {
const {
collection: {
admin: { listSearchableFields },
@@ -51,21 +39,20 @@ const ListControls: React.FC<Props> = (props) => {
collection,
enableColumns = true,
enableSort = false,
handleSearchChange,
handleSortChange,
handleWhereChange,
modifySearchQuery = true,
resetParams,
titleField,
} = props
const params = useSearchParams()
const shouldInitializeWhereOpened = validateWhereQuery(params?.where)
const [titleField, setTitleField] = useState(getUseAsTitle(collection))
useEffect(() => {
setTitleField(getUseAsTitle(collection))
}, [collection])
const [textFieldsToBeSearched] = useState(getTextFieldsToBeSearched(listSearchableFields, fields))
const [textFieldsToBeSearched, setFieldsToBeSearched] = useState(
getTextFieldsToBeSearched(listSearchableFields, fields),
)
const [visibleDrawer, setVisibleDrawer] = useState<'columns' | 'sort' | 'where'>(
shouldInitializeWhereOpened ? 'where' : undefined,
)
@@ -74,18 +61,19 @@ const ListControls: React.FC<Props> = (props) => {
breakpoints: { s: smallBreak },
} = useWindowInfo()
React.useEffect(() => {
setFieldsToBeSearched(getTextFieldsToBeSearched(listSearchableFields, fields))
}, [listSearchableFields, fields])
return (
<div className={baseClass}>
<div className={`${baseClass}__wrap`}>
<SearchFilter
fieldLabel={
(titleField &&
fieldAffectsData(titleField) &&
getTranslation(titleField.label || titleField.name, i18n)) ??
undefined
(titleField && getTranslation(titleField.label || titleField.name, i18n)) ?? undefined
}
fieldName={titleField && fieldAffectsData(titleField) ? titleField.name : undefined}
handleChange={handleWhereChange}
handleChange={handleSearchChange}
listSearchableFields={textFieldsToBeSearched}
modifySearchQuery={modifySearchQuery}
/>
@@ -179,5 +167,3 @@ const ListControls: React.FC<Props> = (props) => {
</div>
)
}
export default ListControls

View File

@@ -1,4 +1,5 @@
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { FieldAffectingData } from '../../../../exports/types'
import type { Where } from '../../../../types'
import type { Props as ListProps } from '../../views/collections/List/types'
import type { Column } from '../Table/types'
@@ -7,10 +8,12 @@ export type Props = {
collection: SanitizedCollectionConfig
enableColumns?: boolean
enableSort?: boolean
handleSearchChange?: (search: string) => void
handleSortChange?: (sort: string) => void
handleWhereChange?: (where: Where) => void
modifySearchQuery?: boolean
resetParams?: ListProps['resetParams']
titleField: FieldAffectingData
}
export type ListControls = {

View File

@@ -3,12 +3,14 @@ import React, { useCallback, useEffect, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { Where } from '../../../../exports/types'
import type { Field } from '../../../../fields/config/types'
import type { ListDrawerProps } from './types'
import { baseClass } from '.'
import { getTranslation } from '../../../../utilities/getTranslation'
import usePayloadAPI from '../../../hooks/usePayloadAPI'
import { useUseTitleField } from '../../../hooks/useUseAsTitle'
import Label from '../../forms/Label'
import X from '../../icons/X'
import { useAuth } from '../../utilities/Auth'
@@ -24,6 +26,22 @@ import ReactSelect from '../ReactSelect'
import { TableColumnsProvider } from '../TableColumns'
import ViewDescription from '../ViewDescription'
const hoistQueryParamsToAnd = (where: Where, queryParams: Where) => {
if ('and' in where) {
where.and.push(queryParams)
} else if ('or' in where) {
where = {
and: [where, queryParams],
}
} else {
where = {
and: [where, queryParams],
}
}
return where
}
export const ListDrawerContent: React.FC<ListDrawerProps> = ({
collectionSlugs,
customHeader,
@@ -40,6 +58,8 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const [sort, setSort] = useState(null)
const [page, setPage] = useState(1)
const [where, setWhere] = useState(null)
const [search, setSearch] = useState('')
const {
collections,
routes: { api },
@@ -69,6 +89,8 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const [fields, setFields] = useState<Field[]>(() => formatFields(selectedCollectionConfig))
const titleField = useUseTitleField(selectedCollectionConfig)
useEffect(() => {
setFields(formatFields(selectedCollectionConfig))
}, [selectedCollectionConfig])
@@ -111,31 +133,58 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
useEffect(() => {
const { admin: { listSearchableFields } = {}, slug } = selectedCollectionConfig
const params: {
cacheBust?: number
limit?: number
page?: number
search?: string
sort?: string
where?: unknown
} = {}
if (page) params.page = page
let copyOfWhere = { ...(where || {}) }
params.where = {
...(where ? { ...where } : {}),
...(filterOptions?.[selectedCollectionConfig.slug]
? {
...filterOptions[selectedCollectionConfig.slug],
}
: {}),
if (filterOptions) {
copyOfWhere = hoistQueryParamsToAnd(copyOfWhere, filterOptions[slug])
}
if (search) {
const searchAsConditions = (listSearchableFields || [titleField?.name]).map((fieldName) => {
return {
[fieldName]: {
like: search,
},
}
}, [])
if (searchAsConditions.length > 0) {
const searchFilter: Where = {
or: [...searchAsConditions],
}
copyOfWhere = hoistQueryParamsToAnd(copyOfWhere, searchFilter)
}
}
if (page) params.page = page
if (sort) params.sort = sort
if (limit) params.limit = limit
if (cacheBust) params.cacheBust = cacheBust
if (copyOfWhere) params.where = copyOfWhere
setParams(params)
}, [setParams, page, sort, where, limit, cacheBust, filterOptions, selectedCollectionConfig])
}, [
page,
sort,
where,
search,
cacheBust,
filterOptions,
selectedCollectionConfig,
t,
setParams,
titleField?.name,
])
useEffect(() => {
const newPreferences = {
@@ -241,6 +290,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
data,
handlePageChange: setPage,
handlePerPageChange: setLimit,
handleSearchChange: setSearch,
handleSortChange: setSort,
handleWhereChange: setWhere,
hasCreatePermission,
@@ -249,6 +299,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
newDocumentURL: null,
setLimit,
setSort,
titleField,
}}
/>
</DocumentInfoProvider>

View File

@@ -16,11 +16,12 @@ export const MultiValue: React.FC<MultiValueProps<Option>> = (props) => {
innerProps,
isDisabled,
// @ts-expect-error // TODO Fix this - moduleResolution 16 breaks our declare module
selectProps: { customProps: { disableMouseDown } = {} } = {},
selectProps: { customProps: { disableMouseDown } = {}, isSortable } = {},
} = props
const { attributes, isDragging, listeners, setNodeRef, transform } = useDraggableSortable({
id: value.toString(),
disabled: !isSortable,
})
const classes = [

View File

@@ -33,6 +33,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
const {
className,
components,
customProps,
disabled = false,
filterOption = undefined,
isClearable = true,
@@ -45,7 +46,6 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
onMenuOpen,
options,
placeholder = t('general:selectValue'),
selectProps,
showError,
value,
} = props
@@ -58,7 +58,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
return (
<Select
captureMenuScroll
customProps={selectProps}
customProps={customProps}
isLoading={isLoading}
placeholder={getTranslation(placeholder, i18n)}
{...props}

View File

@@ -78,10 +78,6 @@ export type Props = {
onMenuScrollToBottom?: () => void
options: Option[] | OptionGroup[]
placeholder?: string
/**
* @deprecated Since version 1.0. Will be deleted in version 2.0. Use customProps instead.
*/
selectProps?: CustomSelectProps
showError?: boolean
value?: Option | Option[]
}

View File

@@ -3,7 +3,6 @@ import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom'
import type { Where, WhereField } from '../../../../types'
import type { Props } from './types'
import { getTranslation } from '../../../../utilities/getTranslation'
@@ -27,7 +26,7 @@ const SearchFilter: React.FC<Props> = (props) => {
const history = useHistory()
const { i18n, t } = useTranslation('general')
const [search, setSearch] = useState('')
const [search, setSearch] = useState(typeof params?.search === 'string' ? params?.search : '')
const [previousSearch, setPreviousSearch] = useState('')
const placeholder = useRef(t('searchBy', { label: getTranslation(fieldLabel, i18n) }))
@@ -35,48 +34,15 @@ const SearchFilter: React.FC<Props> = (props) => {
const debouncedSearch = useDebounce(search, 300)
useEffect(() => {
const newWhere: Where = {
...(typeof params?.where === 'object' ? (params.where as Where) : {}),
}
const fieldNamesToSearch =
listSearchableFields?.length > 0
? [...listSearchableFields.map(({ name }) => name)]
: [fieldName]
fieldNamesToSearch.forEach((fieldNameToSearch) => {
const hasOrQuery = Array.isArray(newWhere.or)
const existingFieldSearchIndex = hasOrQuery
? newWhere.or.findIndex((condition) => {
return (condition?.[fieldNameToSearch] as WhereField)?.like
})
: -1
if (debouncedSearch) {
if (!hasOrQuery) newWhere.or = []
if (existingFieldSearchIndex > -1) {
;(newWhere.or[existingFieldSearchIndex][fieldNameToSearch] as WhereField).like =
debouncedSearch
} else {
newWhere.or.push({
[fieldNameToSearch]: {
like: debouncedSearch,
},
})
}
} else if (existingFieldSearchIndex > -1) {
newWhere.or.splice(existingFieldSearchIndex, 1)
}
})
if (debouncedSearch !== previousSearch) {
if (handleChange) handleChange(newWhere)
if (handleChange) handleChange(debouncedSearch)
if (modifySearchQuery) {
history.replace({
search: queryString.stringify({
...params,
page: 1,
where: newWhere,
search: debouncedSearch || undefined,
}),
})
}

View File

@@ -1,10 +1,9 @@
import type { FieldAffectingData } from '../../../../fields/config/types'
import type { Where } from '../../../../types'
export type Props = {
fieldLabel?: string
fieldName?: string
handleChange?: (where: Where) => void
handleChange?: (search: string) => void
listSearchableFields?: FieldAffectingData[]
modifySearchQuery?: boolean
}

View File

@@ -76,7 +76,7 @@ const Condition: React.FC<Props> = (props) => {
onChange={(field) =>
dispatch({
andIndex,
field: field.value,
field: field?.value || undefined,
orIndex,
type: 'update',
})

View File

@@ -12,7 +12,17 @@ export type RichTextFieldProps<Value extends object, AdapterProps> = Omit<
export type RichTextAdapter<Value extends object = object, AdapterProps = any> = {
CellComponent: React.FC<CellComponentProps<RichTextField<Value, AdapterProps>>>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps>>
afterReadPromise?: (data: {
afterReadPromise?: ({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<Value, AdapterProps>
incomingEditorState: Value
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
populationPromise?: (data: {
currentDepth?: number
depth: number
field: RichTextField<Value, AdapterProps>

View File

@@ -1,24 +1,19 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { Translation } from '../../../../translations/type'
import type { CollectionEditViewProps } from '../types'
import { DocumentControls } from '../../elements/DocumentControls'
import { DocumentFields } from '../../elements/DocumentFields'
import { DocumentHeader } from '../../elements/DocumentHeader'
import { Gutter } from '../../elements/Gutter'
import { LoadingOverlayToggle } from '../../elements/Loading'
import ReactSelect from '../../elements/ReactSelect'
import Form from '../../forms/Form'
import Label from '../../forms/Label'
import RenderFields from '../../forms/RenderFields'
import { fieldTypes } from '../../forms/field-types'
import { LeaveWithoutSaving } from '../../modals/LeaveWithoutSaving'
import { useAuth } from '../../utilities/Auth'
import Meta from '../../utilities/Meta'
import { OperationContext } from '../../utilities/OperationProvider'
import Auth from '../collections/Edit/Auth'
import { ToggleTheme } from './ToggleTheme'
import { Settings } from './Settings'
import './index.scss'
const baseClass = 'account'
@@ -39,14 +34,7 @@ const DefaultAccount: React.FC<CollectionEditViewProps> = (props) => {
const { auth, fields } = collection
const { refreshCookieAsync } = useAuth()
const { i18n, t } = useTranslation('authentication')
const languageOptions = Object.entries(i18n.options.resources || {}).map(
([language, resource]) => ({
label: (resource as Translation).general.thisLanguage,
value: language,
}),
)
const { t } = useTranslation('authentication')
const onSave = useCallback(async () => {
await refreshCookieAsync()
@@ -55,91 +43,49 @@ const DefaultAccount: React.FC<CollectionEditViewProps> = (props) => {
}
}, [onSaveFromProps, refreshCookieAsync])
const classes = [baseClass].filter(Boolean).join(' ')
return (
<React.Fragment>
<Meta description={t('accountOfCurrentUser')} keywords={t('account')} title={t('account')} />
<LoadingOverlayToggle name="account" show={isLoading} type="withoutNav" />
{!isLoading && (
<div className={classes}>
<OperationContext.Provider value="update">
<Form
action={action}
className={`${baseClass}__form`}
disabled={!hasSavePermission}
initialState={initialState}
method="patch"
onSuccess={onSave}
>
<DocumentHeader apiURL={apiURL} collection={collection} data={data} />
<DocumentControls
apiURL={apiURL}
collection={collection}
data={data}
hasSavePermission={hasSavePermission}
isAccountView
permissions={permissions}
/>
<div className={`${baseClass}__main`}>
<Meta
description={t('accountOfCurrentUser')}
keywords={t('account')}
title={t('account')}
<OperationContext.Provider value="update">
<Form
action={action}
disabled={!hasSavePermission}
initialState={initialState}
method="patch"
onSuccess={onSave}
>
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && (
<LeaveWithoutSaving />
)}
<DocumentHeader apiURL={apiURL} collection={collection} data={data} />
<DocumentControls
apiURL={apiURL}
collection={collection}
data={data}
hasSavePermission={hasSavePermission}
isAccountView
permissions={permissions}
/>
<DocumentFields
AfterFields={() => <Settings className={`${baseClass}__settings`} />}
BeforeFields={() => (
<Auth
className={`${baseClass}__auth`}
collection={collection}
email={data?.email}
operation="update"
readOnly={!hasSavePermission}
useAPIKey={auth.useAPIKey}
/>
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && (
<LeaveWithoutSaving />
)}
<div className={`${baseClass}__edit`}>
<Gutter className={`${baseClass}__header`}>
<Auth
className={`${baseClass}__auth`}
collection={collection}
email={data?.email}
operation="update"
readOnly={!hasSavePermission}
useAPIKey={auth.useAPIKey}
/>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => field?.admin?.position !== 'sidebar'}
permissions={permissions?.fields}
readOnly={!hasSavePermission}
/>
</Gutter>
<Gutter className={`${baseClass}__payload-settings`}>
<h3>{t('general:payloadSettings')}</h3>
<div className={`${baseClass}__language`}>
<Label htmlFor="language-select" label={t('general:language')} />
<ReactSelect
inputId="language-select"
onChange={({ value }) => i18n.changeLanguage(value)}
options={languageOptions}
value={languageOptions.find((language) => language.value === i18n.language)}
/>
</div>
<ToggleTheme />
</Gutter>
</div>
</div>
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => field?.admin?.position === 'sidebar'}
permissions={permissions?.fields}
readOnly={!hasSavePermission}
/>
</div>
</div>
</div>
</div>
</Form>
</OperationContext.Provider>
</div>
)}
fields={fields}
hasSavePermission={hasSavePermission}
permissions={permissions}
/>
</Form>
</OperationContext.Provider>
)}
</React.Fragment>
)

View File

@@ -0,0 +1,45 @@
@import '../../../../scss/styles.scss';
.payload-settings {
position: relative;
h3 {
margin: 0;
}
&::before,
&::after {
content: '';
display: block;
height: 1px;
background: var(--theme-elevation-100);
width: calc(100% + calc(var(--base) * 5));
left: calc(var(--gutter-h) * -1);
top: 0;
position: absolute;
}
&::after {
display: none;
bottom: 0;
top: unset;
}
margin-top: base(3);
padding-top: base(3);
padding-bottom: base(3);
display: flex;
flex-direction: column;
gap: var(--base);
@include mid-break {
margin-bottom: var(--base);
padding-top: base(2);
margin-top: base(2);
padding-bottom: base(2);
&::after {
display: block;
}
}
}

View File

@@ -0,0 +1,42 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { Translation } from '../../../../../translations/type'
import ReactSelect from '../../../elements/ReactSelect'
import Label from '../../../forms/Label'
import { ToggleTheme } from '../ToggleTheme'
import './index.scss'
const baseClass = 'payload-settings'
export const Settings: React.FC<{
className?: string
}> = (props) => {
const { className } = props
const { i18n, t } = useTranslation('authentication')
const languageOptions = Object.entries(i18n.options.resources || {}).map(
([language, resource]) => ({
label: (resource as Translation).general.thisLanguage,
value: language,
}),
)
return (
<div className={[baseClass, className].filter(Boolean).join(' ')}>
<h3>{t('general:payloadSettings')}</h3>
<div className={`${baseClass}__language`}>
<Label htmlFor="language-select" label={t('general:language')} />
<ReactSelect
inputId="language-select"
onChange={({ value }) => i18n.changeLanguage(value)}
options={languageOptions}
value={languageOptions.find((language) => language.value === i18n.language)}
/>
</div>
<ToggleTheme />
</div>
)
}

View File

@@ -1,47 +1,19 @@
@import '../../../scss/styles.scss';
.account {
width: 100%;
padding-bottom: var(--spacing-view-bottom);
&__form {
height: 100%;
}
&__edit {
margin-top: calc(var(--base) * 3);
}
&__auth {
margin-bottom: var(--base);
margin-bottom: calc(var(--base) * 2);
margin-top: calc(var(--base) * 0.5);
}
&__header {
display: flex;
flex-direction: column;
}
&__payload-settings {
margin-top: base(3);
padding-top: base(3);
border-top: 1px solid var(--theme-elevation-100);
}
&__language {
margin-bottom: $baseline;
&___settings {
margin-bottom: calc(var(--base) * 2);
}
@include small-break {
&__edit {
margin: var(--base) 0;
}
&__payload-settings {
margin-top: base(1);
padding-top: base(1);
padding-bottom: base(0.5);
border-top: 1px solid var(--theme-elevation-100);
border-bottom: 1px solid var(--theme-elevation-100);
&__auth {
margin-top: 0;
margin-bottom: var(--base);
}
}
}

View File

@@ -5,39 +5,27 @@ import type { GlobalEditViewProps } from '../../types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import { DocumentControls } from '../../../elements/DocumentControls'
import { Gutter } from '../../../elements/Gutter'
import ViewDescription from '../../../elements/ViewDescription'
import RenderFields from '../../../forms/RenderFields'
import { filterFields } from '../../../forms/RenderFields/filterFields'
import { fieldTypes } from '../../../forms/field-types'
import { DocumentFields } from '../../../elements/DocumentFields'
import { LeaveWithoutSaving } from '../../../modals/LeaveWithoutSaving'
import Meta from '../../../utilities/Meta'
import { SetStepNav } from '../../collections/Edit/SetStepNav'
import './index.scss'
const baseClass = 'global-default-edit'
export const DefaultGlobalEdit: React.FC<GlobalEditViewProps> = (props) => {
const { i18n } = useTranslation('general')
const { apiURL, data, global, permissions } = props
const { i18n } = useTranslation()
const { admin: { description } = {}, fields, label } = global
const hasSavePermission = permissions?.update?.permission
const sidebarFields = filterFields({
fieldSchema: fields,
fieldTypes,
filter: (field) => field?.admin?.position === 'sidebar',
permissions: permissions.fields,
readOnly: !hasSavePermission,
})
const hasSidebar = sidebarFields && sidebarFields.length > 0
return (
<React.Fragment>
<Meta
description={getTranslation(label, i18n)}
keywords={`${getTranslation(label, i18n)}, Payload, CMS`}
title={getTranslation(label, i18n)}
/>
{!(global.versions?.drafts && global.versions?.drafts?.autosave) && <LeaveWithoutSaving />}
<SetStepNav global={global} />
<DocumentControls
apiURL={apiURL}
@@ -47,60 +35,12 @@ export const DefaultGlobalEdit: React.FC<GlobalEditViewProps> = (props) => {
isEditing
permissions={permissions}
/>
<div
className={[
baseClass,
hasSidebar ? `${baseClass}--has-sidebar` : `${baseClass}--no-sidebar`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__main`}>
<Meta
description={getTranslation(label, i18n)}
keywords={`${getTranslation(label, i18n)}, Payload, CMS`}
title={getTranslation(label, i18n)}
/>
{!(global.versions?.drafts && global.versions?.drafts?.autosave) && (
<LeaveWithoutSaving />
)}
<Gutter className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
{description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
</div>
)}
</header>
<RenderFields
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) =>
!field.admin.position ||
(field.admin.position && field.admin.position !== 'sidebar')
}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</Gutter>
</div>
{hasSidebar && (
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldTypes={fieldTypes}
fields={sidebarFields}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
</div>
</div>
</div>
)}
</div>
<DocumentFields
description={description}
fields={fields}
hasSavePermission={hasSavePermission}
permissions={permissions}
/>
</React.Fragment>
)
}

View File

@@ -1,168 +1,15 @@
@import '../../../../../scss/styles.scss';
.collection-default-edit {
width: 100%;
display: flex;
--doc-sidebar-width: 500px;
&--has-sidebar {
.collection-default-edit {
&__edit {
[dir='ltr'] & {
top: 0;
right: 0;
border-right: 1px solid var(--theme-elevation-100);
padding-right: calc(var(--base) * 2);
}
[dir='rtl'] & {
top: 0;
left: 0;
border-left: 1px solid var(--theme-elevation-100);
padding-left: calc(var(--base) * 2);
}
}
&__fields {
& > .tabs-field,
& > .group-field {
margin-right: calc(var(--base) * -2);
}
}
}
}
&__main {
width: 100%;
display: flex;
flex-direction: column;
min-height: 100%;
flex-grow: 1;
}
&__edit {
padding-top: calc(var(--base) * 1.5);
padding-bottom: var(--spacing-view-bottom);
flex-grow: 1;
}
&__auth {
margin-bottom: var(--base);
}
&__sidebar-wrap {
position: sticky;
top: var(--doc-controls-height);
height: calc(100vh - var(--doc-controls-height));
width: var(--doc-sidebar-width);
flex-shrink: 0;
}
&__sidebar {
width: 100%;
height: 100%;
overflow-y: auto;
}
&__sidebar-sticky-wrap {
display: flex;
flex-direction: column;
min-height: 100%;
}
&__sidebar-fields {
display: flex;
flex-direction: column;
gap: var(--base);
padding-top: calc(var(--base) * 1.5);
padding-left: calc(var(--base) * 2);
padding-right: var(--gutter-h);
padding-bottom: var(--spacing-view-bottom);
}
&__label {
color: var(--theme-elevation-400);
}
@include large-break {
--doc-sidebar-width: 350px;
}
@include mid-break {
display: block;
&--has-sidebar {
.collection-default-edit {
&__main {
width: 100%;
}
&__edit {
[dir='ltr'] & {
border-right: 0;
padding-right: var(--gutter-h);
}
[dir='rtl'] & {
border-left: 0;
padding-left: var(--gutter-h);
}
}
&__fields {
& > .tabs-field,
& > .group-field {
margin-right: calc(var(--gutter-h) * -1);
}
}
}
}
&__main {
width: 100%;
min-height: initial;
}
&__sidebar-wrap {
position: static;
width: 100%;
height: initial;
border-left: 0;
margin-top: calc(var(--base) / 2);
}
&__form {
display: block;
}
&__sidebar-fields {
padding-top: 0;
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
gap: base(0.5);
[dir='ltr'] & {
padding-right: var(--gutter-h);
}
[dir='rtl'] & {
padding-left: var(--gutter-h);
}
}
&__sidebar {
padding-bottom: base(3.5);
overflow: visible;
}
margin-bottom: calc(var(--base) * 2);
margin-top: calc(var(--base) * 0.5);
}
@include small-break {
&__sidebar-wrap {
min-width: initial;
}
&__edit {
padding-top: calc(var(--base) / 2);
&__auth {
margin-top: 0;
margin-bottom: var(--base);
}
}
}

View File

@@ -5,10 +5,7 @@ import type { CollectionEditViewProps } from '../../../types'
import { getTranslation } from '../../../../../../utilities/getTranslation'
import { DocumentControls } from '../../../../elements/DocumentControls'
import { Gutter } from '../../../../elements/Gutter'
import RenderFields from '../../../../forms/RenderFields'
import { filterFields } from '../../../../forms/RenderFields/filterFields'
import { fieldTypes } from '../../../../forms/field-types'
import { DocumentFields } from '../../../../elements/DocumentFields'
import { LeaveWithoutSaving } from '../../../../modals/LeaveWithoutSaving'
import Meta from '../../../../utilities/Meta'
import Auth from '../Auth'
@@ -38,18 +35,21 @@ export const DefaultCollectionEdit: React.FC<CollectionEditViewProps> = (props)
const operation = isEditing ? 'update' : 'create'
const sidebarFields = filterFields({
fieldSchema: fields,
fieldTypes,
filter: (field) => field?.admin?.position === 'sidebar',
permissions: permissions.fields,
readOnly: !hasSavePermission,
})
const hasSidebar = sidebarFields && sidebarFields.length > 0
return (
<Fragment>
<Meta
description={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
keywords={`${getTranslation(collection.labels.singular, i18n)}, Payload, CMS`}
title={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
/>
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
<SetStepNav collection={collection} id={id} isEditing={isEditing} />
<DocumentControls
apiURL={apiURL}
@@ -61,29 +61,9 @@ export const DefaultCollectionEdit: React.FC<CollectionEditViewProps> = (props)
isEditing={isEditing}
permissions={permissions}
/>
<div
className={[
baseClass,
hasSidebar ? `${baseClass}--has-sidebar` : `${baseClass}--no-sidebar`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__main`}>
<Meta
description={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
keywords={`${getTranslation(collection.labels.singular, i18n)}, Payload, CMS`}
title={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(
collection.labels.singular,
i18n,
)}`}
/>
{!(collection.versions?.drafts && collection.versions?.drafts?.autosave) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
<Gutter className={`${baseClass}__edit`}>
<DocumentFields
BeforeFields={() => (
<Fragment>
{auth && (
<Auth
className={`${baseClass}__auth`}
@@ -97,33 +77,12 @@ export const DefaultCollectionEdit: React.FC<CollectionEditViewProps> = (props)
/>
)}
{upload && <Upload collection={collection} internalState={internalState} />}
<RenderFields
className={`${baseClass}__fields`}
fieldSchema={fields}
fieldTypes={fieldTypes}
filter={(field) => !field?.admin?.position || field?.admin?.position !== 'sidebar'}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</Gutter>
</div>
{hasSidebar && (
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
fieldTypes={fieldTypes}
fields={sidebarFields}
permissions={permissions.fields}
readOnly={!hasSavePermission}
/>
</div>
</div>
</div>
</div>
</Fragment>
)}
</div>
fields={fields}
hasSavePermission={hasSavePermission}
permissions={permissions}
/>
</Fragment>
)
}

View File

@@ -10,7 +10,7 @@ import Button from '../../../elements/Button'
import DeleteMany from '../../../elements/DeleteMany'
import EditMany from '../../../elements/EditMany'
import { Gutter } from '../../../elements/Gutter'
import ListControls from '../../../elements/ListControls'
import { ListControls } from '../../../elements/ListControls'
import ListSelection from '../../../elements/ListSelection'
import Paginator from '../../../elements/Paginator'
import PerPage from '../../../elements/PerPage'
@@ -41,6 +41,7 @@ const DefaultList: React.FC<Props> = (props) => {
data,
handlePageChange,
handlePerPageChange,
handleSearchChange,
handleSortChange,
handleWhereChange,
hasCreatePermission,
@@ -48,6 +49,7 @@ const DefaultList: React.FC<Props> = (props) => {
modifySearchParams,
newDocumentURL,
resetParams,
titleField,
} = props
const {
@@ -99,10 +101,12 @@ const DefaultList: React.FC<Props> = (props) => {
</header>
<ListControls
collection={collection}
handleSearchChange={handleSearchChange}
handleSortChange={handleSortChange}
handleWhereChange={handleWhereChange}
modifySearchQuery={modifySearchParams}
resetParams={resetParams}
titleField={titleField}
/>
{Array.isArray(BeforeListTable) &&
BeforeListTable.map((Component, i) => <Component key={i} {...props} />)}

View File

@@ -4,10 +4,12 @@ import { useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom'
import { v4 as uuid } from 'uuid'
import type { Field } from '../../../../../fields/config/types'
import type { Where } from '../../../../../exports/types'
import type { ListIndexProps, ListPreferences, Props } from './types'
import { type Field } from '../../../../../fields/config/types'
import usePayloadAPI from '../../../../hooks/usePayloadAPI'
import { useUseTitleField } from '../../../../hooks/useUseAsTitle'
import { useStepNav } from '../../../elements/StepNav'
import { TableColumnsProvider } from '../../../elements/TableColumns'
import { useAuth } from '../../../utilities/Auth'
@@ -18,6 +20,22 @@ import { useSearchParams } from '../../../utilities/SearchParams'
import DefaultList from './Default'
import formatFields from './formatFields'
const hoistQueryParamsToAnd = (where: Where, queryParams: Where) => {
if ('and' in where) {
where.and.push(queryParams)
} else if ('or' in where) {
where = {
and: [where, queryParams],
}
} else {
where = {
and: [where, queryParams],
}
}
return where
}
/**
* The ListView component is table which lists the collection's documents.
* The default list view can be found at the {@link DefaultList} component.
@@ -30,6 +48,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
collection: {
admin: {
components: { views: { List: CustomList } = {} } = {},
listSearchableFields,
pagination: { defaultLimit },
},
labels: { plural },
@@ -45,7 +64,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
const { permissions } = useAuth()
const { setStepNav } = useStepNav()
const { getPreference, setPreference } = usePreferences()
const { limit, page, sort, where } = useSearchParams()
const { limit, page, search, sort, where } = useSearchParams()
const history = useHistory()
const { t } = useTranslation('general')
const [fetchURL, setFetchURL] = useState<string>('')
@@ -54,6 +73,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
const hasCreatePermission = collectionPermissions?.create?.permission
const newDocumentURL = `${admin}/collections/${slug}/create`
const [{ data }, { setParams }] = usePayloadAPI(fetchURL, { initialParams: { page: 1 } })
const titleField = useUseTitleField(collection)
useEffect(() => {
setStepNav([
@@ -69,23 +89,45 @@ const ListView: React.FC<ListIndexProps> = (props) => {
const resetParams = useCallback<Props['resetParams']>(
(overrides = {}) => {
const params: Record<string, unknown> = {
const params: Record<string, unknown> & { where?: Where } = {
depth: 0,
draft: 'true',
limit,
page: overrides?.page,
search: overrides?.search,
sort: overrides?.sort,
where: overrides?.where,
where: overrides?.where || {},
}
if (page) params.page = page
if (sort) params.sort = sort
if (where) params.where = where
if (where) params.where = where as Where
params.invoke = uuid()
if (search) {
let copyOfWhere = { ...((where as Where) || {}) }
const searchAsConditions = (listSearchableFields || [titleField?.name]).map((fieldName) => {
return {
[fieldName]: {
like: search,
},
}
}, [])
if (searchAsConditions.length > 0) {
const conditionalSearchFields = {
or: [...searchAsConditions],
}
copyOfWhere = hoistQueryParamsToAnd(copyOfWhere, conditionalSearchFields)
}
params.where = copyOfWhere
}
setParams(params)
},
[limit, page, setParams, sort, where],
[limit, page, setParams, sort, where, search, listSearchableFields, titleField?.name],
)
useEffect(() => {
@@ -178,6 +220,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
limit: limit || defaultLimit,
newDocumentURL,
resetParams,
titleField,
}}
/>
</TableColumnsProvider>

View File

@@ -1,5 +1,6 @@
import type { SanitizedCollectionConfig } from '../../../../../collections/config/types'
import type { PaginatedDocs } from '../../../../../database/types'
import type { FieldAffectingData } from '../../../../../exports/types'
import type { Where } from '../../../../../types'
import type { Props as ListControlsProps } from '../../../elements/ListControls/types'
import type { Props as PaginatorProps } from '../../../elements/Paginator/types'
@@ -12,6 +13,7 @@ export type Props = {
handleDelete?: () => void
handlePageChange?: PaginatorProps['onChange']
handlePerPageChange?: PerPageProps['handleChange']
handleSearchChange?: ListControlsProps['handleSearchChange']
handleSortChange?: ListControlsProps['handleSortChange']
handleWhereChange?: ListControlsProps['handleWhereChange']
hasCreatePermission: boolean
@@ -19,10 +21,16 @@ export type Props = {
modifySearchParams?: boolean
newDocumentURL: string
onCreateNewClick?: () => void
resetParams: (overrides?: { page?: number; sort?: string; where?: Where }) => void
resetParams: (overrides?: {
page?: number
search?: string
sort?: string
where?: Where
}) => void
setLimit: (limit: number) => void
setListControls: (controls: unknown) => void
setSort: (sort: string) => void
titleField?: FieldAffectingData
toggleColumn: (column: string) => void
}

View File

@@ -0,0 +1,16 @@
import type { FieldAffectingData, SanitizedCollectionConfig } from '../../exports/types'
import { fieldAffectsData } from '../../exports/types'
import flattenFields from '../../utilities/flattenTopLevelFields'
export const useUseTitleField = (collection: SanitizedCollectionConfig): FieldAffectingData => {
const {
admin: { useAsTitle },
fields,
} = collection
const topLevelFields = flattenFields(fields)
return topLevelFields.find(
(field) => fieldAffectsData(field) && field.name === useAsTitle,
) as FieldAffectingData
}

View File

@@ -38,6 +38,7 @@ async function forgotPassword(incomingArgs: Arguments): Promise<null | string> {
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'forgotPassword',
})) || args
@@ -139,7 +140,7 @@ async function forgotPassword(incomingArgs: Arguments): Promise<null | string> {
await collectionConfig.hooks.afterForgotPassword.reduce(async (priorHook, hook) => {
await priorHook
await hook({ args, context: req.context })
await hook({ args, collection: args.collection?.config, context: req.context })
}, Promise.resolve())
// /////////////////////////////////////
@@ -148,6 +149,7 @@ async function forgotPassword(incomingArgs: Arguments): Promise<null | string> {
token = await buildAfterOperation({
args,
collection: args.collection?.config,
operation: 'forgotPassword',
result: token,
})

View File

@@ -30,7 +30,7 @@ async function localForgotPassword<T extends keyof GeneratedTypes['collections']
expiration,
req = {} as PayloadRequest,
} = options
setRequestContext(options.req)
setRequestContext(req)
const collection = payload.collections[collectionSlug]

View File

@@ -41,7 +41,7 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
res,
showHiddenFields,
} = options
setRequestContext(options.req)
setRequestContext(req)
const collection = payload.collections[collectionSlug]

View File

@@ -24,7 +24,8 @@ async function localResetPassword<T extends keyof GeneratedTypes['collections']>
options: Options<T>,
): Promise<Result> {
const { collection: collectionSlug, data, overrideAccess, req = {} as PayloadRequest } = options
setRequestContext(options.req)
setRequestContext(req)
const collection = payload.collections[collectionSlug]

View File

@@ -27,7 +27,7 @@ async function localUnlock<T extends keyof GeneratedTypes['collections']>(
overrideAccess = true,
req = {} as PayloadRequest,
} = options
setRequestContext(options.req)
setRequestContext(req)
const collection = payload.collections[collectionSlug]

View File

@@ -18,7 +18,7 @@ async function localVerifyEmail<T extends keyof GeneratedTypes['collections']>(
options: Options<T>,
): Promise<boolean> {
const { collection: collectionSlug, req = {} as PayloadRequest, token } = options
setRequestContext(options.req)
setRequestContext(req)
const collection = payload.collections[collectionSlug]

View File

@@ -54,6 +54,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
})) || args
@@ -138,6 +139,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
user,
@@ -175,6 +177,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
token,
@@ -187,10 +190,11 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
user = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: user,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -205,6 +209,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
user =
(await hook({
collection: args.collection?.config,
context: req.context,
doc: user,
req,
@@ -220,6 +225,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
user =
(await hook({
collection: args.collection?.config,
context: req.context,
doc: user,
req,
@@ -238,6 +244,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: args.collection?.config,
operation: 'login',
result,
})

View File

@@ -46,6 +46,7 @@ async function logout(incomingArgs: Arguments): Promise<string> {
args =
(await hook({
collection: args.collection?.config,
context: req.context,
req,
res,

View File

@@ -69,6 +69,7 @@ async function me({ collection, req }: Arguments): Promise<Result> {
response =
(await hook({
collection: collection?.config,
context: req.context,
req,
response,

View File

@@ -39,6 +39,7 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'refresh',
})) || args
@@ -112,6 +113,7 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
result =
(await hook({
collection: args.collection?.config,
context: args.req.context,
exp,
req: args.req,
@@ -126,6 +128,7 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
result = await buildAfterOperation({
args,
collection: args.collection?.config,
operation: 'refresh',
result,
})

View File

@@ -41,6 +41,8 @@ type CreateOrUpdateOperation = Extract<HookOperationType, 'create' | 'update'>
export type BeforeOperationHook = (args: {
args?: any
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
/**
* Hook operation being performed
@@ -49,6 +51,8 @@ export type BeforeOperationHook = (args: {
}) => any
export type BeforeValidateHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
data?: Partial<T>
/**
@@ -65,6 +69,8 @@ export type BeforeValidateHook<T extends TypeWithID = any> = (args: {
}) => any
export type BeforeChangeHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
data: Partial<T>
/**
@@ -81,6 +87,8 @@ export type BeforeChangeHook<T extends TypeWithID = any> = (args: {
}) => any
export type AfterChangeHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
doc: T
/**
@@ -92,6 +100,8 @@ export type AfterChangeHook<T extends TypeWithID = any> = (args: {
}) => any
export type BeforeReadHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
doc: T
query: { [key: string]: any }
@@ -99,6 +109,8 @@ export type BeforeReadHook<T extends TypeWithID = any> = (args: {
}) => any
export type AfterReadHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
doc: T
findMany?: boolean
@@ -107,12 +119,16 @@ export type AfterReadHook<T extends TypeWithID = any> = (args: {
}) => any
export type BeforeDeleteHook = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
id: number | string
req: PayloadRequest
}) => any
export type AfterDeleteHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
doc: T
id: number | string
@@ -127,15 +143,21 @@ export type AfterErrorHook = (
err: Error,
res: unknown,
context: RequestContext,
/** The collection which this hook is being run on. This is null if the AfterError hook was be added to the payload-wide config */
collection: SanitizedCollectionConfig | null,
) => { response: any; status: number } | void
export type BeforeLoginHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
req: PayloadRequest
user: T
}) => any
export type AfterLoginHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
req: PayloadRequest
token: string
@@ -143,18 +165,24 @@ export type AfterLoginHook<T extends TypeWithID = any> = (args: {
}) => any
export type AfterLogoutHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
req: PayloadRequest
res: Response
}) => any
export type AfterMeHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
req: PayloadRequest
response: unknown
}) => any
export type AfterRefreshHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
exp: number
req: PayloadRequest
@@ -162,9 +190,16 @@ export type AfterRefreshHook<T extends TypeWithID = any> = (args: {
token: string
}) => any
export type AfterForgotPasswordHook = (args: { args?: any; context: RequestContext }) => any
export type AfterForgotPasswordHook = (args: {
args?: any
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
context: RequestContext
}) => any
type BeforeDuplicateArgs<T> = {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
data: T
locale?: string
}

View File

@@ -23,7 +23,9 @@ import { afterChange } from '../../fields/hooks/afterChange'
import { afterRead } from '../../fields/hooks/afterRead'
import { beforeChange } from '../../fields/hooks/beforeChange'
import { beforeValidate } from '../../fields/hooks/beforeValidate'
import fileExists from '../../uploads/fileExists'
import { generateFileData } from '../../uploads/generateFileData'
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles'
import { uploadFiles } from '../../uploads/uploadFiles'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
@@ -65,6 +67,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'create',
})) || args
@@ -139,10 +142,11 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
data = await beforeValidate({
collection: collectionConfig,
context: req.context,
data,
doc: {},
entityConfig: collectionConfig,
global: null,
operation: 'create',
overrideAccess,
req,
@@ -158,6 +162,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'create',
@@ -184,6 +189,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'create',
@@ -196,11 +202,12 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
const resultWithLocales = await beforeChange<Record<string, unknown>>({
collection: collectionConfig,
context: req.context,
data,
doc: {},
docWithLocales: {},
entityConfig: collectionConfig,
global: null,
operation: 'create',
req,
skipValidation: shouldSaveDraft,
@@ -293,10 +300,11 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -311,6 +319,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
@@ -322,27 +331,16 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
result = await afterChange({
collection: collectionConfig,
context: req.context,
data,
doc: result,
entityConfig: collectionConfig,
global: null,
operation: 'create',
previousDoc: {},
req,
})
// Remove temp files if enabled, as express-fileupload does not do this automatically
if (config.upload?.useTempFiles && collectionConfig.upload) {
const { files } = req
const fileArray = Array.isArray(files) ? files : [files]
await mapAsync(fileArray, async ({ file }) => {
// Still need this check because this will not be populated if using local API
if (file.tempFilePath) {
await unlinkFile(file.tempFilePath)
}
})
}
// /////////////////////////////////////
// afterChange - Collection
// /////////////////////////////////////
@@ -353,6 +351,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'create',
@@ -369,21 +368,12 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: collectionConfig,
operation: 'create',
result,
})
// Remove temp files if enabled, as express-fileupload does not do this automatically
if (config.upload?.useTempFiles && collectionConfig.upload) {
const { files } = req
const fileArray = Array.isArray(files) ? files : [files]
await mapAsync(fileArray, async ({ file }) => {
// Still need this check because this will not be populated if using local API
if (file.tempFilePath) {
await unlinkFile(file.tempFilePath)
}
})
}
await unlinkTempFiles({ collectionConfig, config, req })
// /////////////////////////////////////
// Return results

View File

@@ -49,6 +49,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'delete',
})) || args
@@ -126,6 +127,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
return hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
@@ -171,10 +173,11 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result || doc,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -189,6 +192,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result || doc,
req,
@@ -205,6 +209,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
@@ -249,6 +254,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: collectionConfig,
operation: 'delete',
result,
})

View File

@@ -40,6 +40,7 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'delete',
})) || args
@@ -82,6 +83,7 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
return hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
@@ -148,10 +150,11 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -166,6 +169,7 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
@@ -182,6 +186,7 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
@@ -194,6 +199,7 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: collectionConfig,
operation: 'deleteByID',
result,
})

View File

@@ -46,6 +46,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
})) || args
@@ -166,6 +167,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
docRef =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef,
query: fullWhere,
@@ -187,12 +189,13 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
docs: await Promise.all(
result.docs.map(async (doc) =>
afterRead<T>({
collection: collectionConfig,
context: req.context,
currentDepth,
depth,
doc,
entityConfig: collectionConfig,
findMany: true,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -216,6 +219,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
docRef =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef,
findMany: true,
@@ -235,6 +239,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
result = await buildAfterOperation<T>({
args,
collection: collectionConfig,
operation: 'find',
result,
})

View File

@@ -39,6 +39,7 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
})) || args
@@ -138,6 +139,7 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
@@ -150,11 +152,12 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
currentDepth,
depth,
doc: result,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -169,6 +172,7 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
@@ -182,6 +186,7 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
result = await buildAfterOperation<T>({
args,
collection: collectionConfig,
operation: 'findByID',
result: result as any,
}) // TODO: fix this typing

View File

@@ -93,6 +93,7 @@ async function findVersionByID<T extends TypeWithID = any>(
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
@@ -105,11 +106,12 @@ async function findVersionByID<T extends TypeWithID = any>(
// /////////////////////////////////////
result.version = await afterRead({
collection: collectionConfig,
context: req.context,
currentDepth,
depth,
doc: result.version,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -124,6 +126,7 @@ async function findVersionByID<T extends TypeWithID = any>(
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,

View File

@@ -94,6 +94,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
docRef.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef.version,
query: fullWhere,
@@ -116,11 +117,12 @@ async function findVersions<T extends TypeWithVersion<T>>(
result.docs.map(async (data) => ({
...data,
version: await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: data.version,
entityConfig: collectionConfig,
findMany: true,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -144,6 +146,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
docRef.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: doc.version,
findMany: true,

View File

@@ -56,7 +56,7 @@ export default async function findLocal<T extends keyof GeneratedTypes['collecti
user,
where,
} = options
setRequestContext(options.req, context)
setRequestContext(req, context)
const collection = payload.collections[collectionSlug]
const defaultLocale = payload?.config?.localization

View File

@@ -47,7 +47,7 @@ export default async function findByIDLocal<T extends keyof GeneratedTypes['coll
showHiddenFields,
user,
} = options
setRequestContext(options.req, context)
setRequestContext(req, context)
const collection = payload.collections[collectionSlug]
const defaultLocale = payload?.config?.localization

View File

@@ -44,7 +44,7 @@ export default async function findVersionByIDLocal<T extends keyof GeneratedType
req = {} as PayloadRequest,
showHiddenFields,
} = options
setRequestContext(options.req, context)
setRequestContext(req, context)
const collection = payload.collections[collectionSlug]
const defaultLocale = payload?.config?.localization

View File

@@ -135,10 +135,11 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -153,6 +154,7 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
@@ -164,10 +166,11 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
// /////////////////////////////////////
result = await afterChange({
collection: collectionConfig,
context: req.context,
data: result,
doc: result,
entityConfig: collectionConfig,
global: null,
operation: 'update',
previousDoc: prevDocWithLocales,
req,
@@ -182,6 +185,7 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',

View File

@@ -56,6 +56,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
})) || args
@@ -169,10 +170,11 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
try {
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc,
entityConfig: collectionConfig,
global: null,
overrideAccess: true,
req,
showHiddenFields: true,
@@ -193,10 +195,11 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
data = await beforeValidate<DeepPartial<GeneratedTypes['collections'][TSlug]>>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
entityConfig: collectionConfig,
global: null,
operation: 'update',
overrideAccess,
req,
@@ -211,6 +214,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
@@ -236,6 +240,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
@@ -250,11 +255,12 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
let result = await beforeChange<GeneratedTypes['collections'][TSlug]>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
docWithLocales: doc,
entityConfig: collectionConfig,
global: null,
operation: 'update',
req,
skipValidation: shouldSaveDraft || data._status === 'draft',
@@ -297,10 +303,11 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -315,6 +322,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
@@ -326,10 +334,11 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
result = await afterChange<GeneratedTypes['collections'][TSlug]>({
collection: collectionConfig,
context: req.context,
data,
doc: result,
entityConfig: collectionConfig,
global: null,
operation: 'update',
previousDoc: originalDoc,
req,
@@ -344,6 +353,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
@@ -385,6 +395,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: collectionConfig,
operation: 'update',
result,
})

View File

@@ -55,6 +55,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
})) || args
@@ -123,10 +124,11 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
if (!docWithLocales && hasWherePolicy) throw new Forbidden(t)
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: docWithLocales,
entityConfig: collectionConfig,
global: null,
overrideAccess: true,
req,
showHiddenFields: true,
@@ -166,10 +168,11 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
data = await beforeValidate<DeepPartial<GeneratedTypes['collections'][TSlug]>>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
entityConfig: collectionConfig,
global: null,
operation: 'update',
overrideAccess,
req,
@@ -184,6 +187,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
@@ -209,6 +213,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
@@ -223,11 +228,12 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
let result = await beforeChange<GeneratedTypes['collections'][TSlug]>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
docWithLocales,
entityConfig: collectionConfig,
global: null,
operation: 'update',
req,
skipValidation: shouldSaveDraft || data._status === 'draft',
@@ -285,10 +291,11 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result,
entityConfig: collectionConfig,
global: null,
overrideAccess,
req,
showHiddenFields,
@@ -303,6 +310,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
@@ -314,10 +322,11 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
// /////////////////////////////////////
result = await afterChange<GeneratedTypes['collections'][TSlug]>({
collection: collectionConfig,
context: req.context,
data,
doc: result,
entityConfig: collectionConfig,
global: null,
operation: 'update',
previousDoc: originalDoc,
req,
@@ -332,6 +341,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
@@ -346,6 +356,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: collectionConfig,
operation: 'updateByID',
result,
})

View File

@@ -1,7 +1,7 @@
import type forgotPassword from '../../auth/operations/forgotPassword'
import type login from '../../auth/operations/login'
import type refresh from '../../auth/operations/refresh'
import type { AfterOperationHook, TypeWithID } from '../config/types'
import type { AfterOperationHook, SanitizedCollectionConfig, TypeWithID } from '../config/types'
import type create from './create'
import type deleteOperation from './delete'
import type deleteByID from './deleteByID'
@@ -25,51 +25,71 @@ export type AfterOperationMap<T extends TypeWithID> = {
export type AfterOperationArg<T extends TypeWithID> =
| {
args: Parameters<AfterOperationMap<T>['create']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'create'
result: Awaited<ReturnType<AfterOperationMap<T>['create']>>
}
| {
args: Parameters<AfterOperationMap<T>['delete']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'delete'
result: Awaited<ReturnType<AfterOperationMap<T>['delete']>>
}
| {
args: Parameters<AfterOperationMap<T>['deleteByID']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'deleteByID'
result: Awaited<ReturnType<AfterOperationMap<T>['deleteByID']>>
}
| {
args: Parameters<AfterOperationMap<T>['find']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'find'
result: Awaited<ReturnType<AfterOperationMap<T>['find']>>
}
| {
args: Parameters<AfterOperationMap<T>['findByID']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'findByID'
result: Awaited<ReturnType<AfterOperationMap<T>['findByID']>>
}
| {
args: Parameters<AfterOperationMap<T>['forgotPassword']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'forgotPassword'
result: Awaited<ReturnType<AfterOperationMap<T>['forgotPassword']>>
}
| {
args: Parameters<AfterOperationMap<T>['login']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'login'
result: Awaited<ReturnType<AfterOperationMap<T>['login']>>
}
| {
args: Parameters<AfterOperationMap<T>['refresh']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'refresh'
result: Awaited<ReturnType<AfterOperationMap<T>['refresh']>>
}
| {
args: Parameters<AfterOperationMap<T>['update']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'update'
result: Awaited<ReturnType<AfterOperationMap<T>['update']>>
}
| {
args: Parameters<AfterOperationMap<T>['updateByID']>[0]
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
operation: 'updateByID'
result: Awaited<ReturnType<AfterOperationMap<T>['updateByID']>>
}
@@ -82,7 +102,7 @@ export const buildAfterOperation = async <
>(
operationArgs: AfterOperationArg<T> & { operation: O },
): Promise<Awaited<ReturnType<AfterOperationMap<T>[O]>>> => {
const { args, operation, result } = operationArgs
const { args, collection, operation, result } = operationArgs
let newResult = result
@@ -92,6 +112,7 @@ export const buildAfterOperation = async <
const hookResult = await hook({
args,
collection,
operation,
result: newResult,
} as AfterOperationArg<T>)

View File

@@ -90,12 +90,17 @@ export default joi.object({
debug: joi.boolean(),
defaultDepth: joi.number().min(0).max(30),
defaultMaxTextLength: joi.number(),
editor: joi.object().required().keys({
CellComponent: component.required(),
FieldComponent: component.required(),
afterReadPromise: joi.func().required(),
validate: joi.func().required(),
}),
editor: joi
.object()
.required()
.keys({
CellComponent: component.required(),
FieldComponent: component.required(),
afterReadPromise: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})
.unknown(),
email: joi.object(),
endpoints: endpointsSchema,
express: joi.object().keys({

View File

@@ -47,11 +47,17 @@ const errorHandler =
err,
response,
req.context,
req.collection.config,
)) || { response, status })
}
if (typeof config.hooks.afterError === 'function') {
;({ response, status } = (await config.hooks.afterError(err, response, req.context)) || {
;({ response, status } = (await config.hooks.afterError(
err,
response,
req.context,
req.collection.config,
)) || {
response,
status,
})

View File

@@ -353,13 +353,17 @@ export const blocks = baseField.keys({
export const richText = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.default(),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
editor: joi.object().keys({
CellComponent: componentSchema.required(),
FieldComponent: componentSchema.required(),
afterReadPromise: joi.func().required(),
validate: joi.func().required(),
}),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func(), joi.object()),
editor: joi
.object()
.keys({
CellComponent: componentSchema.required(),
FieldComponent: componentSchema.required(),
afterReadPromise: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})
.unknown(),
type: joi.string().valid('richText').required(),
})

View File

@@ -9,18 +9,25 @@ import type { Description } from '../../admin/components/forms/FieldDescription/
import type { RowLabel } from '../../admin/components/forms/RowLabel/types'
import type { RichTextAdapter } from '../../admin/components/forms/field-types/RichText/types'
import type { User } from '../../auth'
import type { TypeWithID } from '../../collections/config/types'
import type { SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types'
import type { SanitizedConfig } from '../../config/types'
import type { PayloadRequest, RequestContext } from '../../express/types'
import type { SanitizedGlobalConfig } from '../../globals/config/types'
import type { Payload } from '../../payload'
import type { Operation, Where } from '../../types'
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
collection: SanitizedCollectionConfig | null
context: RequestContext
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
data?: Partial<T>
/** The field which the hook is running against. */
field: FieldAffectingData
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
findMany?: boolean
/** The global which the field belongs to. If the field belongs to a collection, this will be null. */
global: SanitizedGlobalConfig | null
/** 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. */
operation?: 'create' | 'delete' | 'read' | 'update'
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */

View File

@@ -6,20 +6,23 @@ import { deepCopyObject } from '../../../utilities/deepCopyObject'
import { traverseFields } from './traverseFields'
type Args<T> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown> | T
doc: Record<string, unknown> | T
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
previousDoc: Record<string, unknown> | T
req: PayloadRequest
}
export const afterChange = async <T extends Record<string, unknown>>({
collection,
context,
data,
doc: incomingDoc,
entityConfig,
global,
operation,
previousDoc,
req,
@@ -27,10 +30,12 @@ export const afterChange = async <T extends Record<string, unknown>>({
const doc = deepCopyObject(incomingDoc)
await traverseFields({
collection,
context,
data,
doc,
fields: entityConfig.fields,
fields: collection?.fields || global?.fields,
global,
operation,
previousDoc,
previousSiblingDoc: previousDoc,

View File

@@ -1,15 +1,19 @@
/* eslint-disable no-param-reassign */
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Field, TabAsField } from '../../config/types'
import { fieldAffectsData, tabHasName } from '../../config/types'
import { traverseFields } from './traverseFields'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
doc: Record<string, unknown>
field: Field | TabAsField
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
previousDoc: Record<string, unknown>
previousSiblingDoc: Record<string, unknown>
@@ -22,10 +26,12 @@ type Args = {
// - Execute field hooks
export const promise = async ({
collection,
context,
data,
doc,
field,
global,
operation,
previousDoc,
previousSiblingDoc,
@@ -40,8 +46,11 @@ export const promise = async ({
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
previousDoc,
@@ -63,10 +72,12 @@ export const promise = async ({
switch (field.type) {
case 'group': {
await traverseFields({
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
previousDoc,
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
@@ -86,10 +97,12 @@ export const promise = async ({
rows.forEach((row, i) => {
promises.push(
traverseFields({
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
previousDoc,
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
@@ -115,10 +128,12 @@ export const promise = async ({
if (block) {
promises.push(
traverseFields({
collection,
context,
data,
doc,
fields: block.fields,
global,
operation,
previousDoc,
previousSiblingDoc:
@@ -139,10 +154,12 @@ export const promise = async ({
case 'row':
case 'collapsible': {
await traverseFields({
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
previousDoc,
previousSiblingDoc: { ...previousSiblingDoc },
@@ -166,10 +183,12 @@ export const promise = async ({
}
await traverseFields({
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
previousDoc,
previousSiblingDoc: tabPreviousSiblingDoc,
@@ -183,10 +202,12 @@ export const promise = async ({
case 'tabs': {
await traverseFields({
collection,
context,
data,
doc,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
global,
operation,
previousDoc,
previousSiblingDoc: { ...previousSiblingDoc },

View File

@@ -1,13 +1,17 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Field, TabAsField } from '../../config/types'
import { promise } from './promise'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
doc: Record<string, unknown>
fields: (Field | TabAsField)[]
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
previousDoc: Record<string, unknown>
previousSiblingDoc: Record<string, unknown>
@@ -17,10 +21,12 @@ type Args = {
}
export const traverseFields = async ({
collection,
context,
data,
doc,
fields,
global,
operation,
previousDoc,
previousSiblingDoc,
@@ -33,10 +39,12 @@ export const traverseFields = async ({
fields.forEach((field) => {
promises.push(
promise({
collection,
context,
data,
doc,
field,
global,
operation,
previousDoc,
previousSiblingDoc,

View File

@@ -6,13 +6,14 @@ import { deepCopyObject } from '../../../utilities/deepCopyObject'
import { traverseFields } from './traverseFields'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
currentDepth?: number
depth: number
doc: Record<string, unknown>
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
findMany?: boolean
flattenLocales?: boolean
global: SanitizedGlobalConfig | null
overrideAccess: boolean
req: PayloadRequest
showHiddenFields: boolean
@@ -20,13 +21,14 @@ type Args = {
export async function afterRead<T = any>(args: Args): Promise<T> {
const {
collection,
context,
currentDepth: incomingCurrentDepth,
depth: incomingDepth,
doc: incomingDoc,
entityConfig,
findMany,
flattenLocales = true,
global,
overrideAccess,
req,
showHiddenFields,
@@ -45,14 +47,16 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
const currentDepth = incomingCurrentDepth || 1
traverseFields({
collection,
context,
currentDepth,
depth,
doc,
fieldPromises,
fields: entityConfig.fields,
fields: collection?.fields || global?.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,

View File

@@ -1,6 +1,8 @@
/* eslint-disable no-param-reassign */
import type { RichTextAdapter } from '../../../admin/components/forms/field-types/RichText/types'
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Field, TabAsField } from '../../config/types'
import { fieldAffectsData, tabHasName } from '../../config/types'
@@ -8,6 +10,7 @@ import relationshipPopulationPromise from './relationshipPopulationPromise'
import { traverseFields } from './traverseFields'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
currentDepth: number
depth: number
@@ -16,6 +19,7 @@ type Args = {
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
global: SanitizedGlobalConfig | null
overrideAccess: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
@@ -32,6 +36,7 @@ type Args = {
// - Populate relationships
export const promise = async ({
collection,
context,
currentDepth,
depth,
@@ -40,6 +45,7 @@ export const promise = async ({
fieldPromises,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -129,8 +135,9 @@ export const promise = async ({
case 'richText': {
const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) {
const afterReadPromise = editor.afterReadPromise({
// This is run here AND in the GraphQL Resolver
if (editor?.populationPromise) {
const populationPromise = editor.populationPromise({
currentDepth,
depth,
field,
@@ -140,6 +147,19 @@ export const promise = async ({
siblingDoc,
})
if (populationPromise) {
populationPromises.push(populationPromise)
}
}
// This is only run here, independent of depth
if (editor?.afterReadPromise) {
const afterReadPromise = editor?.afterReadPromise({
field,
incomingEditorState: siblingDoc[field.name] as object,
siblingDoc,
})
if (afterReadPromise) {
populationPromises.push(afterReadPromise)
}
@@ -179,8 +199,11 @@ export const promise = async ({
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
(async () => {
const hookedValue = await currentHook({
collection,
context,
data: doc,
field,
global,
operation: 'read',
originalDoc: doc,
req,
@@ -197,9 +220,12 @@ export const promise = async ({
await Promise.all(hookPromises)
} else {
const hookedValue = await currentHook({
collection,
context,
data: doc,
field,
findMany,
global,
operation: 'read',
originalDoc: doc,
req,
@@ -252,6 +278,7 @@ export const promise = async ({
if (typeof siblingDoc[field.name] !== 'object') groupDoc = {}
traverseFields({
collection,
context,
currentDepth,
depth,
@@ -260,6 +287,7 @@ export const promise = async ({
fields: field.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -276,6 +304,7 @@ export const promise = async ({
if (Array.isArray(rows)) {
rows.forEach((row) => {
traverseFields({
collection,
context,
currentDepth,
depth,
@@ -284,6 +313,7 @@ export const promise = async ({
fields: field.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -296,6 +326,7 @@ export const promise = async ({
if (Array.isArray(localeRows)) {
localeRows.forEach((row) => {
traverseFields({
collection,
context,
currentDepth,
depth,
@@ -304,6 +335,7 @@ export const promise = async ({
fields: field.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -328,6 +360,7 @@ export const promise = async ({
if (block) {
traverseFields({
collection,
context,
currentDepth,
depth,
@@ -336,6 +369,7 @@ export const promise = async ({
fields: block.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -352,6 +386,7 @@ export const promise = async ({
if (block) {
traverseFields({
collection,
context,
currentDepth,
depth,
@@ -360,6 +395,7 @@ export const promise = async ({
fields: block.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -380,6 +416,7 @@ export const promise = async ({
case 'row':
case 'collapsible': {
traverseFields({
collection,
context,
currentDepth,
depth,
@@ -388,6 +425,7 @@ export const promise = async ({
fields: field.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -406,6 +444,7 @@ export const promise = async ({
}
await traverseFields({
collection,
context,
currentDepth,
depth,
@@ -414,6 +453,7 @@ export const promise = async ({
fields: field.fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -426,6 +466,7 @@ export const promise = async ({
case 'tabs': {
traverseFields({
collection,
context,
currentDepth,
depth,
@@ -434,6 +475,7 @@ export const promise = async ({
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,

View File

@@ -1,9 +1,12 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Field, TabAsField } from '../../config/types'
import { promise } from './promise'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
currentDepth: number
depth: number
@@ -12,6 +15,7 @@ type Args = {
fields: (Field | TabAsField)[]
findMany: boolean
flattenLocales: boolean
global: SanitizedGlobalConfig | null
overrideAccess: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
@@ -20,6 +24,7 @@ type Args = {
}
export const traverseFields = ({
collection,
context,
currentDepth,
depth,
@@ -28,6 +33,7 @@ export const traverseFields = ({
fields,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,
@@ -37,6 +43,7 @@ export const traverseFields = ({
fields.forEach((field) => {
fieldPromises.push(
promise({
collection,
context,
currentDepth,
depth,
@@ -45,6 +52,7 @@ export const traverseFields = ({
fieldPromises,
findMany,
flattenLocales,
global,
overrideAccess,
populationPromises,
req,

View File

@@ -8,11 +8,12 @@ import { deepCopyObject } from '../../../utilities/deepCopyObject'
import { traverseFields } from './traverseFields'
type Args<T> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown> | T
doc: Record<string, unknown> | T
docWithLocales: Record<string, unknown>
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
global: SanitizedGlobalConfig | null
id?: number | string
operation: Operation
req: PayloadRequest
@@ -21,11 +22,12 @@ type Args<T> = {
export const beforeChange = async <T extends Record<string, unknown>>({
id,
collection,
context,
data: incomingData,
doc,
docWithLocales,
entityConfig,
global,
operation,
req,
skipValidation,
@@ -36,12 +38,14 @@ export const beforeChange = async <T extends Record<string, unknown>>({
await traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: entityConfig.fields,
fields: collection?.fields || global?.fields,
global,
mergeLocaleActions,
operation,
path: '',

View File

@@ -1,7 +1,9 @@
/* eslint-disable no-param-reassign */
import merge from 'deepmerge'
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Operation } from '../../../types'
import type { Field, TabAsField } from '../../config/types'
@@ -10,12 +12,14 @@ import { getExistingRowDoc } from './getExistingRowDoc'
import { traverseFields } from './traverseFields'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
doc: Record<string, unknown>
docWithLocales: Record<string, unknown>
errors: { field: string; message: string }[]
field: Field | TabAsField
global: SanitizedGlobalConfig | null
id?: number | string
mergeLocaleActions: (() => void)[]
operation: Operation
@@ -36,12 +40,14 @@ type Args = {
export const promise = async ({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
field,
global,
mergeLocaleActions,
operation,
path,
@@ -75,8 +81,11 @@ export const promise = async ({
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
req,
@@ -183,12 +192,14 @@ export const promise = async ({
await traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: field.fields,
global,
mergeLocaleActions,
operation,
path: `${path}${field.name}.`,
@@ -211,12 +222,14 @@ export const promise = async ({
promises.push(
traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: field.fields,
global,
mergeLocaleActions,
operation,
path: `${path}${field.name}.${i}.`,
@@ -251,12 +264,14 @@ export const promise = async ({
promises.push(
traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: block.fields,
global,
mergeLocaleActions,
operation,
path: `${path}${field.name}.${i}.`,
@@ -280,12 +295,14 @@ export const promise = async ({
case 'collapsible': {
await traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: field.fields,
global,
mergeLocaleActions,
operation,
path,
@@ -319,12 +336,14 @@ export const promise = async ({
await traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: field.fields,
global,
mergeLocaleActions,
operation,
path: tabPath,
@@ -341,12 +360,14 @@ export const promise = async ({
case 'tabs': {
await traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
global,
mergeLocaleActions,
operation,
path,

View File

@@ -1,16 +1,20 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Operation } from '../../../types'
import type { Field, TabAsField } from '../../config/types'
import { promise } from './promise'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
doc: Record<string, unknown>
docWithLocales: Record<string, unknown>
errors: { field: string; message: string }[]
fields: (Field | TabAsField)[]
global: SanitizedGlobalConfig | null
id?: number | string
mergeLocaleActions: (() => void)[]
operation: Operation
@@ -24,12 +28,14 @@ type Args = {
export const traverseFields = async ({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields,
global,
mergeLocaleActions,
operation,
path,
@@ -45,12 +51,14 @@ export const traverseFields = async ({
promises.push(
promise({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
field,
global,
mergeLocaleActions,
operation,
path,

View File

@@ -6,10 +6,11 @@ import { deepCopyObject } from '../../../utilities/deepCopyObject'
import { traverseFields } from './traverseFields'
type Args<T> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown> | T
doc?: Record<string, unknown> | T
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
global: SanitizedGlobalConfig | null
id?: number | string
operation: 'create' | 'update'
overrideAccess: boolean
@@ -18,10 +19,11 @@ type Args<T> = {
export const beforeValidate = async <T extends Record<string, unknown>>({
id,
collection,
context,
data: incomingData,
doc,
entityConfig,
global,
operation,
overrideAccess,
req,
@@ -30,10 +32,12 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
await traverseFields({
id,
collection,
context,
data,
doc,
fields: entityConfig.fields,
fields: collection?.fields || global?.fields,
global,
operation,
overrideAccess,
req,

View File

@@ -1,5 +1,7 @@
/* eslint-disable no-param-reassign */
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Field, TabAsField } from '../../config/types'
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types'
@@ -9,10 +11,12 @@ import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc'
import { traverseFields } from './traverseFields'
type Args<T> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: T
doc: T
field: Field | TabAsField
global: SanitizedGlobalConfig | null
id?: number | string
operation: 'create' | 'update'
overrideAccess: boolean
@@ -30,10 +34,12 @@ type Args<T> = {
export const promise = async <T>({
id,
collection,
context,
data,
doc,
field,
global,
operation,
overrideAccess,
req,
@@ -209,8 +215,11 @@ export const promise = async <T>({
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
req,
@@ -263,10 +272,12 @@ export const promise = async <T>({
await traverseFields({
id,
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
overrideAccess,
req,
@@ -282,14 +293,16 @@ export const promise = async <T>({
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
rows.forEach((row) => {
promises.push(
traverseFields({
id,
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
overrideAccess,
req,
@@ -308,7 +321,7 @@ export const promise = async <T>({
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
rows.forEach((row) => {
const rowSiblingDoc = getExistingRowDoc(row, siblingDoc[field.name])
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
@@ -319,10 +332,12 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
collection,
context,
data,
doc,
fields: block.fields,
global,
operation,
overrideAccess,
req,
@@ -342,10 +357,12 @@ export const promise = async <T>({
case 'collapsible': {
await traverseFields({
id,
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
overrideAccess,
req,
@@ -372,10 +389,12 @@ export const promise = async <T>({
await traverseFields({
id,
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
overrideAccess,
req,
@@ -389,10 +408,12 @@ export const promise = async <T>({
case 'tabs': {
await traverseFields({
id,
collection,
context,
data,
doc,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
global,
operation,
overrideAccess,
req,

View File

@@ -1,13 +1,17 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { SanitizedGlobalConfig } from '../../../globals/config/types'
import type { Field, TabAsField } from '../../config/types'
import { promise } from './promise'
type Args<T> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: T
doc: T
fields: (Field | TabAsField)[]
global: SanitizedGlobalConfig | null
id?: number | string
operation: 'create' | 'update'
overrideAccess: boolean
@@ -18,10 +22,12 @@ type Args<T> = {
export const traverseFields = async <T>({
id,
collection,
context,
data,
doc,
fields,
global,
operation,
overrideAccess,
req,
@@ -33,10 +39,12 @@ export const traverseFields = async <T>({
promises.push(
promise({
id,
collection,
context,
data,
doc,
field,
global,
operation,
overrideAccess,
req,

View File

@@ -18,6 +18,7 @@ import type {
LivePreviewConfig,
} from '../../config/types'
import type { PayloadRequest } from '../../express/types'
import type { RequestContext } from '../../express/types'
import type { Field } from '../../fields/config/types'
import type { Where } from '../../types'
import type { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types'
@@ -27,20 +28,46 @@ export type TypeWithID = {
}
export type BeforeValidateHook = (args: {
context: RequestContext
data?: any
/** The global which this hook is being run on */
global: SanitizedGlobalConfig
originalDoc?: any
req?: PayloadRequest
}) => any
export type BeforeChangeHook = (args: { data: any; originalDoc?: any; req: PayloadRequest }) => any
export type BeforeChangeHook = (args: {
context: RequestContext
data: any
/** The global which this hook is being run on */
global: SanitizedGlobalConfig
originalDoc?: any
req: PayloadRequest
}) => any
export type AfterChangeHook = (args: { doc: any; previousDoc: any; req: PayloadRequest }) => any
export type AfterChangeHook = (args: {
context: RequestContext
doc: any
/** The global which this hook is being run on */
global: SanitizedGlobalConfig
previousDoc: any
req: PayloadRequest
}) => any
export type BeforeReadHook = (args: { doc: any; req: PayloadRequest }) => any
export type BeforeReadHook = (args: {
context: RequestContext
doc: any
/** The global which this hook is being run on */
global: SanitizedGlobalConfig
req: PayloadRequest
}) => any
export type AfterReadHook = (args: {
context: RequestContext
doc: any
findMany?: boolean
/** The global which this hook is being run on */
global: SanitizedGlobalConfig
query?: Where
req: PayloadRequest
}) => any

View File

@@ -83,7 +83,9 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}, Promise.resolve())
@@ -93,10 +95,11 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
// /////////////////////////////////////
doc = await afterRead({
collection: null,
context: req.context,
depth,
doc,
entityConfig: globalConfig,
global: globalConfig,
overrideAccess,
req,
showHiddenFields,
@@ -111,7 +114,9 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}, Promise.resolve())

View File

@@ -87,7 +87,9 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
result =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
req,
})) || result.version
}, Promise.resolve())
@@ -97,11 +99,12 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
// /////////////////////////////////////
result.version = await afterRead({
collection: null,
context: req.context,
currentDepth,
depth,
doc: result.version,
entityConfig: globalConfig,
global: globalConfig,
overrideAccess,
req,
showHiddenFields,
@@ -116,7 +119,9 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
result.version =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
query: findGlobalVersionsArgs.where,
req,
})) || result.version

View File

@@ -88,11 +88,12 @@ async function findVersions<T extends TypeWithVersion<T>>(
paginatedDocs.docs.map(async (data) => ({
...data,
version: await afterRead({
collection: null,
context: req.context,
depth,
doc: data.version,
entityConfig: globalConfig,
findMany: true,
global: globalConfig,
overrideAccess,
req,
showHiddenFields,
@@ -115,8 +116,14 @@ async function findVersions<T extends TypeWithVersion<T>>(
await priorHook
docRef.version =
(await hook({ doc: doc.version, findMany: true, query: fullWhere, req })) ||
doc.version
(await hook({
context: req.context,
doc: doc.version,
findMany: true,
global: globalConfig,
query: fullWhere,
req,
})) || doc.version
}, Promise.resolve())
return docRef

View File

@@ -97,10 +97,11 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
// /////////////////////////////////////
result = await afterRead({
collection: null,
context: req.context,
depth,
doc: result,
entityConfig: globalConfig,
global: globalConfig,
overrideAccess,
req,
showHiddenFields,
@@ -115,7 +116,9 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}, Promise.resolve())
@@ -125,10 +128,11 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
// /////////////////////////////////////
result = await afterChange({
collection: null,
context: req.context,
data: result,
doc: result,
entityConfig: globalConfig,
global: globalConfig,
operation: 'update',
previousDoc,
req,
@@ -143,7 +147,9 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc,
req,
})) || result

View File

@@ -92,10 +92,11 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
}
const originalDoc = await afterRead({
collection: null,
context: req.context,
depth: 0,
doc: globalJSON,
entityConfig: globalConfig,
global: globalConfig,
overrideAccess: true,
req,
showHiddenFields,
@@ -106,10 +107,11 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
// /////////////////////////////////////
data = await beforeValidate({
collection: null,
context: req.context,
data,
doc: originalDoc,
entityConfig: globalConfig,
global: globalConfig,
operation: 'update',
overrideAccess,
req,
@@ -124,7 +126,9 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
@@ -139,7 +143,9 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
@@ -150,11 +156,12 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
// /////////////////////////////////////
let result = await beforeChange({
collection: null,
context: req.context,
data,
doc: originalDoc,
docWithLocales: globalJSON,
entityConfig: globalConfig,
global: globalConfig,
operation: 'update',
req,
skipValidation: shouldSaveDraft,
@@ -204,10 +211,11 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
// /////////////////////////////////////
result = await afterRead({
collection: null,
context: req.context,
depth,
doc: result,
entityConfig: globalConfig,
global: globalConfig,
overrideAccess,
req,
showHiddenFields,
@@ -222,7 +230,9 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}, Promise.resolve())
@@ -232,10 +242,11 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
// /////////////////////////////////////
result = await afterChange({
collection: null,
context: req.context,
data,
doc: result,
entityConfig: globalConfig,
global: globalConfig,
operation: 'update',
previousDoc: originalDoc,
req,
@@ -250,7 +261,9 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc: originalDoc,
req,
})) || result

View File

@@ -35,7 +35,7 @@ const errorHandler = async (
}
if (afterErrorHook) {
;({ response } = (await afterErrorHook(err, response, null)) || { response })
;({ response } = (await afterErrorHook(err, response, null, null)) || { response })
}
return response

View File

@@ -429,8 +429,13 @@ function buildObjectType({
if (typeof args.depth !== 'undefined') depth = args.depth
const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) {
await editor?.afterReadPromise({
// RichText fields have their own depth argument in GraphQL.
// This is why the populationPromise (which populates richtext fields like uploads and relationships)
// is run here again, with the provided depth.
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
if (editor?.populationPromise) {
await editor?.populationPromise({
depth,
field,
req: context.req,

7
packages/plugin-stripe/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules
.env
dist
demo/uploads
build
.DS_Store
package-lock.json

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": "inline",
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "commonjs"
}
}

View File

@@ -0,0 +1,289 @@
# Payload Stripe Plugin
[![NPM](https://img.shields.io/npm/v/@payloadcms/plugin-stripe)](https://www.npmjs.com/package/@payloadcms/plugin-stripe)
A plugin for [Payload](https://github.com/payloadcms/payload) to connect [Stripe](https://stripe.com) and Payload.
Core features:
- Hides your Stripe credentials when shipping SaaS applications
- Allows restricted keys through [Payload access control](https://payloadcms.com/docs/access-control/overview)
- Enables a two-way communication channel between Stripe and Payload
- Proxies the [Stripe REST API](https://stripe.com/docs/api)
- Proxies [Stripe webhooks](https://stripe.com/docs/webhooks)
- Automatically syncs data between the two platforms
## Installation
```bash
yarn add @payloadcms/plugin-stripe
# OR
npm i @payloadcms/plugin-stripe
```
## Basic Usage
In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options):
```js
import { buildConfig } from 'payload/config'
import stripePlugin from '@payloadcms/plugin-stripe'
const config = buildConfig({
plugins: [
stripePlugin({
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
}),
],
})
export default config
```
### Options
- `stripeSecretKey`: string
Required. Your Stripe secret key.
- `sync`: array
Optional. An array of sync configs. This will automatically configure a sync between Payload collections and Stripe resources on create, delete, and update. See [sync](#sync) for more details.
- `stripeWebhooksEndpointSecret`: string
Optional. Your Stripe webhook endpoint secret. This is needed only if you wish to sync data _from_ Stripe _to_ Payload.
- `rest`: boolean
Optional. When `true`, opens the `/api/stripe/rest` endpoint. See [endpoints](#endpoints) for more details.
- `webhooks`: object | function
Optional. Either a function to handle all webhooks events, or an object of Stripe webhook handlers, keyed to the name of the event. See [webhooks](#webhooks) for more details or for a list of all available webhooks, see [here](https://stripe.com/docs/cli/trigger#trigger-event).
- `logs`: boolean
Optional. When `true`, logs sync events to the console as they happen.
## Sync
This option will setup a basic sync between Payload collections and Stripe resources for you automatically. It will create all the necessary hooks and webhooks handlers, so the only thing you have to do is map your Payload fields to their corresponding Stripe properties. As documents are created, updated, and deleted from either Stripe or Payload, the changes are reflected on either side.
> NOTE: If you wish to enable a _two-way_ sync, be sure to setup [`webhooks`](#webhooks) and pass the `stripeWebhooksEndpointSecret` through your config.
> NOTE: Due to limitations in the Stripe API, this currently only works with top-level fields. This is because every Stripe object is a separate entity, making it difficult to abstract into a simple reusable library. In the future, we may find a pattern around this. But for now, cases like that will need to be hard-coded. See the [demo](./demo) for an example of this.
```js
import { buildConfig } from 'payload/config'
import stripePlugin from '@payloadcms/plugin-stripe'
const config = buildConfig({
plugins: [
stripePlugin({
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET,
sync: [
{
collection: 'customers',
stripeResourceType: 'customers',
stripeResourceTypeSingular: 'customer',
fields: [
{
fieldPath: 'name', // this is a field on your own Payload config
stripeProperty: 'name', // use dot notation, if applicable
},
],
},
],
}),
],
})
export default config
```
Using `sync` will do the following:
- Adds and maintains a `stripeID` read-only field on each collection, this is a field generated _by Stripe_ and used as a cross-reference
- Adds a direct link to the resource on Stripe.com
- Adds and maintains an `skipSync` read-only flag on each collection to prevent infinite syncs when hooks trigger webhooks
- Adds the following hooks to each collection:
- `beforeValidate`: `createNewInStripe`
- `beforeChange`: `syncExistingWithStripe`
- `afterDelete`: `deleteFromStripe`
- Handles the following Stripe webhooks
- `STRIPE_TYPE.created`: `handleCreatedOrUpdated`
- `STRIPE_TYPE.updated`: `handleCreatedOrUpdated`
- `STRIPE_TYPE.deleted`: `handleDeleted`
### Endpoints
The following custom endpoints are automatically opened for you:
> NOTE: the `/api` part of these routes may be different based on the settings defined in your Payload config.
- #### `POST /api/stripe/rest`
If `rest` is true, proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](https://payloadcms.com/docs/access-control/overview) and returns the result. If you need to proxy the API server-side, use the [stripeProxy](#node) function.
```js
const res = await fetch(`/api/stripe/rest`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
// Authorization: `JWT ${token}` // NOTE: do this if not in a browser (i.e. curl or Postman)
},
body: JSON.stringify({
stripeMethod: 'stripe.subscriptions.list',
stripeArgs: [
{
customer: 'abc',
},
],
}),
})
```
- #### `POST /stripe/webhooks`
Returns an http status code. This is where all Stripe webhook events are sent to be handled. See [webhooks](#webhooks).
### Webhooks
[Stripe webhooks](https://stripe.com/docs/webhooks) are used to sync from Stripe to Payload. Webhooks listen for events on your Stripe account so you can trigger reactions to them. Follow the steps below to enable webhooks.
Development:
1. Login using Stripe cli `stripe login`
1. Forward events to localhost `stripe listen --forward-to localhost:3000/stripe/webhooks`
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
Production:
1. Login and [create a new webhook](https://dashboard.stripe.com/test/webhooks/create) from the Stripe dashboard
1. Paste `YOUR_DOMAIN_NAME/api/stripe/webhooks` as the "Webhook Endpoint URL"
1. Select which events to broadcast
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
1. Then, handle these events using the `webhooks` portion of this plugin's config:
```js
import { buildConfig } from 'payload/config'
import stripePlugin from '@payloadcms/plugin-stripe'
const config = buildConfig({
plugins: [
stripePlugin({
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET,
webhooks: {
'customer.subscription.updated': ({ event, stripe, stripeConfig }) => {
// do something...
},
},
// NOTE: you can also catch all Stripe webhook events and handle the event types yourself
// webhooks: (event, stripe, stripeConfig) => {
// switch (event.type): {
// case 'customer.subscription.updated': {
// // do something...
// break;
// }
// default: {
// break;
// }
// }
// }
}),
],
})
export default config
```
For a full list of available webhooks, see [here](https://stripe.com/docs/cli/trigger#trigger-event).
### Node
On the server you should interface with Stripe directly using the [stripe](https://www.npmjs.com/package/stripe) npm module. That might look something like this:
```js
import Stripe from 'stripe'
const stripeSecretKey = process.env.STRIPE_SECRET_KEY
const stripe = new Stripe(stripeSecretKey, { apiVersion: '2022-08-01' })
export const MyFunction = async () => {
try {
const customer = await stripe.customers.create({
email: data.email,
})
// do something...
} catch (error) {
console.error(error.message)
}
}
```
Alternatively, you can interface with the Stripe using the `stripeProxy`, which is exactly what the `/api/stripe/rest` endpoint does behind-the-scenes. Here's the same example as above, but piped through the proxy:
```js
import { stripeProxy } from '@payloadcms/plugin-stripe'
export const MyFunction = async () => {
try {
const customer = await stripeProxy({
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
stripeMethod: 'customers.create',
stripeArgs: [
{
email: data.email,
},
],
})
if (customer.status === 200) {
// do something...
}
if (customer.status >= 400) {
throw new Error(customer.message)
}
} catch (error) {
console.error(error.message)
}
}
```
## TypeScript
All types can be directly imported:
```js
import {
StripeConfig,
StripeWebhookHandler,
StripeProxy,
...
} from '@payloadcms/plugin-stripe/types';
```
### Development
For development purposes, there is a full working example of how this plugin might be used in the [demo](./demo) of this repo. This demo can be developed locally using any Stripe account, you just need a working API key. Then:
```bash
git clone git@github.com:payloadcms/plugin-stripe.git \
cd plugin-stripe && yarn \
cd demo && yarn \
cp .env.example .env \
vim .env \ # add your Stripe creds to this file
yarn dev
```
Now you have a running Payload server with this plugin installed, so you can authenticate and begin hitting the routes. To do this, open [Postman](https://www.postman.com/) and import [our config](https://github.com/payloadcms/plugin-stripe/blob/main/src/payload-stripe-plugin.postman_collection.json). First, login to retrieve your Payload access token. This token is automatically attached to the header of all other requests.
## Screenshots
<!-- ![screenshot 1](https://github.com/@payloadcms/plugin-stripe/blob/main/images/screenshot-1.jpg?raw=true) -->

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