Compare commits

..

155 Commits

Author SHA1 Message Date
Elliot DeNolf
736e7b822e chore(release): v3.0.0-beta.2 [skip ci] 2024-04-10 17:32:32 -04:00
Elliot DeNolf
2ebda95036 fix(next): proper named export of withPayload 2024-04-10 17:30:47 -04:00
Elliot DeNolf
d8783eaad4 chore(release): v3.0.0-beta.1 [skip ci] 2024-04-10 17:16:20 -04:00
Elliot DeNolf
cddb08de1a chore: ignore payload/i18n 2024-04-10 17:11:20 -04:00
Elliot DeNolf
03a110a750 feat(next)!: cjs support (#5772) 2024-04-10 16:44:20 -04:00
Elliot DeNolf
6accc705be chore(next): cjs build 2024-04-10 16:32:36 -04:00
Elliot DeNolf
312dca003b feat(cpa): CJS next config AST parsing 2024-04-10 16:32:19 -04:00
Jarrod Flesch
94af06466b chore: re-exports languages in payload (#5771) 2024-04-10 15:55:01 -04:00
Jacob Fletcher
7cf2686097 fix: optionally types req in auth operation (#5769) 2024-04-10 14:08:47 -04:00
Elliot DeNolf
f14883aa11 chore: update blank 3.0 template for withPayload change 2024-04-10 13:41:11 -04:00
Elliot DeNolf
9df8de2386 chore: adjust exports 2024-04-10 13:41:11 -04:00
James
8b2cf4705e chore: withPayload CJS 2024-04-10 13:41:10 -04:00
Jarrod Flesch
364e9832ac fix(next): ensures requested lang header is supported (#5765) 2024-04-10 13:05:56 -04:00
Jarrod Flesch
2deeb61f17 fix: locale switcher flakey test (#5761) 2024-04-10 13:05:30 -04:00
Patrik
14498e8a9c fix(ui): avoids getting and setting doc preferences when creating new (#5758) 2024-04-10 11:42:03 -04:00
Alessio Gravili
eb78022387 fix: undo changing baseBlockFields type to FieldWithRichTextRequiredEditor (#5767) 2024-04-10 11:38:27 -04:00
Elliot DeNolf
3677a59a78 fix(cpa): dependency tag (#5768) 2024-04-10 11:25:54 -04:00
Alessio Gravili
7c1c840a59 chore: increase admin e2e beforeAll timeout, as prebuild sometimes takes longer than the timeout 2024-04-10 11:22:17 -04:00
Alessio Gravili
a73eaf5d37 chore: fix test suite types, add LexicalBlock type 2024-04-10 11:07:01 -04:00
Alessio Gravili
68989a58a8 fix: undo changing baseBlockFields types to FieldWithRichTextRequiredEditor 2024-04-10 10:56:07 -04:00
Alessio Gravili
9841731ae7 feat: properly type withPayload (#5756) 2024-04-10 09:51:46 -04:00
Elliot DeNolf
ba7ac5d439 test: fix unit tests (#5760) 2024-04-09 23:08:20 -04:00
Elliot DeNolf
7c60772b26 ci: alpha -> beta branch push list 2024-04-09 23:07:09 -04:00
Alessio Gravili
1141a5d3af fix(richtext-lexical): do not render uploads extra fields drawer if no extra fields are provided (#5755) 2024-04-09 16:30:43 -04:00
Elliot DeNolf
6f74fd1f98 chore(release): v3.0.0-beta.0 [skip ci] 2024-04-09 15:00:39 -04:00
Alessio Gravili
75873bfcfa fix(richtext-lexical): catch errors that may occur during HTML generation (#5752) 2024-04-09 14:53:17 -04:00
Jarrod Flesch
1faf621f17 fix: persist locale when navigating (#5753) 2024-04-09 14:49:26 -04:00
Elliot DeNolf
1d1c73dfcc chore(release): v3.0.0-alpha.61 [skip ci] 2024-04-09 14:36:04 -04:00
Patrik
d057ce0a85 fix(next): removes global slug from collectionSlug prop in SetStepNav (#5744) 2024-04-09 14:26:09 -04:00
James Mikrut
0ce26d2c08 Feat/config i18n (#5735) 2024-04-09 14:13:17 -04:00
Alessio Gravili
abf285d713 chore: get dev:generate-types to work again (#5750) 2024-04-09 14:12:23 -04:00
James
c2ee8e3999 chore: de-flakes fields/index tests 2024-04-09 13:56:32 -04:00
James
167ba0c68f chore: adds back all tests 2024-04-09 13:50:54 -04:00
James
9ad1cbe920 chore: adjusts test snapshot logic to restore uploads properly 2024-04-09 13:33:42 -04:00
James
313ea52e3d chore: getRequestLanguage now defaults 2024-04-09 13:17:43 -04:00
James
3acfb7a83f chore: corrects imports 2024-04-09 12:43:36 -04:00
James
783dae2bbb Merge branch 'alpha' of github.com:payloadcms/payload into feat/config-i18n 2024-04-09 12:35:57 -04:00
Alessio Gravili
59681b211b fix(richtext-lexical): upload nodes weren't visible (#5746) 2024-04-09 12:32:24 -04:00
James
98438175cf chore: handles server errors 2024-04-09 12:31:25 -04:00
Alessio Gravili
af40302e5f fix(richtext-lexical): get links to work again (#5745) 2024-04-09 12:29:45 -04:00
Alessio Gravili
ec0e0ae449 chore(richtext-lexical): add e2e test to ensure that pre-seeded upload nodes are visible 2024-04-09 12:24:54 -04:00
Alessio Gravili
607ff17033 fix(richtext-lexical): upload nodes weren't visible due to incorrect relationships condition 2024-04-09 12:24:30 -04:00
Alessio Gravili
e73e610669 chore: fields test suite: clearAndSeedEverything instead of seed for dev as well, to ensure same state as test runs (most importantly, this gets rid of leftover uploads) 2024-04-09 12:22:42 -04:00
Jarrod Flesch
30fddde066 chore: corrects type for getLocalI18n 2024-04-09 11:51:40 -04:00
Jarrod Flesch
a56d2842fb chore: converts ua to uk 2024-04-09 11:44:34 -04:00
Jarrod Flesch
35f59a47cc chore: corrects dateFNS keys, stricter types 2024-04-09 11:38:38 -04:00
Jarrod Flesch
817d57bd12 chore: migrate langs 2024-04-09 11:05:35 -04:00
Elliot DeNolf
5826048e7b ci: publish script throttling 2024-04-09 09:50:23 -04:00
Elliot DeNolf
1a975b31cf chore(release): v3.0.0-alpha.60 [skip ci] 2024-04-09 09:36:41 -04:00
James
73298a80f0 chore: adds more de-flake to tabs 2024-04-09 09:34:11 -04:00
James
a5d14ef4c1 chore: de-flakes tabs 2024-04-09 09:34:11 -04:00
James
d0c79b65f8 chore: skips index to see what remains 2024-04-09 09:34:11 -04:00
James
2fc50b1a1f chore: de-flakes array 2024-04-09 09:34:11 -04:00
James
0ddeedb0b3 chore: de-flakes relationship suite 2024-04-09 09:34:11 -04:00
James
09c2fb10f3 chore: bug in ci 2024-04-09 09:34:11 -04:00
James
5bfff5b7ba chore: ensures artifacts work 2024-04-09 09:34:11 -04:00
James
702088375c chore: attempts to de-flake 2024-04-09 09:34:11 -04:00
James
ea507fbcc4 chore: moves lexical tests into collection folder 2024-04-09 09:34:11 -04:00
James
0ff1e6632b chore: splits out relationship 2024-04-09 09:34:11 -04:00
James
996ee47f96 chore: splits out blocks and array into their own suites 2024-04-09 09:34:11 -04:00
James
3e9bd5bb62 chore: uses ci env var to set max retries 2024-04-09 09:34:11 -04:00
James
ee7221c986 chore: sets maxRetries 2024-04-09 09:34:11 -04:00
James
318c126ae3 chore: turns off prebuild for fields 2024-04-09 09:34:11 -04:00
Jarrod Flesch
2154aea89f chore: comment out all other test suites for now 2024-04-09 09:34:11 -04:00
Jarrod Flesch
4d3ad1af35 chore: run fields e2e on ci 2024-04-09 09:34:11 -04:00
Alessio Gravili
75cab7688f chore: fix incorrect next tsconfig paths breaking monorepo setup (#5743) 2024-04-09 09:26:09 -04:00
James
5084d6dd97 chore: attempts to de-flake live preview 2024-04-08 22:34:23 -04:00
James
518f80cbb6 chore: es 2024-04-08 22:29:41 -04:00
James
c9399efa65 chore: fixes access control test 2024-04-08 22:25:35 -04:00
Elliot DeNolf
dd9133659c ci: rework caching, consolidates build (#5737) 2024-04-08 22:19:37 -04:00
James
d3016b7eb5 chore: merge 2024-04-08 22:13:16 -04:00
James
69e884f5b7 chore: dynamically loads date-fns locales 2024-04-08 22:12:05 -04:00
Elliot DeNolf
6d122905f4 chore: ignore new pointer files 2024-04-08 21:19:03 -04:00
Elliot DeNolf
e6e016ac2d chore(cpa): build for es6 (#5736) 2024-04-08 17:11:03 -04:00
James
3e7925e33f chore: removes async nature from a few lexical things 2024-04-08 16:48:56 -04:00
James
7a2ccba63c Merge branch 'feat/config-i18n' of github.com:payloadcms/payload into feat/config-i18n 2024-04-08 16:39:03 -04:00
James
be2134eb69 chore: requires languages to be passed to config 2024-04-08 16:38:12 -04:00
James Mikrut
95e422b0e1 Merge branch 'alpha' into feat/config-i18n 2024-04-08 16:26:38 -04:00
James
53d9c4ca95 chore: dynamically loads translations 2024-04-08 16:25:24 -04:00
James
30948ab545 chore: dynamically loads translations 2024-04-08 16:25:21 -04:00
Jacob Fletcher
906df6b401 fix(next): properly renders document-level unauthorized view (#5734) 2024-04-08 15:40:46 -04:00
Jacob Fletcher
b9c585bab5 fix(ui): uses correct save draft button label (#5730) 2024-04-08 14:34:28 -04:00
Elliot DeNolf
12203140ad chore: unprettified pointer files 2024-04-08 13:59:03 -04:00
Patrik
0704152e38 chore(next): removes unnecessary apostrophe from payload-lng cookie (#5729) 2024-04-08 13:42:44 -04:00
Elliot DeNolf
f582efe98d chore(release): v3.0.0-alpha.59 [skip ci] 2024-04-08 13:32:21 -04:00
James Mikrut
540579f520 chore: corrects invalid export (#5728) 2024-04-08 13:26:37 -04:00
James
24c348dc49 chore: corrects invalid export 2024-04-08 13:26:04 -04:00
Jacob Fletcher
4b4c245507 fix(ui): properly initializes collapsible context (#5725) 2024-04-08 12:48:44 -04:00
Dan Ribbens
400f68d1aa chore: add fetch to dev.js to trigger admin (#5724) 2024-04-08 12:26:33 -04:00
Jacob Fletcher
5bb27ed9cd fix(ui): renders searchable fields in list controls (#5723) 2024-04-08 11:49:02 -04:00
Jacob Fletcher
833498c269 chore(deps): regenerates frozen lockfile 2024-04-08 11:30:12 -04:00
Jacob Fletcher
80507d487b Merge branch 'alpha' into fix/list-searchable-fields 2024-04-08 11:23:24 -04:00
Jacob Fletcher
08f4ebaaf8 fix(ui): adds css specificity to select all 2024-04-08 11:22:05 -04:00
Jacob Fletcher
4c418525eb fix(ui): renders searchable fields in list controls 2024-04-08 11:01:47 -04:00
Elliot DeNolf
4962f6c926 chore(release): v3.0.0-alpha.58 [skip ci] 2024-04-08 10:52:04 -04:00
James Mikrut
50f0e9298c chore: adds upload export back (#5722) 2024-04-08 10:47:10 -04:00
James
89efcc5db1 chore: adds upload export back 2024-04-08 10:46:41 -04:00
Elliot DeNolf
c3119a5632 chore(release): v3.0.0-alpha.57 [skip ci] 2024-04-08 10:29:26 -04:00
James Mikrut
069bbd92b0 Feat/bulletproof loader (#5721) 2024-04-08 10:18:38 -04:00
James
c4422a2593 chore: improves logic of loader 2024-04-08 10:17:18 -04:00
James
3aab9d368e chore: working loader 2024-04-08 10:07:55 -04:00
Elliot DeNolf
b6afab63b2 chore(cpa): remove test from prepublishOnly 2024-04-08 09:34:52 -04:00
Alessio Gravili
b93f5e9c44 feat!: pass in req to access.admin, to match 2.0 behavior, and strongly type it (#5712) 2024-04-07 22:22:48 -04:00
James
630082035f chore: sets up test environment for loader 2024-04-07 19:48:42 -04:00
Elliot DeNolf
c74e41fc76 chore(release): v3.0.0-alpha.56 [skip ci] 2024-04-07 13:53:49 -04:00
Alessio Gravili
08ce7c58b5 chore: upgrade playwright & TS, and hide deprecation warnings (#5708) 2024-04-06 17:15:02 -04:00
Alessio Gravili
1853fde379 chore: upgrade typescript and "@types/"-prefixed packages 2024-04-06 15:35:09 -04:00
Alessio Gravili
f29d22ca95 chore: ensure node deprecation warnings stay hidden during e2e test runs 2024-04-06 15:06:04 -04:00
Alessio Gravili
5ea5f928ab chore: upgrade playwright from 1.42.1 to 1.43.0 and update patches 2024-04-06 15:05:27 -04:00
James Mikrut
efd6d35eb3 Fix/live preview flake (#5707) 2024-04-06 14:31:19 -04:00
James
2b2538f13a chore: removes old bin 2024-04-06 14:31:02 -04:00
James
9375dae179 Merge branch 'alpha' of github.com:payloadcms/payload into fix/live-preview-flake 2024-04-06 14:22:50 -04:00
James
674bb3758d chore: reduces parallel creates in live preview seed 2024-04-06 14:22:43 -04:00
James Mikrut
cedf9a2eb8 chore: pre-builds in CI (#5690) 2024-04-06 14:10:25 -04:00
James
7d2dc5b6c6 chore: adds more tests to loader int suite 2024-04-06 14:07:17 -04:00
James
9d2aad7bf9 chore: lockfile 2024-04-06 14:01:16 -04:00
James
f085d7609b chore: cleanup 2024-04-06 13:59:29 -04:00
James
a49243a42a chore: cleanup 2024-04-06 13:58:05 -04:00
James
e79f431f14 chore: cleans up tsconfigs 2024-04-06 13:40:30 -04:00
James
38e5b6e8e3 chore: temp disables fields 2024-04-06 13:38:14 -04:00
James
35bdb785c4 Merge branch 'chore/pre-build-e2e' of github.com:payloadcms/payload into chore/pre-build-e2e 2024-04-06 13:35:35 -04:00
James
25cb146fde feat: moduleResolution: bundler in config loaders 2024-04-06 13:35:09 -04:00
Jarrod Flesch
c10f0f4a9e chore: fix sort header route replace 2024-04-05 22:34:04 -04:00
Elliot DeNolf
3a3a7f6e16 ci(scripts): remove p-map 2024-04-05 17:48:21 -04:00
Elliot DeNolf
1d3b500962 chore(release): v3.0.0-alpha.55 [skip ci] 2024-04-05 17:40:07 -04:00
Dan Ribbens
f39f95af6d feat: custom db naming for postgres and mongodb (#5697) 2024-04-05 17:09:08 -04:00
Dan Ribbens
6fec2bbe1c chore: refactor auth from next into new payload local operation (#5641) 2024-04-05 17:08:51 -04:00
Elliot DeNolf
2412134073 chore: importConfig and importWithoutClientFiles (#5701) 2024-04-05 16:49:20 -04:00
Dan Ribbens
136545d1fd feat: move disableDuplicate out of admin and update APIs (#5620) 2024-04-05 16:44:41 -04:00
Jarrod Flesch
684c4d2113 chore: fix admin sort flake, fix console warning for test package.json 2024-04-05 16:39:13 -04:00
Elliot DeNolf
6624fd0401 ci: single job per branch (#5702) 2024-04-05 16:24:53 -04:00
James
31502d2da3 chore: attempts to de-flake admin 2024-04-05 16:12:49 -04:00
James
3ee39ecca3 chore: increases timeout for fields prebuild 2024-04-05 16:06:00 -04:00
Paul
89e7c305e7 chore: emails e2e suite (#5698)
fix: move 'email' configuration to server only
2024-04-05 16:50:47 -03:00
James
60dd71c59e chore: sets longer timeout for prebuild in admin and fields 2024-04-05 15:49:12 -04:00
James
0936f77930 Merge branch 'alpha' of github.com:payloadcms/payload into chore/pre-build-e2e 2024-04-05 15:25:41 -04:00
James
3c09b95a8c chore: builds in child process for e2e 2024-04-05 15:23:07 -04:00
Jacob Fletcher
780f26f135 fix(next): incorrect access of select options in versions view (#5687) 2024-04-05 15:03:49 -04:00
Jacob Fletcher
77618674a0 fix(next): live preview url (#5692) 2024-04-05 15:00:58 -04:00
Alessio Gravili
c9c89a6005 fix(richtext-lexical): do not allow omitting editor prop for sub-richtext fields within lexical defined in the payload config (#5699) 2024-04-05 14:42:15 -04:00
Jacob Fletcher
d1276c4299 feat(next): threads payload through live preview url 2024-04-05 14:36:14 -04:00
Jacob Fletcher
69730b6c95 Merge branch 'alpha' into fix/lp-url 2024-04-05 14:25:17 -04:00
James
9147d30152 Merge branch 'chore/green-ci' of github.com:payloadcms/payload into chore/pre-build-e2e 2024-04-05 14:13:28 -04:00
James
6217c70fb5 chore: prebuilds fields and admin in ci 2024-04-05 14:13:14 -04:00
Jarrod Flesch
17352c9a56 chore: change pw buttons, field readOnly prop, admin panel thumbnailURL fallback (#5694) 2024-04-05 13:51:17 -04:00
Jacob Fletcher
1b6026304f fix(next): adjusts args sent through live preview url 2024-04-05 13:37:12 -04:00
Jacob Fletcher
91f9973c6c fix(next): properly types locale in live preview url 2024-04-05 13:37:07 -04:00
Elliot DeNolf
ca2acee38a chore(release): create-payload-app/v3.0.0-alpha.54 [skip ci] 2024-04-05 11:53:19 -04:00
James
3bd455ced2 chore: pre-builds in CI 2024-04-05 11:46:02 -04:00
Patrik
2c25abd143 test(versions): replaces outdated CustomPublishButtonProps type (#5688) 2024-04-05 11:11:16 -04:00
PatrikKozak
08a9fb8cd7 Merge branch 'alpha' of https://github.com/payloadcms/payload into fix/alpha/versions-select-options 2024-04-05 10:38:23 -04:00
PatrikKozak
d8a38b4e35 fix: incorrect access of select options in versions 2024-04-05 10:38:09 -04:00
Paul
e64660f2d8 fix(db-postgres): querying by localised relations postgres (#5686) 2024-04-05 10:37:58 -04:00
James
0c45d5773a chore: brings back admin and fields 2024-04-04 21:02:31 -04:00
James
f48335444b chore: flake 2024-04-04 21:01:38 -04:00
588 changed files with 20212 additions and 29688 deletions

View File

@@ -4,7 +4,11 @@ on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches: ['main', 'alpha']
branches: ['main', 'beta']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
@@ -35,7 +39,7 @@ jobs:
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
echo "templates: ${{ steps.filter.outputs.templates }}"
core-build:
build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
@@ -61,66 +65,29 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
- name: Setup pnpm cache
uses: actions/cache@v4
timeout-minutes: 720
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- run: pnpm run build:core
- run: pnpm run build:all
- name: Cache build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
plugins-build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- run: pnpm run build:plugins
tests-unit:
runs-on: ubuntu-latest
needs: core-build
if: false # Disable until tests are updated for 3.0
needs: build
steps:
- name: Use Node.js 18
@@ -136,6 +103,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -147,7 +115,7 @@ jobs:
tests-int:
runs-on: ubuntu-latest
needs: core-build
needs: build
strategy:
fail-fast: false
matrix:
@@ -180,6 +148,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -238,7 +207,7 @@ jobs:
tests-e2e:
runs-on: ubuntu-latest
needs: core-build
needs: build
strategy:
fail-fast: false
matrix:
@@ -246,12 +215,16 @@ jobs:
suite:
- _community
- access-control
# - admin
- admin
- auth
- email
- field-error-states
- fields-relationship
# - fields
- fields/lexical
- fields
- fields__collections__Blocks
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__Lexical
- live-preview
- localization
- plugin-form-builder
@@ -274,6 +247,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -294,7 +268,7 @@ jobs:
tests-type-generation:
if: false # This should be replaced with gen on a real Payload project
runs-on: ubuntu-latest
needs: core-build
needs: build
steps:
- name: Use Node.js 18
@@ -310,6 +284,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}

View File

@@ -10,3 +10,5 @@
**/temp
**/docs/**
tsconfig.json
packages/payload/*.js
packages/payload/*.d.ts

11
.vscode/launch.json vendored
View File

@@ -23,6 +23,17 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/loader/init.js",
"cwd": "${workspaceFolder}",
"name": "Run Loader",
"request": "launch",
"type": "node-terminal",
"env": {
"LOADER_TEST_FILE_PATH": "./dependency-test.js"
// "LOADER_TEST_FILE_PATH": "../fields/config.ts"
}
},
{
"command": "node --no-deprecation test/dev.js admin",
"cwd": "${workspaceFolder}",

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views/NotFound/index.js'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views/Root/index.js'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {

View File

@@ -1,7 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes/index.js'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes/index.js'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts/Root/index.js'
import { RootLayout } from '@payloadcms/next/layouts'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'

View File

@@ -1,35 +0,0 @@
import QueryString from 'qs'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export const fetchDoc = async <T>(args: {
collection: string
depth?: number
id?: string
slug?: string
}): Promise<T> => {
const { id, slug, collection, depth = 2 } = args || {}
const queryString = QueryString.stringify(
{
...(slug ? { 'where[slug][equals]': slug } : {}),
...(depth ? { depth } : {}),
},
{ addQueryPrefix: true },
)
const doc: T = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}${queryString}`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')
return res?.docs?.[0]
})
return doc
}

View File

@@ -1,19 +0,0 @@
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export const fetchDocs = async <T>(collection: string): Promise<T[]> => {
const docs: T[] = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}?depth=0&limit=100`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs')
return res?.docs
})
return docs
}

View File

@@ -1,25 +0,0 @@
import type { Footer } from '../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export async function fetchFooter(): Promise<Footer> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const footer = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/footer`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching footer')
return res
})
return footer
}

View File

@@ -1,25 +0,0 @@
import type { Header } from '../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export async function fetchHeader(): Promise<Header> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const header = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/header`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching header')
return res
})
return header
}

View File

@@ -639,12 +639,12 @@ export const CustomArrayManager = () => {
The `useCollapsible` hook allows you to control parent collapsibles:
| Property | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | --- |
| **`collapsed`** | State of the collapsible. `true` if open, `false` if collapsed |
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise |
| **`toggle`** | Toggles the state of the nearest collapsible |
| **`withinCollapsible`** | Determine when you are within another collaspible | |
| Property | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------ | --- |
| **`isCollapsed`** | State of the collapsible. `true` if open, `false` if collapsed |
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise |
| **`toggle`** | Toggles the state of the nearest collapsible |
| **`isWithinCollapsible`** | Determine when you are within another collaspible | |
**Example:**
@@ -654,10 +654,11 @@ import React from 'react'
import { useCollapsible } from 'payload/components/utilities'
const CustomComponent: React.FC = () => {
const { collapsed, toggle } = useCollapsible()
const { isCollapsed, toggle } = useCollapsible()
return (
<div>
<p className="field-type">I am {collapsed ? 'closed' : 'open'}</p>
<p className="field-type">I am {isCollapsed ? 'closed' : 'open'}</p>
<button onClick={toggle} type="button">
Toggle
</button>

View File

@@ -13,23 +13,25 @@ It's often best practice to write your Collections in separate files and then im
## Options
| Option | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs. |
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined.
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
_\* An asterisk denotes that a property is required._
@@ -75,7 +77,6 @@ property on a collection's config.
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
| `enableRichTextLink` | The [Rich Text](/docs/fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](/docs/fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |

View File

@@ -26,6 +26,7 @@ As with Collection configs, it's often best practice to write your Globals in se
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name for this global depending on the database adapter. Auto-generated from slug if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -38,12 +38,18 @@ export default buildConfig({
### Options
| Option | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pool` | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| Option | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
### Access to Drizzle

View File

@@ -45,6 +45,7 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`dbName`** | Custom table name for the field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
_\* An asterisk denotes that a property is required._

View File

@@ -80,6 +80,7 @@ Blocks are defined as separate configs of their own.
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. |
| **`dbName`** | Custom table name for this block type when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined.
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
#### Auto-generated data per block

View File

@@ -20,27 +20,27 @@ keywords: number, fields, config, configuration, documentation, Content Manageme
### Config
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
_\* An asterisk denotes that a property is required._

View File

@@ -36,6 +36,7 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -38,6 +38,8 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
_\* An asterisk denotes that a property is required._

View File

@@ -10,6 +10,12 @@ const withBundleAnalyzer = bundleAnalyzer({
export default withBundleAnalyzer(
withPayload({
reactStrictMode: false,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
async redirects() {
return [
{

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-alpha.54",
"version": "3.0.0-beta.2",
"private": true,
"type": "module",
"workspaces:": [
@@ -76,7 +76,7 @@
"@octokit/core": "^5.1.0",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
"@playwright/test": "^1.42.1",
"@playwright/test": "1.43.0",
"@swc/cli": "^0.1.62",
"@swc/jest": "0.2.36",
"@testing-library/jest-dom": "6.4.2",
@@ -88,13 +88,13 @@
"@types/conventional-changelog-writer": "^4.0.10",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
"@types/minimist": "1.2.2",
"@types/node": "20.11.28",
"@types/minimist": "1.2.5",
"@types/node": "20.12.5",
"@types/prompts": "^2.4.5",
"@types/qs": "6.9.7",
"@types/react": "18.2.15",
"@types/qs": "6.9.14",
"@types/react": "18.2.74",
"@types/semver": "^7.5.3",
"@types/shelljs": "0.8.12",
"@types/shelljs": "0.8.15",
"add-stream": "^1.0.0",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -131,11 +131,11 @@
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
"p-map": "^7.0.2",
"p-limit": "^5.0.0",
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"playwright": "^1.42.1",
"playwright-core": "^1.42.1",
"playwright": "1.43.0",
"playwright-core": "1.43.0",
"prettier": "^3.0.3",
"prompts": "2.4.2",
"qs": "6.11.2",
@@ -155,7 +155,7 @@
"ts-node": "10.9.1",
"tsx": "^4.7.1",
"turbo": "^1.13.2",
"typescript": "5.4.2",
"typescript": "5.4.4",
"uuid": "^9.0.1",
"yocto-queue": "^1.0.0"
},
@@ -195,8 +195,7 @@
"domexception": "4"
},
"patchedDependencies": {
"playwright@1.42.1": "patches/playwright@1.42.1.patch"
"playwright@1.43.0": "patches/playwright@1.43.0.patch"
}
},
"packageManager": "pnpm@8.15.4+sha256.cea6d0bdf2de3a0549582da3983c70c92ffc577ff4410cbf190817ddc35137c2"
}
}

View File

@@ -10,6 +10,6 @@
}
},
"module": {
"type": "commonjs"
"type": "es6"
}
}

View File

@@ -1,2 +1,4 @@
#!/usr/bin/env node
require('../dist/index.js')
import { main } from '../dist/index.js'
main()

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-alpha.50",
"version": "3.0.0-beta.2",
"license": "MIT",
"type": "module",
"homepage": "https://payloadcms.com",
@@ -19,7 +19,7 @@
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"clean": "rimraf {dist,*.tsbuildinfo}",
"test": "jest",
"prepublishOnly": "pnpm test && pnpm clean && pnpm build"
"prepublishOnly": "pnpm clean && pnpm build"
},
"files": [
"package.json",
@@ -48,7 +48,7 @@
"@types/esprima": "^4.0.6",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^27.0.3",
"@types/node": "^16.6.2"
"@types/node": "20.12.5"
},
"exports": {
"./commands": {

View File

@@ -1,8 +1,12 @@
import { Main } from './main.js'
import { error } from './utils/log.js'
async function main(): Promise<void> {
await new Main().init()
export async function main(): Promise<void> {
try {
await new Main().init()
} catch (e) {
if (e instanceof Error) {
error(e.message)
}
}
}
main().catch((e) => error(`An error has occurred: ${e instanceof Error ? e.message : e}`))

View File

@@ -4,6 +4,7 @@ import * as p from '@clack/prompts'
import { parse, stringify } from 'comment-json'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'
import { promisify } from 'util'
@@ -31,6 +32,8 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
useDistFiles?: boolean
}
type NextConfigType = 'cjs' | 'esm'
type InitNextResult =
| {
isSrcDir: boolean
@@ -45,11 +48,22 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const nextAppDetails = args.nextAppDetails || (await getNextAppDetails(projectDir))
const { hasTopLevelLayout, isSrcDir, nextAppDir } =
nextAppDetails || (await getNextAppDetails(projectDir))
if (!nextAppDetails.nextAppDir) {
warning(`Could not find app directory in ${projectDir}, creating...`)
const createdAppDir = path.resolve(projectDir, nextAppDetails.isSrcDir ? 'src/app' : 'app')
fse.mkdirSync(createdAppDir, { recursive: true })
nextAppDetails.nextAppDir = createdAppDir
}
if (!nextAppDir) {
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
const { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigType } = nextAppDetails
if (!nextConfigType) {
return {
isSrcDir,
nextAppDir,
reason: `Could not determine Next Config type in ${projectDir}. Possibly try renaming next.config.js to next.config.cjs or next.config.mjs.`,
success: false,
}
}
if (hasTopLevelLayout) {
@@ -69,6 +83,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const configurationResult = installAndConfigurePayload({
...args,
nextAppDetails,
nextConfigType,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
})
@@ -96,6 +111,13 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean) {
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
// Check if tsconfig.json exists
if (!fs.existsSync(tsConfigPath)) {
warning(`Could not find tsconfig.json to add @payload-config path.`)
return
}
const userTsConfigContent = await readFile(tsConfigPath, {
encoding: 'utf8',
})
@@ -119,13 +141,18 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
}
function installAndConfigurePayload(
args: InitNextArgs & { nextAppDetails: NextAppDetails; useDistFiles?: boolean },
args: InitNextArgs & {
nextAppDetails: NextAppDetails
nextConfigType: NextConfigType
useDistFiles?: boolean
},
):
| { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } {
const {
'--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
nextConfigType,
projectDir,
useDistFiles,
} = args
@@ -172,6 +199,7 @@ function installAndConfigurePayload(
logDebug(`nextAppDir: ${nextAppDir}`)
logDebug(`projectDir: ${projectDir}`)
logDebug(`nextConfigPath: ${nextConfigPath}`)
logDebug(`payloadConfigPath: ${path.resolve(projectDir, 'payload.config.ts')}`)
logDebug(
`isSrcDir: ${isSrcDir}. source: ${templateSrcDir}. dest: ${path.dirname(nextConfigPath)}`,
@@ -181,7 +209,7 @@ function installAndConfigurePayload(
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
// Wrap next.config.js with withPayload
wrapNextConfig({ nextConfigPath })
wrapNextConfig({ nextConfigPath, nextConfigType })
return {
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
@@ -191,10 +219,10 @@ function installAndConfigurePayload(
async function installDeps(projectDir: string, packageManager: PackageManager, dbType: DbType) {
const packagesToInstall = ['payload', '@payloadcms/next', '@payloadcms/richtext-lexical'].map(
(pkg) => `${pkg}@alpha`,
(pkg) => `${pkg}@beta`,
)
packagesToInstall.push(`@payloadcms/db-${dbType}@alpha`)
packagesToInstall.push(`@payloadcms/db-${dbType}@beta`)
let exitCode = 0
switch (packageManager) {
@@ -226,6 +254,7 @@ type NextAppDetails = {
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
}
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
@@ -246,6 +275,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
ignore: ['**/node_modules/**'],
onlyDirectories: true,
})
)?.[0]
@@ -254,9 +284,31 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
nextAppDir = undefined
}
const configType = await getProjectType(projectDir, nextConfigPath)
const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
: false
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath }
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath, nextConfigType: configType }
}
async function getProjectType(projectDir: string, nextConfigPath: string): Promise<'cjs' | 'esm'> {
if (nextConfigPath.endsWith('.mjs')) {
return 'esm'
}
if (nextConfigPath.endsWith('.cjs')) {
return 'cjs'
}
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
const packageJsonType = packageObj.type
if (packageJsonType === 'module') {
return 'esm'
}
if (packageJsonType === 'commonjs') {
return 'cjs'
}
return 'cjs'
}

View File

@@ -1,61 +1,133 @@
import { parseAndModifyConfigContent, withPayloadImportStatement } from './wrap-next-config.js'
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
import * as p from '@clack/prompts'
const defaultNextConfig = `/** @type {import('next').NextConfig} */
const esmConfigs = {
defaultNextConfig: `/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
`
const nextConfigWithFunc = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(nextConfig)
`
const nextConfigWithFuncMultiline = `const nextConfig = {
// Your Next.js config here
}
`,
nextConfigWithFunc: `const nextConfig = {};
export default someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};;
export default someFunc(
nextConfig
)
`
const nextConfigExportNamedDefault = `const nextConfig = {
// Your Next.js config here
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
export { wrapped as default };
`,
}
const cjsConfigs = {
defaultNextConfig: `
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;
`,
anonConfig: `module.exports = {};`,
nextConfigWithFunc: `const nextConfig = {};
module.exports = someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};
module.exports = someFunc(
nextConfig
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
module.exports = wrapped;
`,
}
const wrapped = someFunc(asdf)
export { wrapped as default }
`
describe('parseAndInsertWithPayload', () => {
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(defaultNextConfig)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFunc)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
describe('esm', () => {
const configType = 'esm'
const importStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
esmConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Could not automatically wrap'),
)
})
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFuncMultiline)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
nextConfigExportNamedDefault,
)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(expect.stringContaining('Could not automatically wrap'))
describe('cjs', () => {
const configType = 'cjs'
const requireStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse anonymous default config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.anonConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload({})')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a named export as default', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(wrapped)')
})
})
})

View File

@@ -5,12 +5,23 @@ import fs from 'fs'
import { warning } from '../utils/log.js'
import { log } from '../utils/log.js'
export const withPayloadImportStatement = `import { withPayload } from '@payloadcms/next'\n`
export const withPayloadStatement = {
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`,
esm: `import { withPayload } from '@payloadcms/next/withPayload'\n`,
}
export const wrapNextConfig = (args: { nextConfigPath: string }) => {
const { nextConfigPath } = args
type NextConfigType = 'cjs' | 'esm'
export const wrapNextConfig = (args: {
nextConfigPath: string
nextConfigType: NextConfigType
}) => {
const { nextConfigPath, nextConfigType: configType } = args
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(configContent)
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(
configContent,
configType,
)
if (!success) {
return
@@ -22,72 +33,110 @@ export const wrapNextConfig = (args: { nextConfigPath: string }) => {
/**
* Parses config content with AST and wraps it with withPayload function
*/
export function parseAndModifyConfigContent(content: string): {
modifiedConfigContent: string
success: boolean
} {
content = withPayloadImportStatement + content
export function parseAndModifyConfigContent(
content: string,
configType: NextConfigType,
): { modifiedConfigContent: string; success: boolean } {
content = withPayloadStatement[configType] + content
const ast = parseModule(content, { loc: true })
const exportDefaultDeclaration = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
| Directive
| undefined
const exportNamedDeclaration = ast.body.find((p) => p.type === 'ExportNamedDeclaration') as
| ExportNamedDeclaration
| undefined
if (configType === 'esm') {
const exportDefaultDeclaration = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
| Directive
| undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
const exportNamedDeclaration = ast.body.find((p) => p.type === 'ExportNamedDeclaration') as
| ExportNamedDeclaration
| undefined
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
configType,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
} else if (configType === 'cjs') {
// Find `module.exports = X`
const moduleExports = ast.body.find(
(p) =>
p.type === 'ExpressionStatement' &&
p.expression?.type === 'AssignmentExpression' &&
p.expression.left?.type === 'MemberExpression' &&
p.expression.left.object?.type === 'Identifier' &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === 'Identifier' &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
moduleExports.expression.right.loc,
configType,
)
return { modifiedConfigContent, success: true }
}
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful()
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
function warnUserWrapNotSuccessful() {
function warnUserWrapNotSuccessful(configType: NextConfigType) {
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)}
import withPayload from '@payloadcms/next/withPayload'
${withPayloadStatement[configType]}
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)
${configType === 'esm' ? 'export default withPayload(nextConfig)' : 'module.exports = withPayload(nextConfig)'}
`
@@ -125,7 +174,7 @@ type Loc = {
start: { column: number; line: number }
}
function insertBeforeAndAfter(content: string, loc: Loc) {
function insertBeforeAndAfter(content: string, loc: Loc, configType: NextConfigType) {
const { end, start } = loc
const lines = content.split('\n')

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-alpha.54",
"version": "3.0.0-beta.2",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",
@@ -30,7 +30,6 @@
"get-port": "5.1.1",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"uuid": "9.0.0"

View File

@@ -5,11 +5,7 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import mongoose from 'mongoose'
import paginate from 'mongoose-paginate-v2'
import {
buildVersionCollectionFields,
buildVersionGlobalFields,
getVersionsModelName,
} from 'payload/versions'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import type { MongooseAdapter } from './index.js'
import type { CollectionModel } from './types.js'
@@ -18,13 +14,14 @@ import buildCollectionSchema from './models/buildCollectionSchema.js'
import { buildGlobalModel } from './models/buildGlobalModel.js'
import buildSchema from './models/buildSchema.js'
import getBuildQueryPlugin from './queries/buildQuery.js'
import { getDBName } from './utilities/getDBName.js'
export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schema = buildCollectionSchema(collection, this.payload.config)
if (collection.versions) {
const versionModelName = getVersionsModelName(collection)
const versionModelName = getDBName({ config: collection, versions: true })
const versionCollectionFields = buildVersionCollectionFields(collection)
@@ -54,7 +51,7 @@ export const init: Init = function init(this: MongooseAdapter) {
}
const model = mongoose.model(
collection.slug,
getDBName({ config: collection }),
schema,
this.autoPluralization === true ? undefined : collection.slug,
) as CollectionModel
@@ -72,7 +69,7 @@ export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.globals.forEach((global) => {
if (global.versions) {
const versionModelName = getVersionsModelName(global)
const versionModelName = getDBName({ config: global, versions: true })
const versionGlobalFields = buildVersionGlobalFields(global)

View File

@@ -363,7 +363,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
if (field.localized && config.localization) {
config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale}`]: '2dsphere' }, indexOptions)
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
})
} else {
schema.index({ [field.name]: '2dsphere' }, indexOptions)

View File

@@ -1,18 +1,20 @@
import { sanitizeConfig } from 'payload/config'
import { SanitizedConfig, sanitizeConfig } from 'payload/config'
import { Config } from 'payload/config'
import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'
const config = {
const config = sanitizeConfig({
localization: {
locales: ['en', 'es'],
defaultLocale: 'en',
fallback: true,
},
} as Config
} as Config) as SanitizedConfig
describe('get localized sort property', () => {
it('passes through a non-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -28,7 +30,7 @@ describe('get localized sort property', () => {
it('properly localizes an un-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -45,7 +47,7 @@ describe('get localized sort property', () => {
it('keeps specifically asked-for localized sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['title', 'es'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -62,7 +64,7 @@ describe('get localized sort property', () => {
it('properly localizes nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'group',
@@ -85,7 +87,7 @@ describe('get localized sort property', () => {
it('keeps requested locale with nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title', 'es'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'group',
@@ -108,7 +110,7 @@ describe('get localized sort property', () => {
it('properly localizes field within row', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'row',
@@ -130,7 +132,7 @@ describe('get localized sort property', () => {
it('properly localizes field within named tab', () => {
const result = getLocalizedSortProperty({
segments: ['tab', 'title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'tabs',
@@ -157,7 +159,7 @@ describe('get localized sort property', () => {
it('properly localizes field within unnamed tab', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'tabs',

View File

@@ -0,0 +1,41 @@
import type { DBIdentifierName } from 'payload/database'
type Args = {
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
locales?: boolean
target?: 'dbName' | 'enumName'
versions?: boolean
}
/**
* Used to name database enums and collections
* Returns the collection or enum name for a given entity
*/
export const getDBName = ({
config: { name, slug },
config,
target = 'dbName',
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({}) : custom
} else {
result = name ?? slug
}
if (versions) result = `_${result}_versions`
return result
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-alpha.54",
"version": "3.0.0-beta.2",
"description": "The officially supported Postgres database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -1,9 +1,8 @@
import type { Create } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const create: Create = async function create(
@@ -20,7 +19,10 @@ export const create: Create = async function create(
fields: collection.fields,
operation: 'create',
req,
tableName: toSnakeCase(collectionSlug),
tableName: getTableName({
adapter: this,
config: collection,
}),
})
return result

View File

@@ -1,10 +1,9 @@
import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobal<T extends TypeWithID>(
@@ -21,7 +20,10 @@ export async function createGlobal<T extends TypeWithID>(
fields: globalConfig.fields,
operation: 'create',
req,
tableName: toSnakeCase(slug),
tableName: getTableName({
adapter: this,
config: globalConfig,
}),
})
return result

View File

@@ -4,10 +4,10 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { type CreateGlobalVersionArgs } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>(
@@ -16,8 +16,11 @@ export async function createGlobalVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const globalTableName = toSnakeCase(globalSlug)
const tableName = `_${globalTableName}_v`
const tableName = getTableName({
adapter: this,
config: global,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,

View File

@@ -3,10 +3,10 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createVersion<T extends TypeWithID>(
@@ -21,8 +21,11 @@ export async function createVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const collectionTableName = toSnakeCase(collectionSlug)
const tableName = `_${collectionTableName}_v`
const tableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,
@@ -40,7 +43,15 @@ export async function createVersion<T extends TypeWithID>(
})
const table = this.tables[tableName]
const relationshipsTable = this.tables[`${tableName}_rels`]
const relationshipsTable =
this.tables[
getTableName({
adapter: this,
config: collection,
relationships: true,
versions: true,
})
]
if (collection.versions.drafts) {
await db.execute(sql`

View File

@@ -2,11 +2,11 @@ import type { DeleteMany } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter,
@@ -14,7 +14,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = toSnakeCase(collection)
const tableName = getTableName({ adapter: this, config: collectionConfig })
const result = await findMany({
adapter: this,

View File

@@ -2,13 +2,13 @@ import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne(
@@ -17,7 +17,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
let docToDelete: Record<string, unknown>
const { joinAliases, joins, selectFields, where } = await buildQuery({

View File

@@ -3,11 +3,11 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
@@ -16,7 +16,11 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({

View File

@@ -1,38 +1,41 @@
import type { Find } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const find: Find = async function find(
this: PostgresAdapter,
{
collection,
limit: limitArg,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
sort: sortArg,
where: whereArg,
where,
},
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
return findMany({
adapter: this,
fields: collectionConfig.fields,
limit: limitArg,
limit,
locale,
page,
pagination,
req,
sort,
tableName: toSnakeCase(collection),
where: whereArg,
tableName,
where,
})
}

View File

@@ -2,11 +2,12 @@
import type { Field } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js'
import { getTableName } from '../schema/getTableName.js'
type TraverseFieldArgs = {
_locales: Record<string, unknown>
adapter: PostgresAdapter
@@ -78,9 +79,22 @@ export const traverseFields = ({
with: {},
}
const arrayTableName = `${currentTableName}_${path}${toSnakeCase(field.name)}`
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales
const arrayTableNameWithLocales = getTableName({
adapter,
config: field,
locales: true,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[arrayTableNameWithLocales]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray
traverseFields({
@@ -128,9 +142,16 @@ export const traverseFields = ({
with: {},
}
const tableName = `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`
const tableName = getTableName({
adapter,
config: block,
parentTableName: topLevelTableName,
prefix: `${topLevelTableName}_blocks_`,
})
if (adapter.tables[`${tableName}_locales`]) withBlock.with._locales = _locales
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
withBlock.with._locales = _locales
}
topLevelArgs.with[blockKey] = withBlock
traverseFields({

View File

@@ -1,17 +1,19 @@
import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter,
{ slug, locale, req, where },
) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = toSnakeCase(slug)
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const {
docs: [doc],

View File

@@ -2,11 +2,11 @@ import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
@@ -27,7 +27,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
)
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const tableName = `_${toSnakeCase(global)}_v`
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
return findMany({

View File

@@ -1,17 +1,20 @@
import type { FindOneArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export async function findOne<T extends TypeWithID>(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where: incomingWhere }: FindOneArgs,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
const { docs } = await findMany({
adapter: this,
@@ -22,8 +25,8 @@ export async function findOne<T extends TypeWithID>(
pagination: false,
req,
sort: undefined,
tableName: toSnakeCase(collection),
where: incomingWhere,
tableName,
where,
})
return docs?.[0] || null

View File

@@ -2,11 +2,11 @@ import type { FindVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -25,7 +25,11 @@ export const findVersions: FindVersions = async function findVersions(
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
return findMany({

View File

@@ -1,6 +1,5 @@
import type { Payload } from 'payload'
import type { DatabaseAdapterObj } from 'payload/database'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
import fs from 'fs'
import path from 'path'
@@ -39,6 +38,8 @@ import { updateGlobal } from './updateGlobal.js'
import { updateGlobalVersion } from './updateGlobalVersion.js'
import { updateVersion } from './updateVersion.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
@@ -50,20 +51,24 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
name: 'postgres',
// Postgres-specific
blockTableNames: {},
drizzle: undefined,
enums: {},
fieldConstraints: {},
idType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
pgSchema: undefined,
pool: undefined,
poolOptions: args.pool,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',
schema: {},
schemaName: args.schemaName,
sessions: {},
tables: {},
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter
beginTransaction,

View File

@@ -4,11 +4,11 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildTable } from './schema/build.js'
import { getTableName } from './schema/getTableName.js'
export const init: Init = function init(this: PostgresAdapter) {
if (this.schemaName) {
@@ -25,7 +25,10 @@ export const init: Init = function init(this: PostgresAdapter) {
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = toSnakeCase(collection.slug)
const tableName = getTableName({
adapter: this,
config: collection,
})
buildTable({
adapter: this,
@@ -37,10 +40,15 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: collection.fields,
tableName,
timestamps: collection.timestamps,
versions: false,
})
if (collection.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const versionFields = buildVersionCollectionFields(collection)
buildTable({
@@ -53,12 +61,13 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})
this.payload.config.globals.forEach((global) => {
const tableName = toSnakeCase(global.slug)
const tableName = getTableName({ adapter: this, config: global })
buildTable({
adapter: this,
@@ -70,10 +79,11 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: global.fields,
tableName,
timestamps: false,
versions: false,
})
if (global.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({ adapter: this, config: global, versions: true })
const versionFields = buildVersionGlobalFields(global)
buildTable({
@@ -86,6 +96,7 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})

View File

@@ -92,7 +92,7 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
payload.logger.info({ msg: `Migrating: ${migration.name}` })
const pgAdapter = payload.db as PostgresAdapter
const pgAdapter = payload.db
const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
try {

View File

@@ -14,6 +14,8 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery.js'
import { getTableName } from '../schema/getTableName.js'
type Constraint = {
columnName: string
table: GenericTable | PgTableWithColumns<any>
@@ -183,7 +185,13 @@ export const getTableColumnFromPath = ({
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}_locales`
newTableName = getTableName({
adapter,
config: field,
locales: true,
parentTableName: tableName,
prefix: `${tableName}_`,
})
joins[tableName] = eq(
adapter.tables[tableName].id,
@@ -218,7 +226,12 @@ export const getTableColumnFromPath = ({
}
case 'array': {
newTableName = `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -265,7 +278,12 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => {
const block = field.blocks.find((block) => block.slug === blockType)
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}`
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID,
@@ -285,7 +303,12 @@ export const getTableColumnFromPath = ({
}
const hasBlockField = field.blocks.some((block) => {
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}`
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
constraintPath = `${constraintPath}${field.name}.%.`
let result
const blockConstraints = []
@@ -351,7 +374,7 @@ export const getTableColumnFromPath = ({
case 'relationship':
case 'upload': {
let relationshipFields
const relationTableName = `${rootTableName}_rels`
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
const newCollectionPath = pathSegments.slice(1).join('.')
const aliasRelationshipTableName = uuid()
const aliasRelationshipTable = alias(
@@ -360,22 +383,45 @@ export const getTableColumnFromPath = ({
)
// Join in the relationships table
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
if (locale && field.localized && adapter.payload.config.localization) {
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
eq(aliasRelationshipTable.locale, locale),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
if (locale !== 'all') {
constraints.push({
columnName: 'locale',
table: aliasRelationshipTable,
value: locale,
})
}
} else {
// Join in the relationships table
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
}
selectFields[`${relationTableName}.path`] = aliasRelationshipTable.path
let newAliasTable
if (typeof field.relationTo === 'string') {
newTableName = `${toSnakeCase(field.relationTo)}`
const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = getTableName({
adapter,
config: relationshipConfig,
})
// parent to relationship join table
relationshipFields = adapter.payload.collections[field.relationTo].config.fields
relationshipFields = relationshipConfig.fields
newAliasTable = alias(adapter.tables[newTableName], toSnakeCase(uuid()))
@@ -394,7 +440,11 @@ export const getTableColumnFromPath = ({
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map(
(relationTo) => `"${aliasRelationshipTableName}"."${toSnakeCase(relationTo)}_id"`,
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
)
return {
constraints,
@@ -441,7 +491,7 @@ export const getTableColumnFromPath = ({
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}_locales`
newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName]

View File

@@ -2,9 +2,9 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { type QueryDrafts, combineQueries } from 'payload/database'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
@@ -17,7 +17,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts({
where,
}) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -11,10 +11,10 @@ import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
import { getTableName } from './getTableName.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js'
@@ -35,6 +35,7 @@ type Args = {
rootTableName?: string
tableName: string
timestamps?: boolean
versions: boolean
}
type Result = {
@@ -59,6 +60,7 @@ export const buildTable = ({
rootTableName: incomingRootTableName,
tableName,
timestamps,
versions,
}: Args): Result => {
const rootTableName = incomingRootTableName || tableName
const columns: Record<string, PgColumnBuilder> = baseColumns
@@ -113,6 +115,7 @@ export const buildTable = ({
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
}))
if (timestamps) {
@@ -147,7 +150,7 @@ export const buildTable = ({
adapter.tables[tableName] = table
if (hasLocalizedField) {
const localeTableName = `${tableName}_locales`
const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
@@ -288,11 +291,16 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const formattedRelationTo = toSnakeCase(relationTo)
const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = getTableName({
adapter,
config: relationshipConfig,
throwValidationError: true,
})
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
const relatedCollectionCustomID = adapter.payload.collections[
relationTo
].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id')
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
@@ -301,7 +309,7 @@ export const buildTable = ({
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
const relationshipsTableName = `${tableName}_rels`
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
@@ -333,7 +341,11 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = toSnakeCase(relationTo)
const relatedTableName = getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,
})
const idColumnName = `${relationTo}ID`
result[idColumnName] = one(adapter.tables[relatedTableName], {
fields: [relationshipsTable[idColumnName]],

View File

@@ -0,0 +1,75 @@
import type { DBIdentifierName } from 'payload/database'
import { APIError } from 'payload/errors'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js'
type Args = {
adapter: PostgresAdapter
/** The collection, global or field config **/
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
/** Localized tables need to be given the locales suffix */
locales?: boolean
/** For nested tables passed for the user custom dbName functions to handle their own iterations */
parentTableName?: string
/** For sub tables (array for example) this needs to include the parentTableName */
prefix?: string
/** Adds the relationships suffix */
relationships?: boolean
/** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */
target?: 'dbName' | 'enumName'
throwValidationError?: boolean
/** Adds the versions suffix, should only be used on the base collection to duplicate suffixing */
versions?: boolean
}
/**
* Used to name database enums and tables
* Returns the table or enum name for a given entity
*/
export const getTableName = ({
adapter,
config: { name, slug },
config,
locales = false,
parentTableName,
prefix = '',
relationships = false,
target = 'dbName',
throwValidationError = false,
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({ tableName: parentTableName }) : custom
} else {
result = `${prefix}${toSnakeCase(name ?? slug)}`
}
if (locales) result = `${result}${adapter.localesSuffix}`
if (versions) result = `_${result}${adapter.versionsSuffix}`
if (relationships) result = `${result}${adapter.relationshipsSuffix}`
if (!throwValidationError) {
return result
}
if (result.length > 63) {
throw new APIError(
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
)
}
return result
}

View File

@@ -27,6 +27,7 @@ import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { getTableName } from './getTableName.js'
import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical.js'
@@ -53,6 +54,7 @@ type Args = {
rootRelationsToBuild?: Map<string, string>
rootTableIDColType: string
rootTableName: string
versions: boolean
}
type Result = {
@@ -86,7 +88,9 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
}: Args): Result => {
const throwValidationError = true
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
@@ -217,7 +221,15 @@ export const traverseFields = ({
case 'radio':
case 'select': {
const enumName = `enum_${newTableName}_${toSnakeCase(field.name)}`
const enumName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
enumName,
@@ -231,7 +243,14 @@ export const traverseFields = ({
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = `${newTableName}_${toSnakeCase(field.name)}`
const selectTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
@@ -266,6 +285,7 @@ export const traverseFields = ({
disableUnique,
fields: [],
tableName: selectTableName,
versions,
})
relationsToBuild.set(fieldName, selectTableName)
@@ -296,7 +316,13 @@ export const traverseFields = ({
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
})
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
@@ -334,6 +360,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
versions,
})
if (subHasManyTextField) {
@@ -356,7 +383,7 @@ export const traverseFields = ({
}
if (hasLocalesTable(field.fields)) {
result._locales = many(adapter.tables[`${arrayTableName}_locales`])
result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`])
}
subRelationsToBuild.forEach((val, key) => {
@@ -375,7 +402,13 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}`
const blockTableName = getTableName({
adapter,
config: block,
parentTableName: rootTableName,
prefix: `${rootTableName}_blocks_`,
throwValidationError,
})
if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
@@ -416,6 +449,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: blockTableName,
versions,
})
if (subHasManyTextField) {
@@ -439,7 +473,9 @@ export const traverseFields = ({
}
if (hasLocalesTable(block.fields)) {
result._locales = many(adapter.tables[`${blockTableName}_locales`])
result._locales = many(
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
)
}
subRelationsToBuild.forEach((val, key) => {
@@ -451,7 +487,7 @@ export const traverseFields = ({
)
adapter.relations[`relations_${blockTableName}`] = blockTableRelations
} else if (process.env.NODE_ENV !== 'production') {
} else if (process.env.NODE_ENV !== 'production' && !versions) {
validateExistingBlockIsIdentical({
block,
localized: field.localized,
@@ -459,7 +495,7 @@ export const traverseFields = ({
table: adapter.tables[blockTableName],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
})
@@ -498,6 +534,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -540,6 +577,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -583,6 +621,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (tabHasLocalizedField) hasLocalizedField = true
@@ -626,6 +665,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (rowHasLocalizedField) hasLocalizedField = true

View File

@@ -24,11 +24,14 @@ export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = {
idType?: 'serial' | 'uuid'
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
push?: boolean
relationshipsSuffix?: string
schemaName?: string
versionsSuffix?: string
}
export type GenericColumn = PgColumn<
@@ -58,6 +61,10 @@ export type DrizzleTransaction = PgTransaction<
>
export type PostgresAdapter = BaseDatabaseAdapter & {
/**
* Used internally to map the block name to the table name
*/
blockTableNames: Record<string, string>
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
/**
@@ -66,12 +73,14 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
*/
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
localesSuffix?: string
logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
@@ -82,6 +91,7 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
}
}
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
@@ -98,9 +108,11 @@ declare module 'payload' {
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>>
localeSuffix?: string
pool: Pool
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
sessions: {
[id: string]: {
@@ -110,5 +122,6 @@ declare module 'payload' {
}
}
tables: Record<string, GenericTable>
versionsSuffix?: string
}
}

View File

@@ -1,11 +1,10 @@
import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -14,7 +13,10 @@ export const updateOne: UpdateOne = async function updateOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id
@@ -49,7 +51,7 @@ export const updateOne: UpdateOne = async function updateOne(
fields: collection.fields,
operation: 'update',
req,
tableName: toSnakeCase(collectionSlug),
tableName,
})
return result

View File

@@ -1,10 +1,9 @@
import type { UpdateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobal<T extends TypeWithID>(
@@ -13,7 +12,10 @@ export async function updateGlobal<T extends TypeWithID>(
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = toSnakeCase(slug)
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const existingGlobal = await db.query[tableName].findFirst({})

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobalVersion<T extends TypeWithID>(
@@ -25,7 +25,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global,
)
const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${toSnakeCase(global)}_v`
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
const { where } = await buildQuery({

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateVersion<T extends TypeWithID>(
@@ -23,7 +23,11 @@ export async function updateVersion<T extends TypeWithID>(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-alpha.54",
"version": "3.0.0-beta.2",
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",

View File

@@ -238,12 +238,14 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
resolve: getDeleteResolver(collection),
}
graphqlResult.Mutation.fields[`duplicate${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: duplicateResolver(collection),
if (collectionConfig.disableDuplicate !== true) {
graphqlResult.Mutation.fields[`duplicate${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: duplicateResolver(collection),
}
}
if (collectionConfig.versions) {

View File

@@ -26,7 +26,7 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "^18.2.0",
"@types/react": "18.2.74",
"payload": "workspace:*"
},
"peerDependencies": {

15
packages/next/.swcrc-cjs Normal file
View File

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

View File

@@ -1,54 +0,0 @@
import path from 'path'
import fs from 'fs'
import { copyRecursiveSync } from './utilities/copyRecursiveSync'
import { copyFile } from './utilities/copyFile'
const install = () => {
const useSrc = fs.existsSync(path.resolve(process.cwd(), './src'))
const hasAppFolder = fs.existsSync(path.resolve(process.cwd(), `./${useSrc ? 'src/' : 'app'}`))
if (!hasAppFolder) {
console.error(
`You need to have a ${
useSrc ? 'src/' : 'app/'
} folder in your project before running this command.`,
)
process.exit(1)
}
const basePath = useSrc ? './src' : '.'
// Copy handlers into /api
copyRecursiveSync(
path.resolve(__dirname, './templates/pages/api'),
path.resolve(process.cwd(), `${basePath}/pages/api`),
)
// Copy admin into /app
copyRecursiveSync(
path.resolve(__dirname, './templates/app'),
path.resolve(process.cwd(), `${basePath}/app`),
)
const payloadConfigPath = path.resolve(process.cwd(), `${basePath}/payload`)
if (!fs.existsSync(payloadConfigPath)) {
fs.mkdirSync(payloadConfigPath)
}
// Copy payload initialization
copyFile(
path.resolve(__dirname, './templates/payloadClient.ts'),
path.resolve(process.cwd(), `${basePath}/payload/payloadClient.ts`),
)
// Copy base payload config
copyFile(
path.resolve(__dirname, './templates/payload.config.ts'),
path.resolve(process.cwd(), `${basePath}/payload/payload.config.ts`),
)
process.exit(0)
}
export default install()

View File

@@ -1,7 +0,0 @@
import fs from 'fs'
export const copyFile = (source, target) => {
if (!fs.existsSync(target)) {
fs.writeFileSync(target, fs.readFileSync(source))
}
}

View File

@@ -1,16 +0,0 @@
import fs from 'fs'
import path from 'path'
export function copyRecursiveSync(src, dest) {
var exists = fs.existsSync(src)
var stats = exists && fs.statSync(src)
var isDirectory = exists && stats && stats.isDirectory()
if (isDirectory) {
fs.mkdirSync(dest, { recursive: true })
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
})
} else {
fs.copyFileSync(src, dest)
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-alpha.54",
"version": "3.0.0-beta.2",
"main": "./src/index.js",
"types": "./src/index.js",
"type": "module",
@@ -10,11 +10,9 @@
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/next"
},
"bin": {
"@payloadcms/next": "./dist/bin/index.js"
},
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:webpack && rm dist/prod/index.js",
"build:cjs": "swc ./src/withPayload.js -o ./dist/cjs/withPayload.cjs --config-file .swcrc-cjs",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:cjs && pnpm build:types && pnpm build:webpack && rm dist/prod/index.js",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:webpack": "webpack --config webpack.config.js",
@@ -30,6 +28,10 @@
"require": "./src/index.js",
"types": "./src/index.js"
},
"./withPayload": {
"import": "./src/withPayload.js",
"require": "./src/withPayload.js"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
@@ -39,8 +41,8 @@
"devDependencies": {
"@next/eslint-plugin-next": "^14.1.0",
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
"@types/react": "18.2.74",
"@types/react-dom": "18.2.24",
"@types/ws": "^8.5.10",
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
@@ -87,9 +89,9 @@
"require": "./dist/prod/styles.css",
"default": "./dist/prod/styles.css"
},
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
"./withPayload": {
"import": "./dist/withPayload.js",
"require": "./dist/cjs/withPayload.cjs"
},
"./*": {
"import": "./dist/exports/*.js",

View File

@@ -1 +1,2 @@
export { getNextRequestI18n } from '../utilities/getNextRequestI18n.js'
export { getPayloadHMR } from '../utilities/getPayloadHMR.js'

View File

@@ -1,13 +1,14 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { translations } from '@payloadcms/translations/client'
import { rtlLanguages } from '@payloadcms/translations'
import { initI18n } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui/providers/Root'
import '@payloadcms/ui/scss/app.scss'
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { parseCookies } from 'payload/auth'
import { createClientConfig } from 'payload/config'
import { deepMerge } from 'payload/utilities'
import React from 'react'
import 'react-toastify/dist/ReactToastify.css'
@@ -20,8 +21,6 @@ export const metadata = {
title: 'Next.js',
}
const rtlLanguages = ['ar', 'fa', 'ha', 'ku', 'ur', 'ps', 'dv', 'ks', 'khw', 'he', 'yi']
export const RootLayout = async ({
children,
config: configPromise,
@@ -30,32 +29,42 @@ export const RootLayout = async ({
config: Promise<SanitizedConfig>
}) => {
const config = await configPromise
const clientConfig = await createClientConfig(config)
const headers = getHeaders()
const cookies = parseCookies(headers)
const lang =
getRequestLanguage({
config,
cookies,
headers,
}) ?? clientConfig.i18n.fallbackLanguage
const languageCode = getRequestLanguage({
config,
cookies,
headers,
})
const dir = rtlLanguages.includes(lang) ? 'RTL' : 'LTR'
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
const clientConfig = await createClientConfig({ config, t: i18n.t })
const mergedTranslations = deepMerge(translations, clientConfig.i18n.translations)
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
: 'LTR'
const languageOptions = Object.entries(translations || {}).map(([language, translations]) => ({
label: translations.general.thisLanguage,
value: language,
}))
const languageOptions = Object.entries(config.i18n.supportedLanguages || {}).reduce(
(acc, [language, languageConfig]) => {
if (Object.keys(config.i18n.supportedLanguages).includes(language)) {
acc.push({
label: languageConfig.translations.general.thisLanguage,
value: language,
})
}
return acc
},
[],
)
// eslint-disable-next-line @typescript-eslint/require-await
async function switchLanguageServerAction(lang: string): Promise<void> {
'use server'
nextCookies().set({
name: `${config.cookiePrefix || 'payload'}-lng'`,
name: `${config.cookiePrefix || 'payload'}-lng`,
path: '/',
value: lang,
})
@@ -66,20 +75,22 @@ export const RootLayout = async ({
DefaultListView,
children,
config,
i18n,
})
return (
<html dir={dir} lang={lang}>
<html dir={dir} lang={languageCode}>
<body>
<RootProvider
componentMap={componentMap}
config={clientConfig}
dateFNSKey={i18n.dateFNSKey}
fallbackLang={clientConfig.i18n.fallbackLanguage}
lang={lang}
languageCode={languageCode}
languageOptions={languageOptions}
// eslint-disable-next-line react/jsx-no-bind
switchLanguageServerAction={switchLanguageServerAction}
translations={mergedTranslations[lang]}
translations={i18n.translations}
>
{wrappedChildren}
</RootProvider>

View File

@@ -1,11 +1,5 @@
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
import type {
DocumentPreferences,
Field,
PayloadRequest,
SanitizedConfig,
TypeWithID,
} from 'payload/types'
import type { DocumentPreferences, Field, PayloadRequest, TypeWithID } from 'payload/types'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
@@ -22,12 +16,12 @@ if (!cached) {
cached = global._payload_fieldSchemaMap = null
}
export const getFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
export const getFieldSchemaMap = (req: PayloadRequest): FieldSchemaMap => {
if (cached && process.env.NODE_ENV !== 'development') {
return cached
}
cached = buildFieldSchemaMap(config)
cached = buildFieldSchemaMap(req)
return cached
}
@@ -46,7 +40,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction(req)
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
return Response.json(null, {
@@ -65,7 +59,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
})
}
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
const fieldSchemaMap = getFieldSchemaMap(req)
const id = collectionSlug ? reqData.id : undefined
const schemaPathSegments = schemaPath.split('.')

View File

@@ -378,7 +378,7 @@ export const POST =
res = await (
endpoints.collection.POST[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
)({ id: slug3, collection, req })
} else if (slug3 === 'duplicate') {
} else if (slug3 === 'duplicate' && collection.config.disableDuplicate !== true) {
// /:collection/:id/duplicate
res = await endpoints.collection.POST.duplicate({ id: slug2, collection, req })
}

View File

@@ -1,36 +0,0 @@
import type { Payload, PayloadRequest } from 'payload/types'
import { getAccessResults, getAuthenticatedUser, parseCookies } from 'payload/auth'
type Args = {
headers: Request['headers']
payload: Payload
}
export const auth = async ({ headers, payload }: Args) => {
const cookies = parseCookies(headers)
const user = await getAuthenticatedUser({
cookies,
headers,
payload,
})
const permissions = await getAccessResults({
req: {
context: {},
headers,
i18n: undefined,
payload,
payloadAPI: 'REST',
t: undefined,
user,
} as PayloadRequest,
})
return {
cookies,
permissions,
user,
}
}

View File

@@ -1,10 +1,13 @@
import type { SanitizedConfig } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { FieldSchemaMap } from './types.js'
import { traverseFields } from './traverseFields.js'
export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
export const buildFieldSchemaMap = ({
i18n,
payload: { config },
}: PayloadRequest): FieldSchemaMap => {
const result: FieldSchemaMap = new Map()
const validRelationships = config.collections.map((c) => c.slug) || []
@@ -13,6 +16,7 @@ export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap =>
traverseFields({
config,
fields: collection.fields,
i18n,
schemaMap: result,
schemaPath: collection.slug,
validRelationships,
@@ -23,6 +27,7 @@ export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap =>
traverseFields({
config,
fields: global.fields,
i18n,
schemaMap: result,
schemaPath: global.slug,
validRelationships,

View File

@@ -1,3 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { Field, SanitizedConfig } from 'payload/types'
import { tabHasName } from 'payload/types'
@@ -7,6 +8,7 @@ import type { FieldSchemaMap } from './types.js'
type Args = {
config: SanitizedConfig
fields: Field[]
i18n: I18n
schemaMap: FieldSchemaMap
schemaPath: string
validRelationships: string[]
@@ -15,6 +17,7 @@ type Args = {
export const traverseFields = ({
config,
fields,
i18n,
schemaMap,
schemaPath,
validRelationships,
@@ -28,6 +31,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: field.fields,
i18n,
schemaMap,
schemaPath: `${schemaPath}.${field.name}`,
validRelationships,
@@ -39,6 +43,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: field.fields,
i18n,
schemaMap,
schemaPath,
validRelationships,
@@ -54,6 +59,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: block.fields,
i18n,
schemaMap,
schemaPath: blockSchemaPath,
validRelationships,
@@ -65,6 +71,7 @@ export const traverseFields = ({
if (typeof field.editor.generateSchemaMap === 'function') {
field.editor.generateSchemaMap({
config,
i18n,
schemaMap,
schemaPath: `${schemaPath}.${field.name}`,
})
@@ -83,6 +90,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: tab.fields,
i18n,
schemaMap,
schemaPath: tabSchemaPath,
validRelationships,

View File

@@ -6,8 +6,7 @@ import type {
} from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import { getAuthenticatedUser } from 'payload/auth'
import { executeAuthStrategies } from 'payload/auth'
import { parseCookies } from 'payload/auth'
import { getDataLoader } from 'payload/utilities'
import qs from 'qs'
@@ -72,11 +71,10 @@ export const createPayloadRequest = async ({
headers: request.headers,
})
const i18n = initI18n({
const i18n = await initI18n({
config: config.i18n,
context: 'api',
language,
translations,
})
const customRequest: CustomPayloadRequest = {
@@ -117,7 +115,7 @@ export const createPayloadRequest = async ({
if (data) req.json = () => Promise.resolve(data)
req.payloadDataLoader = getDataLoader(req)
req.user = await getAuthenticatedUser({
req.user = await executeAuthStrategies({
cookies,
headers: req.headers,
isGraphQL,

View File

@@ -1,22 +0,0 @@
import type { I18n } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/client'
import { cookies, headers } from 'next/headers.js'
import { getRequestLanguage } from './getRequestLanguage.js'
export const getNextI18n = ({
config,
language,
}: {
config: SanitizedConfig
language?: string
}): I18n =>
initI18n({
config: config.i18n,
context: 'client',
language: language || getRequestLanguage({ config, cookies: cookies(), headers: headers() }),
translations,
})

View File

@@ -0,0 +1,19 @@
import type { I18n } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { cookies, headers } from 'next/headers.js'
import { getRequestLanguage } from './getRequestLanguage.js'
/**
* In the context of NextJS, this function initializes the i18n object for the current request.
*
* It must be called on the server side, and within the lifecycle of a request since it relies on the request headers and cookies.
*/
export const getNextRequestI18n = async ({ config }: { config: SanitizedConfig }): Promise<I18n> =>
initI18n({
config: config.i18n,
context: 'client',
language: getRequestLanguage({ config, cookies: cookies(), headers: headers() }),
})

View File

@@ -1,12 +1,13 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js'
import type { SanitizedConfig } from 'payload/config'
import { matchLanguage } from '@payloadcms/translations'
import { extractHeaderLanguage } from '@payloadcms/translations'
type GetRequestLanguageArgs = {
config: SanitizedConfig
cookies: Map<string, string> | ReadonlyRequestCookies
defaultLanguage?: string
defaultLanguage?: AcceptedLanguages
headers: Request['headers']
}
@@ -15,14 +16,23 @@ export const getRequestLanguage = ({
cookies,
defaultLanguage = 'en',
headers,
}: GetRequestLanguageArgs): string => {
const acceptLanguage = headers.get('Accept-Language')
const cookieLanguage = cookies.get(`${config.cookiePrefix || 'payload'}-lng'`)
}: GetRequestLanguageArgs): AcceptedLanguages => {
const langCookie = cookies.get(`${config.cookiePrefix || 'payload'}-lng`)
const languageFromCookie = typeof langCookie === 'string' ? langCookie : langCookie?.value
const languageFromHeader = headers.get('Accept-Language')
? extractHeaderLanguage(headers.get('Accept-Language'))
: undefined
const fallbackLang = config?.i18n?.fallbackLanguage || defaultLanguage
const reqLanguage =
(typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) ||
acceptLanguage ||
defaultLanguage
const supportedLanguageKeys = Object.keys(config?.i18n?.supportedLanguages || {})
return matchLanguage(reqLanguage)
if (languageFromCookie && supportedLanguageKeys.includes(languageFromCookie)) {
return languageFromCookie as AcceptedLanguages
}
if (languageFromHeader && supportedLanguageKeys.includes(languageFromHeader)) {
return languageFromHeader
}
return supportedLanguageKeys.includes(fallbackLang) ? (fallbackLang as AcceptedLanguages) : 'en'
}

View File

@@ -8,15 +8,14 @@ import type {
} from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
import { headers as getHeaders } from 'next/headers.js'
import { notFound, redirect } from 'next/navigation.js'
import { parseCookies } from 'payload/auth'
import { createLocalReq, isEntityHidden } from 'payload/utilities'
import qs from 'qs'
import { getPayloadHMR } from '../utilities/getPayloadHMR.js'
import { auth } from './auth.js'
import { getRequestLanguage } from './getRequestLanguage.js'
type Args = {
@@ -35,12 +34,42 @@ export const initPage = async ({
const headers = getHeaders()
const localeParam = searchParams?.locale as string
const payload = await getPayloadHMR({ config: configPromise })
const { collections, globals, localization, routes } = payload.config
const { cookies, permissions, user } = await auth({
headers,
payload,
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const localeCode = localeParam || defaultLocale
const locale = localization && findLocaleFromCode(localization, localeCode)
const cookies = parseCookies(headers)
const language = getRequestLanguage({ config: payload.config, cookies, headers })
const i18n = await initI18n({
config: payload.config.i18n,
context: 'client',
language,
})
const req = await createLocalReq(
{
fallbackLocale: null,
locale: locale.code,
req: {
i18n,
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
} as PayloadRequest,
},
payload,
)
const { permissions, user } = await payload.auth({ headers, req })
req.user = user
const visibleEntities: VisibleEntities = {
collections: payload.config.collections
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
@@ -56,8 +85,6 @@ export const initPage = async ({
const globalSlug = entityType === 'globals' ? entitySlug : undefined
const docID = collectionSlug && createOrID !== 'create' ? createOrID : undefined
const { collections, globals, localization, routes } = payload.config
if (redirectUnauthenticatedUser && !user && route !== '/login') {
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
@@ -68,38 +95,6 @@ export const initPage = async ({
redirect(`${routes.admin}/login?redirect=${route + stringifiedSearchParams}`)
}
const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const localeCode = localeParam || defaultLocale
const locale = localization && findLocaleFromCode(localization, localeCode)
const language = getRequestLanguage({ config: payload.config, cookies, headers })
const i18n = initI18n({
config: payload.config.i18n,
context: 'client',
language,
translations,
})
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
const req = createLocalReq(
{
fallbackLocale: null,
locale: locale.code,
req: {
i18n,
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
} as PayloadRequest,
user,
},
payload,
)
let collectionConfig: SanitizedCollectionConfig
let globalConfig: SanitizedGlobalConfig

View File

@@ -12,10 +12,11 @@ import './index.scss'
export { generateCreateFirstUserMetadata } from './meta.js'
export const CreateFirstUser: React.FC<AdminViewProps> = async ({ initPageResult }) => {
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageResult }) => {
const {
req,
req: {
i18n,
payload: {
config,
config: {
@@ -51,6 +52,7 @@ export const CreateFirstUser: React.FC<AdminViewProps> = async ({ initPageResult
const createFirstUserFieldMap = mapFields({
config,
fieldSchema: fields,
i18n,
parentPath: userSlug,
})

View File

@@ -3,7 +3,7 @@ import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload/t
import type { GenerateViewMetadata } from '../Root/index.js'
import { getNextI18n } from '../../utilities/getNextI18n.js'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { generateMetadata as apiMeta } from '../API/meta.js'
import { generateMetadata as editMeta } from '../Edit/meta.js'
import { generateMetadata as livePreviewMeta } from '../LivePreview/meta.js'
@@ -89,7 +89,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
}
}
const i18n = await getNextI18n({
const i18n = await getNextRequestI18n({
config,
})

View File

@@ -10,7 +10,7 @@ import type {
import { APIView as DefaultAPIView } from '../API/index.js'
import { EditView as DefaultEditView } from '../Edit/index.js'
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
import { Unauthorized } from '../Unauthorized/index.js'
import { UnauthorizedView } from '../Unauthorized/index.js'
import { VersionView as DefaultVersionView } from '../Version/index.js'
import { VersionsView as DefaultVersionsView } from '../Versions/index.js'
import { getCustomViewByKey } from './getCustomViewByKey.js'
@@ -77,7 +77,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = DefaultEditView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
break
}
@@ -87,7 +87,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = DefaultEditView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
break
}
@@ -118,7 +118,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Versions')
DefaultView = DefaultVersionsView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
break
}
@@ -150,7 +150,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Version')
DefaultView = DefaultVersionView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
} else {
const baseRoute = [adminRoute, collectionEntity, collectionSlug, segment3]
@@ -191,7 +191,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = DefaultEditView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
break
}
@@ -219,7 +219,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Versions')
DefaultView = DefaultVersionsView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
break
}
@@ -229,7 +229,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Default')
DefaultView = DefaultEditView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
break
}
@@ -244,7 +244,7 @@ export const getViewsFromConfig = ({
CustomView = getCustomViewByKey(views, 'Version')
DefaultView = DefaultVersionView
} else {
ErrorView = Unauthorized
ErrorView = UnauthorizedView
}
} else {
const baseRoute = [adminRoute, 'globals', globalSlug].filter(Boolean).join('/')

View File

@@ -15,6 +15,7 @@ import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import { NotFoundView } from '../NotFound/index.js'
import { getMetaBySegment } from './getMetaBySegment.js'
import { getViewsFromConfig } from './getViewsFromConfig.js'
@@ -106,12 +107,8 @@ export const Document: React.FC<AdminViewProps> = async ({
ErrorView = collectionViews?.ErrorView
}
if (!CustomView && !DefaultView && !ViewOverride) {
if (ErrorView) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
}
notFound()
if (!CustomView && !DefaultView && !ViewOverride && !ErrorView) {
ErrorView = NotFoundView
}
}
@@ -143,12 +140,8 @@ export const Document: React.FC<AdminViewProps> = async ({
DefaultView = globalViews?.DefaultView
ErrorView = globalViews?.ErrorView
if (!CustomView && !DefaultView && !ViewOverride) {
if (ErrorView) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
}
notFound()
if (!CustomView && !DefaultView && !ViewOverride && !ErrorView) {
ErrorView = NotFoundView
}
}
}
@@ -219,11 +212,15 @@ export const Document: React.FC<AdminViewProps> = async ({
uploadEdits: undefined,
}}
>
<RenderCustomComponent
CustomComponent={ViewOverride || CustomView}
DefaultComponent={DefaultView}
componentProps={viewComponentProps}
/>
{ErrorView ? (
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
) : (
<RenderCustomComponent
CustomComponent={ViewOverride || CustomView}
DefaultComponent={DefaultView}
componentProps={viewComponentProps}
/>
)}
</FormQueryParamsProvider>
</EditDepthProvider>
</DocumentInfoProvider>

View File

@@ -106,43 +106,42 @@ export const Auth: React.FC<Props> = (props) => {
required
/>
<ConfirmPassword disabled={readOnly} />
{!requirePassword && (
<Button
buttonStyle="secondary"
disabled={readOnly}
onClick={() => handleChangePassword(false)}
size="small"
>
{t('general:cancel')}
</Button>
)}
</div>
)}
{((!changingPassword && !requirePassword) || operation === 'update') && (
<div className={`${baseClass}__controls`}>
{!changingPassword && !requirePassword && (
<Button
buttonStyle="secondary"
disabled={readOnly}
id="change-password"
onClick={() => handleChangePassword(true)}
size="small"
>
{t('authentication:changePassword')}
</Button>
)}
{operation === 'update' && (
<Button
buttonStyle="secondary"
disabled={readOnly}
onClick={() => unlock()}
size="small"
>
{t('authentication:forceUnlock')}
</Button>
)}
</div>
)}
<div className={`${baseClass}__controls`}>
{changingPassword && !requirePassword && (
<Button
buttonStyle="secondary"
disabled={readOnly}
onClick={() => handleChangePassword(false)}
size="small"
>
{t('general:cancel')}
</Button>
)}
{!changingPassword && !requirePassword && (
<Button
buttonStyle="secondary"
disabled={readOnly}
id="change-password"
onClick={() => handleChangePassword(true)}
size="small"
>
{t('authentication:changePassword')}
</Button>
)}
{operation === 'update' && (
<Button
buttonStyle="secondary"
disabled={readOnly}
onClick={() => unlock()}
size="small"
>
{t('authentication:forceUnlock')}
</Button>
)}
</div>
</React.Fragment>
)}
{useAPIKey && (

View File

@@ -13,7 +13,7 @@ export { generateForgotPasswordMetadata } from './meta.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const forgotPasswordBaseClass = 'forgot-password'
export const ForgotPassword: React.FC<AdminViewProps> = ({ initPageResult }) => {
export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult }) => {
const {
req: {
i18n,

View File

@@ -13,7 +13,7 @@ import React, { Fragment } from 'react'
import type { DefaultListViewProps, ListPreferences } from './Default/types.js'
import { Unauthorized } from '../Unauthorized/index.js'
import { UnauthorizedView } from '../Unauthorized/index.js'
import { DefaultListView } from './Default/index.js'
export { generateListMetadata } from './meta.js'
@@ -35,7 +35,7 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
const collectionSlug = collectionConfig?.slug
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
return <Unauthorized initPageResult={initPageResult} searchParams={searchParams} />
return <UnauthorizedView initPageResult={initPageResult} searchParams={searchParams} />
}
let listPreferences: ListPreferences
@@ -117,7 +117,10 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
<Fragment>
<HydrateClientUser permissions={permissions} user={user} />
<ListInfoProvider
collectionConfig={createClientCollectionConfig(collectionConfig)}
collectionConfig={createClientCollectionConfig({
collection: collectionConfig,
t: initPageResult.req.i18n.t,
})}
collectionSlug={collectionSlug}
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
newDocumentURL={`${admin}/collections/${collectionSlug}/create`}

View File

@@ -6,11 +6,12 @@ import './index.scss'
const baseClass = 'live-preview-iframe'
export const IFrame: React.FC<{
ref: React.Ref<HTMLIFrameElement>
type Props = {
setIframeHasLoaded: (value: boolean) => void
url: string
}> = forwardRef((props, ref) => {
}
export const IFrame = forwardRef<HTMLIFrameElement, Props>((props, ref) => {
const { setIframeHasLoaded, url } = props
const { zoom } = useLivePreviewContext()

View File

@@ -1,6 +1,7 @@
import type { LivePreviewConfig } from 'payload/config'
import type { EditViewComponent } from 'payload/types'
import type { EditViewComponent, TypeWithID } from 'payload/types'
import { notFound } from 'next/navigation.js'
import React from 'react'
import { LivePreviewClient } from './index.client.js'
@@ -11,6 +12,7 @@ export const LivePreviewView: EditViewComponent = async (props) => {
const {
collectionConfig,
docID,
globalConfig,
locale,
req: {
@@ -22,8 +24,30 @@ export const LivePreviewView: EditViewComponent = async (props) => {
} = {},
} = initPageResult
// TODO(JAKE): not sure what `data` is or what it should be
const data = 'data' in props ? props.data : {}
let data: TypeWithID
try {
if (collectionConfig) {
data = await initPageResult.req.payload.findByID({
id: docID,
collection: collectionConfig.slug,
depth: 0,
draft: true,
fallbackLocale: null,
})
}
if (globalConfig) {
data = await initPageResult.req.payload.findGlobal({
slug: globalConfig.slug,
depth: 0,
draft: true,
fallbackLocale: null,
})
}
} catch (error) {
notFound()
}
let livePreviewConfig: LivePreviewConfig = topLevelLivePreviewConfig
@@ -54,10 +78,11 @@ export const LivePreviewView: EditViewComponent = async (props) => {
const url =
typeof livePreviewConfig?.url === 'function'
? await livePreviewConfig.url({
collectionConfig,
data,
documentInfo: {}, // TODO: recreate this object server-side, see `useDocumentInfo`
// @ts-expect-error
globalConfig,
locale,
payload: initPageResult.req.payload,
})
: livePreviewConfig?.url

View File

@@ -11,7 +11,7 @@ export { generateLoginMetadata } from './meta.js'
export const loginBaseClass = 'login'
export const Login: React.FC<AdminViewProps> = ({ initPageResult, searchParams }) => {
export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, searchParams }) => {
const { req } = initPageResult
const {

View File

@@ -10,7 +10,7 @@ const baseClass = 'logout'
export { generateLogoutMetadata } from './meta.js'
export const Logout: React.FC<
export const LogoutView: React.FC<
AdminViewProps & {
inactivity?: boolean
}
@@ -39,5 +39,5 @@ export const Logout: React.FC<
}
export const LogoutInactivity: React.FC<AdminViewProps> = (props) => {
return <Logout inactivity {...props} />
return <LogoutView inactivity {...props} />
}

View File

@@ -1,22 +1,28 @@
import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types'
import type { AdminViewComponent, SanitizedConfig } from 'payload/types'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import React, { Fragment } from 'react'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { initPage } from '../../utilities/initPage.js'
import { NotFoundClient } from './index.client.js'
export const generatePageMetadata = async ({
i18n,
config: configPromise,
}: {
config: SanitizedConfig
i18n: I18n
config: Promise<SanitizedConfig> | SanitizedConfig
params?: { [key: string]: string | string[] }
//eslint-disable-next-line @typescript-eslint/require-await
}): Promise<Metadata> => {
const config = await configPromise
const i18n = await getNextRequestI18n({
config,
})
return {
title: i18n.t('general:notFound'),
}
@@ -59,3 +65,7 @@ export const NotFoundPage = async ({
</Fragment>
)
}
export const NotFoundView: AdminViewComponent = () => {
return <NotFoundClient marginTop="large" />
}

View File

@@ -4,15 +4,15 @@ import type { AdminViewComponent } from 'payload/types'
import type { initPage } from '../../utilities/initPage.js'
import { Account } from '../Account/index.js'
import { CreateFirstUser } from '../CreateFirstUser/index.js'
import { CreateFirstUserView } from '../CreateFirstUser/index.js'
import { Dashboard } from '../Dashboard/index.js'
import { Document as DocumentView } from '../Document/index.js'
import { ForgotPassword, forgotPasswordBaseClass } from '../ForgotPassword/index.js'
import { ForgotPasswordView, forgotPasswordBaseClass } from '../ForgotPassword/index.js'
import { ListView } from '../List/index.js'
import { Login, loginBaseClass } from '../Login/index.js'
import { Logout, LogoutInactivity } from '../Logout/index.js'
import { LoginView, loginBaseClass } from '../Login/index.js'
import { LogoutInactivity, LogoutView } from '../Logout/index.js'
import { ResetPassword, resetPasswordBaseClass } from '../ResetPassword/index.js'
import { Unauthorized } from '../Unauthorized/index.js'
import { UnauthorizedView } from '../Unauthorized/index.js'
import { Verify, verifyBaseClass } from '../Verify/index.js'
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
@@ -24,12 +24,12 @@ const baseClasses = {
}
const oneSegmentViews = {
'create-first-user': CreateFirstUser,
forgot: ForgotPassword,
login: Login,
logout: Logout,
'create-first-user': CreateFirstUserView,
forgot: ForgotPasswordView,
login: LoginView,
logout: LogoutView,
'logout-inactivity': LogoutInactivity,
unauthorized: Unauthorized,
unauthorized: UnauthorizedView,
}
export const getViewFromConfig = ({

View File

@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types'
import { getNextI18n } from '../../utilities/getNextI18n.js'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { generateAccountMetadata } from '../Account/index.js'
import { generateCreateFirstUserMetadata } from '../CreateFirstUser/index.js'
import { generateDashboardMetadata } from '../Dashboard/index.js'
@@ -49,7 +49,7 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
const isGlobal = segmentOne === 'globals'
const isCollection = segmentOne === 'collections'
const i18n = await getNextI18n({
const i18n = await getNextRequestI18n({
config,
})

View File

@@ -0,0 +1,36 @@
@import '../../scss/styles.scss';
.unauthorized {
margin-top: var(--base);
& > * {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
&__button {
margin: 0;
}
&--margin-top-large {
margin-top: calc(var(--base) * 2);
}
@include large-break {
&--margin-top-large {
margin-top: var(--base);
}
}
@include small-break {
margin-top: calc(var(--base) / 2);
&--margin-top-large {
margin-top: calc(var(--base) / 2);
}
}
}

View File

@@ -5,11 +5,15 @@ import { Gutter } from '@payloadcms/ui/elements/Gutter'
import LinkImport from 'next/link.js'
import React from 'react'
import './index.scss'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export { generateUnauthorizedMetadata } from './meta.js'
export const Unauthorized: AdminViewComponent = ({ initPageResult }) => {
const baseClass = 'unauthorized'
export const UnauthorizedView: AdminViewComponent = ({ initPageResult }) => {
const {
req: {
i18n,
@@ -22,11 +26,10 @@ export const Unauthorized: AdminViewComponent = ({ initPageResult }) => {
} = initPageResult
return (
<Gutter className="unauthorized">
<Gutter className={baseClass}>
<h2>{i18n.t('error:unauthorized')}</h2>
<p>{i18n.t('error:notAllowedToAccessPage')}</p>
<br />
<Button Link={Link} el="link" to={logoutRoute}>
<Button Link={Link} className={`${baseClass}__button`} el="link" to={logoutRoute}>
{i18n.t('authentication:logOut')}
</Button>
</Gutter>

View File

@@ -85,7 +85,9 @@ export const SetStepNav: React.FC<{
url: `${adminRoute}/collections/${collectionSlug}/${id}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
label: doc?.createdAt
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
: '',
},
]
}
@@ -101,7 +103,9 @@ export const SetStepNav: React.FC<{
url: `${adminRoute}/globals/${globalConfig.slug}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
label: doc?.createdAt
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
: '',
},
]
}

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