Compare commits

...

213 Commits

Author SHA1 Message Date
Jarrod Flesch
f6f0aee553 chore: adjusts how actions are stored in the provider and set on views 2024-10-30 08:44:27 -04:00
Jarrod Flesch
f6c0dcacee chore: remove SetViewActions 2024-10-29 16:59:16 -04:00
Jarrod Flesch
4a639ccc19 fix action button creation 2024-10-29 16:55:53 -04:00
Jarrod Flesch
d7c57aeeb9 chore: gets viewActions on the server 2024-10-29 16:16:55 -04:00
Jarrod Flesch
cb2771d3a9 default fieldState to empty object 2024-10-29 11:11:20 -04:00
Jarrod Flesch
3176e87a95 Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-29 10:41:51 -04:00
Jarrod Flesch
76f3102d07 rm console log 2024-10-29 10:41:40 -04:00
Jacob Fletcher
53f05ad303 defers rendering fallback column labels to client 2024-10-29 10:32:30 -04:00
Jarrod Flesch
8e1ef2222d Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-29 10:32:13 -04:00
Jarrod Flesch
40d5ae3165 implements RenderCustomComponent 2024-10-29 10:31:45 -04:00
Jacob Fletcher
fe69f9ec35 fix doc drawer callbacks 2024-10-29 09:56:39 -04:00
Jacob Fletcher
a56c6a5757 poc doc drawer context 2024-10-28 18:01:56 -04:00
Jacob Fletcher
186f886c1d poc list drawer context 2024-10-28 18:01:43 -04:00
Jarrod Flesch
2e9af8d258 uncomment test suite text-fields config 2024-10-28 16:44:54 -04:00
Jarrod Flesch
7162ce9ef5 fix readonly not being threaded to array row render fields 2024-10-28 16:21:34 -04:00
Jarrod Flesch
b6b58550e1 correctly fallback on client for field labels, errors, descriptions 2024-10-28 16:11:49 -04:00
Jarrod Flesch
bfd08bbeaf Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-28 11:45:01 -04:00
Jarrod Flesch
290594a232 chore: migrate fields to extract custom components from fieldState 2024-10-28 11:44:44 -04:00
Alessio Gravili
c6325c13cf fix field component props: Array - Hidden 2024-10-28 08:59:28 -06:00
Jacob Fletcher
c7cd6e3fbb fix upload field allowCreate logic 2024-10-28 10:48:31 -04:00
Alessio Gravili
414e68e463 fix custom component rendering other than Field 2024-10-28 08:47:50 -06:00
Jarrod Flesch
a6df86d596 Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-28 10:27:21 -04:00
Jarrod Flesch
78ffe523f5 fix form array mutations, fixes custom components disappearing again 2024-10-28 10:27:07 -04:00
Alessio Gravili
03645d172d fix: use consistent field component types 2024-10-28 08:18:49 -06:00
Alessio Gravili
9f7924b930 Merge remote-tracking branch 'origin/beta' into feat/on-demand-rsc 2024-10-27 23:56:02 -06:00
Jarrod Flesch
9f5f909094 fix ui fields disappearing, fix replaceFieldRow fn 2024-10-28 01:05:56 -04:00
Jarrod Flesch
997ddb4654 Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-28 00:49:15 -04:00
Jarrod Flesch
73ee8b5549 fix: allow customComponents to be merged into state 2024-10-28 00:48:54 -04:00
Jacob Fletcher
8195bd804b poc list drawers 2024-10-28 00:10:41 -04:00
Jacob Fletcher
4395dc8901 auto increments edit depth 2024-10-26 17:57:36 -04:00
Jacob Fletcher
7668e0907e poc server renders document drawers 2024-10-26 17:56:38 -04:00
Jacob Fletcher
c46946a77c client-side renders default document buttons 2024-10-25 17:24:44 -04:00
Jacob Fletcher
4194b8bc61 rendering fields in drawers 2024-10-25 17:24:17 -04:00
Jarrod Flesch
807500d55f fix block and array fields 2024-10-25 16:53:01 -04:00
Jarrod Flesch
456eea1344 Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-25 15:50:17 -04:00
Jarrod Flesch
af8fed9ab3 working blocks 2024-10-25 15:49:52 -04:00
Jacob Fletcher
38dc313051 sets column prefs server side 2024-10-25 14:18:48 -04:00
Jarrod Flesch
5a53c6b130 Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-25 12:32:15 -04:00
Jarrod Flesch
d3dd8aef53 more working things 2024-10-25 12:32:03 -04:00
Jacob Fletcher
7b39acc54d fix custom filter lookup 2024-10-25 12:31:10 -04:00
Jarrod Flesch
e7b69ce70f thread readOnly into default fields 2024-10-25 10:50:22 -04:00
Jarrod Flesch
91b65b4cc6 Merge branch 'rsc-schemaPaths' into feat/on-demand-rsc 2024-10-25 10:08:25 -04:00
Jarrod Flesch
42f78480ba threads localized prop into input components 2024-10-25 10:07:17 -04:00
Jarrod Flesch
bbe0fa38ae Merge branch 'beta' into rsc-schemaPaths 2024-10-25 10:06:38 -04:00
Jarrod Flesch
d32798a2a0 removes unused props, threads readOnly through to missing fields 2024-10-25 09:20:56 -04:00
Jarrod Flesch
11c505930d undo test in array config 2024-10-25 00:52:58 -04:00
Jarrod Flesch
0dcb109101 Merge remote-tracking branch 'refs/remotes/origin/rsc-schemaPaths' into rsc-schemaPaths 2024-10-25 00:26:38 -04:00
Jarrod Flesch
95a2bc4d1e wires up field permissions 2024-10-25 00:18:25 -04:00
Alessio Gravili
3f9c7e2acd remove console log 2024-10-24 15:33:18 -06:00
Alessio Gravili
50a1770d7e fix buildFormState for partial schema paths (fixes adding array rows) 2024-10-24 15:32:37 -06:00
Alessio Gravili
9bf24c0379 fix schema path handling in createClientConfig 2024-10-24 15:13:54 -06:00
Alessio Gravili
2429f64f3a fix faulty logic in buildPathSegments 2024-10-24 15:09:24 -06:00
Alessio Gravili
5adcff3e76 fix usage of createClientField 2024-10-24 14:52:37 -06:00
Alessio Gravili
bc155c4a87 use array paths / schema paths where possible, add _index- path segments to server-side operation paths / schema paths, merge generatePath and getFieldPaths 2024-10-24 14:45:40 -06:00
Jacob Fletcher
cb03d5d197 renders filter component slots 2024-10-24 15:34:07 -04:00
Jarrod Flesch
b3021b559a fix row styling 2024-10-24 14:35:09 -04:00
Jarrod Flesch
1bc7d91c4f remove path path debug paragraph text 2024-10-24 14:27:57 -04:00
Jarrod Flesch
42dd173986 Merge remote-tracking branch 'refs/remotes/origin/rsc-schemaPaths' into rsc-schemaPaths 2024-10-24 14:26:02 -04:00
Jarrod Flesch
d9b188061d feat: working arrays 2024-10-24 14:25:02 -04:00
Alessio Gravili
8e5ec02037 ensure form state always includes _index, ensure _index is always stripped away from DATA 2024-10-24 10:19:19 -06:00
Jacob Fletcher
1d370e0d69 col ordering 2024-10-24 11:00:12 -04:00
Alessio Gravili
e51067ccaf fix: remove _index- from data before sending it to the server, as it's not processed by the server 2024-10-23 22:55:26 -06:00
Alessio Gravili
d48cb1b8eb fix schemaPath handling for collapsibles and rows 2024-10-23 22:22:51 -06:00
Alessio Gravili
acc4432a99 fix partial schema path handling 2024-10-23 22:06:58 -06:00
Alessio Gravili
be5772b71c fix buildStateFromSchema path input 2024-10-23 21:59:46 -06:00
Alessio Gravili
c1222b5e06 fixes 2024-10-23 21:49:10 -06:00
Jarrod Flesch
24d5bc88f6 dont render if hidden 2024-10-23 23:24:12 -04:00
Alessio Gravili
06750e1f4a remove schemaAccessor 2024-10-23 21:20:20 -06:00
Alessio Gravili
0aaa4fe643 throw proper error if block data contains unknown block slug 2024-10-23 20:54:34 -06:00
Alessio Gravili
cb6a0249fb fix array error 2024-10-23 20:54:16 -06:00
Jacob Fletcher
3383bf3479 poc table sorting 2024-10-23 17:56:16 -04:00
Jarrod Flesch
06536eb275 chore: almost working 2024-10-23 17:07:34 -04:00
Jacob Fletcher
e881dcd4b4 poc toggle columns 2024-10-23 16:10:20 -04:00
Jarrod Flesch
67acde45e6 not working 2024-10-23 14:08:55 -04:00
Jacob Fletcher
d6498e442f explores fields in form state 2024-10-22 17:56:27 -04:00
Jarrod Flesch
7e50fc51f1 Merge remote-tracking branch 'refs/remotes/origin/feat/on-demand-rsc' into feat/on-demand-rsc 2024-10-22 16:48:04 -04:00
Jarrod Flesch
b49f9f92be chore: rendering rows 2024-10-22 16:47:34 -04:00
Jacob Fletcher
9ffbb3f7f9 poc list view 2024-10-22 16:25:05 -04:00
Jacob Fletcher
54301c9088 renders cells in column state 2024-10-22 16:25:05 -04:00
Jacob Fletcher
809df54cf0 progress to ssr tables 2024-10-22 16:25:04 -04:00
Jacob Fletcher
d254a6e622 begins server rendering list cells 2024-10-22 16:25:04 -04:00
Jarrod Flesch
f92dcf68c6 chore: refactor, merge generatePath and generateFieldKey 2024-10-22 15:08:44 -04:00
Jarrod Flesch
c8cba855a2 chore: thread docPrefs through to getFormState 2024-10-22 12:39:22 -04:00
Jarrod Flesch
4e5121f6d3 chore: refactors function params 2024-10-22 12:08:12 -04:00
Jarrod Flesch
c3eefec31f remove renderedFieldMap prop 2024-10-21 17:08:10 -04:00
Jarrod Flesch
7dbd1d861f fixes inifite rendering of collapsibles, remove early return for un-named fields 2024-10-21 17:07:13 -04:00
Jacob Fletcher
3ff9e34199 fixes array rows 2024-10-21 15:38:26 -04:00
Jacob Fletcher
e7c1f98b50 fix nested index paths 2024-10-21 15:27:35 -04:00
Jacob Fletcher
7aa604c0d7 reworks field key 2024-10-21 13:52:13 -04:00
Jacob Fletcher
fbfa0fd5d6 wip 2024-10-21 12:36:36 -04:00
Jacob Fletcher
be149b362a working addFieldRow and begins migrating more fields 2024-10-21 11:19:07 -04:00
Jacob Fletcher
0e05c5d60d working field keys 2024-10-21 10:46:49 -04:00
Jacob Fletcher
91cd7672e3 progress to unique field keys 2024-10-19 14:11:29 -04:00
Jacob Fletcher
ca8e8becd2 begins migrating remaining fields 2024-10-18 10:55:10 -04:00
Jacob Fletcher
b09bd65020 group fields 2024-10-18 10:42:23 -04:00
Jacob Fletcher
7882c83f03 threads index path through renderFields 2024-10-18 00:37:51 -04:00
Jacob Fletcher
7498099ede fixes index path lookup 2024-10-17 23:46:48 -04:00
Jacob Fletcher
f800cb8dc5 back to traditional field schema map 2024-10-17 17:59:32 -04:00
Jacob Fletcher
f6360d055f begins setting fields test suite back to original state 2024-10-17 11:54:39 -04:00
Jacob Fletcher
a597579354 blocks field 2024-10-17 11:31:55 -04:00
Jacob Fletcher
7d2fc41f19 a little hacky but it works 2024-10-17 09:50:10 -04:00
Jacob Fletcher
01ba2c0114 working nested arrays 2024-10-16 14:22:45 -04:00
Jacob Fletcher
f0b431e799 ensures values are not lost when rendering new array items 2024-10-16 11:36:32 -04:00
Jacob Fletcher
b68b625899 array items are dynamically rendering again 2024-10-16 11:06:47 -04:00
Jacob Fletcher
57f8475780 reworks fieldSchemaMap to accomodate new pattern 2024-10-16 09:07:49 -04:00
Jacob Fletcher
e14a8876ab puts rows into state instead of fields 2024-10-14 13:46:43 -04:00
Jacob Fletcher
e9cd82bc81 poc array rows 2024-10-14 12:00:03 -04:00
Jacob Fletcher
054d183a96 progress 2024-10-13 22:20:41 -04:00
Jacob Fletcher
e03a330fd3 please be the last poc 2024-10-11 15:59:13 -04:00
Jacob Fletcher
9038020dd9 nested fields are rendering 2024-10-10 15:17:02 -04:00
Jacob Fletcher
1f0e551578 omg it works 2024-10-10 13:12:03 -04:00
Jacob Fletcher
f2c5750e78 poc rendering components in field state 2024-10-09 14:56:46 -04:00
Jacob Fletcher
23f136ab82 path is now a direct prop 2024-10-08 15:49:22 -04:00
Jacob Fletcher
aa38ac9bd8 cleanup 2024-10-08 14:14:11 -04:00
Jacob Fletcher
5d3294b341 fix nested arrays 2024-10-08 14:05:29 -04:00
Jacob Fletcher
7fddc5fbd9 poc arrays 2024-10-08 13:51:41 -04:00
Jacob Fletcher
d056b0b964 properly isolates handleServerFunctions export to avoid client-side contamination 2024-10-07 13:19:58 -04:00
Jacob Fletcher
57e9109f93 Merge branch 'beta' into feat/on-demand-rsc 2024-10-07 10:27:21 -04:00
Jacob Fletcher
515629b51d cleanup 2024-10-07 10:13:57 -04:00
Jacob Fletcher
6ec779fb6c Squashed commit of the following:
commit 7a0609ab57
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Mon Oct 7 10:00:34 2024 -0400

    wires abort controller into server fn provider

commit 633aaa721d
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Mon Oct 7 09:50:12 2024 -0400

    adjust err handling

commit 860b6cf2bf
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Mon Oct 7 09:26:01 2024 -0400

    safely accesses signal

commit 463e05d7b9
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Mon Oct 7 09:13:39 2024 -0400

    adds additional ac check prior to firing action

commit 231c4b3d1f
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Mon Oct 7 09:00:45 2024 -0400

    cleanup

commit 494c970b0a
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Mon Oct 7 08:46:28 2024 -0400

    more acs

commit 8dcb2eae17
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Sun Oct 6 23:33:53 2024 -0400

    adds abort controllers to docinfo and form actions

commit 0da4326e43
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Sun Oct 6 23:05:14 2024 -0400

    adds back remaining actions

commit 292d8b53e4
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Sun Oct 6 22:53:44 2024 -0400

    thrads abort controller to onchange

commit ca8629e5a6
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Sun Oct 6 22:02:20 2024 -0400

    reintroduces some actions

commit 5f001c0f52
Merge: 75dd030e2 2ba40f333
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Sat Oct 5 09:15:25 2024 -0400

    Merge branch 'beta' into feat/server-actions

commit 2ba40f3335
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Sat Oct 5 09:13:43 2024 -0400

    chore: removes duplicative join field test (#8558)

    There are two of the exact same e2e tests for the join field, which
    throws an error when running these tests locally because they have
    identical names.

commit 463490f670
Author: Elliot DeNolf <denolfe@users.noreply.github.com>
Date:   Fri Oct 4 18:38:27 2024 -0700

    fix(templates): await params/cookies properly (#8560)

commit d564cd44e9
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 17:29:38 2024 -0400

    chore: deflakes lexical e2e test (#8559)

    This has caused me great pain. The problem with this test is that the
    page was waiting for a URL which includes a search query that never
    arrives. This moves the check into a regex pattern for a more accurate
    catch.

commit 75dd030e24
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 16:26:19 2024 -0400

    reverts reset back to fetch to test

commit 7c62e2a327
Author: Paul <paul@payloadcms.com>
Date:   Fri Oct 4 13:02:56 2024 -0600

    feat(ui)!: scope all payload css to payload-default layer (#8545)

    All payload css is now encapsulated inside CSS layers under `@layer
    payload-default`

    Any custom css will now have the highest possible specificity.
    We have also provided a new layer `@layer payload` if you want to use
    layers and ensure that your styles are applied after payload.

    To override existing styles in a way that the existing rules of
    specificity would be respected you can use the default layer like so
    ```css
    @layer payload-default {
      // my styles within the payload specificity
    }
    ```

commit 400293b8ee
Author: Sasha <64744993+r1tsuu@users.noreply.github.com>
Date:   Fri Oct 4 21:46:41 2024 +0300

    fix: duplicate with upload collections (#8552)

    Fixes the duplicate operation with uploads
    Enables duplicate for upload collections by default

commit e4a413eb9a
Author: Elliot DeNolf <denolfe@gmail.com>
Date:   Fri Oct 4 11:31:06 2024 -0700

    chore(release): v3.0.0-beta.111 [skip ci]

commit b99590f477
Author: Sasha <64744993+r1tsuu@users.noreply.github.com>
Date:   Fri Oct 4 21:28:43 2024 +0300

    chore(templates): update templates with next.js promises (#8547)

    Updates templates according to this PR
    https://github.com/payloadcms/payload/pull/8489

commit a4704c1453
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 13:53:52 2024 -0400

    moves docinfo back to fetch to test

commit 0d3416c96d
Author: Alessio Gravili <alessio@gravili.de>
Date:   Fri Oct 4 13:39:03 2024 -0400

    fix(db-postgres): missing types for db.pool by moving @types/pg from devDependencies to dependencies (#8556)

    Fixes lack of types in installed project:

    ![CleanShot 2024-10-04 at 19 18
    58@2x](https://github.com/user-attachments/assets/e7c519ee-72fd-424b-8f6c-41032322fa5e)

    Since we expose stuff from @types/pg to the end user, we need it to be
    installed in the end users project => move to dependencies.

commit 2f6ee80a6a
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 12:54:45 2024 -0400

    changes addFieldRow back to fetch

commit 0128eedf70
Author: Sasha <64744993+r1tsuu@users.noreply.github.com>
Date:   Fri Oct 4 19:25:05 2024 +0300

    fix(drizzle)!: make radio and select column names to snake_case (#8439)

    Fixes https://github.com/payloadcms/payload/issues/8402 and
    https://github.com/payloadcms/payload/issues/8027

    Before DB column names were camelCase:

    ![image](https://github.com/user-attachments/assets/d2965bcf-290a-4f86-9bf4-dfe7e8613934)

    After this change, they are snake_case:
    ![Screenshot 2024-10-04
    114226](https://github.com/user-attachments/assets/bbc8c20b-6745-4dd3-b0c8-56263a4e37b1)

    #### Breaking SQLite / Postgres ⚠️
    If you had any select (not `hasMany: true`) or radio fields with the
    name in camelCase, for example:
    ```ts
    {
      name: 'selectReadOnly',
      type: 'select',
      admin: {
        readOnly: true,
      },
      options: [
        {
          label: 'Value One',
          value: 'one',
        },
        {
          label: 'Value Two',
          value: 'two',
        },
      ],
    },
    ```
    This previously was mapped to the db column name `"selectReadOnly"`. Now
    it's `select_read_only`.
    Generate a new migration to rename your columns.
    ```sh
    pnpm payload migrate:create
    ```
    Then select "rename column" for targeted columns and Drizzle will handle
    the migration.

    ---------

    Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>

commit 414030e1f1
Author: Sasha <64744993+r1tsuu@users.noreply.github.com>
Date:   Fri Oct 4 18:48:54 2024 +0300

    fix(drizzle): row / collapsible inside of localized fields (#8539)

    Fixes https://github.com/payloadcms/payload/issues/8405

commit 41efcbe4ac
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 10:02:48 2024 -0400

    build err

commit 2203d6046c
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 08:32:13 2024 -0400

    reverts default edit view onChange

commit 2cd9594f55
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 07:47:49 2024 -0400

    brings back actions for everything except lexical

commit cc2cad3140
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 00:58:35 2024 -0400

    more cleanup

commit 87e11bd6ad
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 00:20:02 2024 -0400

    remove website lockfile

commit caf4c4a0df
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 00:12:25 2024 -0400

    properly scopes effect cleanup return

commit 3ddd972de0
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 00:07:09 2024 -0400

    adjusts abort controllers

commit 6c95f0a658
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Fri Oct 4 00:00:00 2024 -0400

    fix api route

commit 9206adedee
Merge: 726f8b003 f6eb027f2
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 23:45:17 2024 -0400

    Merge branch 'beta' into feat/server-actions

commit 726f8b003d
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 23:11:53 2024 -0400

    again

commit 40e5571c0e
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 22:56:55 2024 -0400

    more

commit bdbe7dd41f
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 22:10:48 2024 -0400

    temprarily brings back fetch to debug

commit 08bd461d4b
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 19:43:59 2024 -0400

    properly aborts on unmount

commit cf617767e5
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 19:21:42 2024 -0400

    prevents signal sent from client to server

commit ee853234fd
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 18:35:23 2024 -0400

    temp debug

commit 23cd537a33
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 14:59:35 2024 -0400

    removes auto gen upload dirs from test

commit 0a950c0f0b
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 14:47:56 2024 -0400

    plz

commit f84822e7d9
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 13:48:10 2024 -0400

    returns errors instead of throws

commit ee577f53f2
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 13:34:36 2024 -0400

    increases test bodysizelimit

commit 6641bff085
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 13:25:44 2024 -0400

    removes default server action body size limit

commit 53a6dcf213
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 12:49:04 2024 -0400

    plz be it

commit 9e8e7c7c3d
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 11:48:11 2024 -0400

    creates shared callback for common server fns

commit b4d081db3f
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Thu Oct 3 09:39:40 2024 -0400

    some docs

commit bc36bf7984
Author: Jacob Fletcher <jacobsfletch@gmail.com>
Date:   Wed Oct 2 16:26:25 2024 -0400

    increases default serverActions.bodySizeLimit
2024-10-07 10:11:44 -04:00
Jacob Fletcher
84fd8d06f0 Merge feat/server-actions into feat/on-demand-rsc 2024-10-07 09:44:15 -04:00
Jacob Fletcher
01b580cb24 cleanup 2024-10-04 12:37:04 -04:00
Jacob Fletcher
4d60f9c7da Merge branch 'beta' into feat/on-demand-rsc 2024-10-04 12:16:00 -04:00
Jacob Fletcher
fde05840fe begins wiring array action 2024-10-04 12:05:59 -04:00
Jacob Fletcher
8ed1766d5c Merge branch 'feat/server-actions' into feat/on-demand-rsc 2024-10-02 15:44:40 -04:00
Jacob Fletcher
6fa47bf854 fix missing id 2024-10-02 15:16:50 -04:00
Jacob Fletcher
362cd1712d fix error handling 2024-10-02 14:55:51 -04:00
Jacob Fletcher
e669368149 proper error handling and cleanup 2024-10-02 13:45:55 -04:00
Jacob Fletcher
068d7eec52 adds next-env.d.ts to tsconfig.include 2024-10-02 12:51:02 -04:00
Jacob Fletcher
fb861a53ec small docs 2024-10-02 12:50:48 -04:00
Jacob Fletcher
f16e55fff2 fixes docinfo server fn 2024-10-02 12:08:46 -04:00
Jacob Fletcher
47c19224c2 Merge branch 'beta' into feat/server-actions 2024-10-02 11:17:17 -04:00
Jacob Fletcher
fd2444e614 properly formats errors and fixes imports in templates 2024-10-02 11:16:12 -04:00
Jacob Fletcher
31ffe3bc43 adds back missing css imports 2024-10-02 10:11:54 -04:00
Jacob Fletcher
4cd89cd5d7 fixes type imports 2024-10-02 10:06:24 -04:00
Jacob Fletcher
1d7bcb365d removes duplicate test 2024-10-02 09:39:03 -04:00
Jacob Fletcher
8ffc090cac Merge branch 'beta' into feat/server-actions 2024-10-02 09:35:02 -04:00
Jacob Fletcher
039bd0f76d adds proper error handling 2024-10-02 09:33:54 -04:00
Jacob Fletcher
5235ce819d temp 2024-10-02 00:03:08 -04:00
Jacob Fletcher
c1d2736e8b reverts auto-changes to test modules 2024-10-01 23:41:50 -04:00
Jacob Fletcher
eca0a25063 regenerates lockfile 2024-10-01 23:33:12 -04:00
Jacob Fletcher
e737c8db32 fix test component deps 2024-10-01 23:27:10 -04:00
Jacob Fletcher
bc84def8d8 temp: filter args before calling server fn 2024-10-01 23:22:38 -04:00
Jacob Fletcher
2cf4a58e89 renames ClientServerFunction to ServerFunctionClient 2024-10-01 22:26:59 -04:00
Jacob Fletcher
9c8f623068 renames props to proper plural/singular for semantics 2024-10-01 22:05:16 -04:00
Jacob Fletcher
60edd35671 more docs 2024-10-01 17:52:34 -04:00
Jacob Fletcher
bca9aece06 cleanup 2024-10-01 17:16:51 -04:00
Jacob Fletcher
e43a03d0ff scaffolds docs 2024-10-01 17:13:07 -04:00
Jacob Fletcher
4424904f58 excludes examples until released 2024-10-01 16:48:50 -04:00
Jacob Fletcher
5150a5a30f lints 2024-10-01 16:44:32 -04:00
Jacob Fletcher
b6d829acd8 updates all app dirs 2024-10-01 16:33:29 -04:00
Jacob Fletcher
41d622f613 adds temp log to test ci 2024-10-01 16:11:18 -04:00
Jacob Fletcher
c32c7d50ef builds 2024-10-01 14:42:47 -04:00
Jacob Fletcher
d59c1c01c9 cleanup 2024-10-01 14:32:15 -04:00
Jacob Fletcher
e5ce24eafb Merge branch 'beta' into feat/server-actions 2024-10-01 14:07:04 -04:00
Jacob Fletcher
0de81afa92 aborts server function results in docinfoprovider 2024-10-01 14:06:31 -04:00
Jacob Fletcher
0f5fe98a1b renames BaseServerFunctionArgs to DefaultServerFunctionArgs 2024-10-01 12:32:00 -04:00
Jacob Fletcher
e330f1756f renames RootServerFunction type to ServerFunctionHandler 2024-10-01 12:30:33 -04:00
Jacob Fletcher
c353a0f296 renames serverFunction to plural 2024-10-01 12:27:39 -04:00
Jacob Fletcher
a7c1dd057d properly inits req 2024-10-01 12:02:30 -04:00
Jacob Fletcher
3cbf7b2603 gets language from action itself and properly drills user into local req 2024-10-01 11:27:30 -04:00
Jacob Fletcher
a4135e5975 renames function property to fn 2024-10-01 10:49:04 -04:00
Jacob Fletcher
7fb860f15b removes unnecessary useAuth hooks 2024-10-01 10:43:52 -04:00
Jacob Fletcher
e684f3ac2e adds test for custom server functions 2024-10-01 10:23:06 -04:00
Jacob Fletcher
ac25118945 sanitizes server functions from client config 2024-10-01 00:47:42 -04:00
Jacob Fletcher
9a859a453e cleanup 2024-09-30 23:25:38 -04:00
Jacob Fletcher
dc46f18af9 epxoses in config and renames server actions to server function in preparation for react v19-beta 2024-09-30 22:20:53 -04:00
Jacob Fletcher
271a8c7191 cleanup 2024-09-30 17:51:15 -04:00
Jacob Fletcher
0dbc3bad57 passing tests 2024-09-30 17:46:46 -04:00
Jacob Fletcher
4f0cb93204 creates local req 2024-09-30 15:54:57 -04:00
Jacob Fletcher
b035afe4e3 adds auth 2024-09-30 15:36:24 -04:00
Jacob Fletcher
d33f9f5a1c feat!: server actions 2024-09-30 10:06:33 -04:00
Jacob Fletcher
ccba668dc1 reworks array rows and renders tabs 2024-09-28 14:29:28 -04:00
Jacob Fletcher
9188fbe396 fields with subfields 2024-09-27 16:58:35 -04:00
Jacob Fletcher
4d66c65958 cleanup old references 2024-09-27 12:18:12 -04:00
Jacob Fletcher
66c767f201 deprecates old RenderFields 2024-09-27 12:05:46 -04:00
Jacob Fletcher
4daf22c03c deprecates field props context 2024-09-27 11:36:56 -04:00
Jacob Fletcher
30cc5a018c field labels 2024-09-27 10:54:04 -04:00
Jacob Fletcher
71eb66b393 poc array fields 2024-09-27 10:35:20 -04:00
Jacob Fletcher
b88fabf148 Merge branch 'beta' into feat/on-demand-rsc 2024-09-26 23:02:16 -04:00
Jacob Fletcher
25385a4923 begins removing MappedComponent 2024-09-26 18:15:40 -04:00
Jacob Fletcher
911d93c207 migrating remaining fields 2024-09-26 18:06:53 -04:00
Jacob Fletcher
afe19b3c53 handles hidden and disabled fields 2024-09-26 17:37:20 -04:00
Jacob Fletcher
95c3eb3313 more field slots 2024-09-26 16:15:33 -04:00
Jacob Fletcher
b5ea0a787d establishes pattern for field slots 2024-09-26 15:40:54 -04:00
Jacob Fletcher
8849655afc handles sidebar fields 2024-09-26 13:36:00 -04:00
Jacob Fletcher
868698ed47 establishes render entity pattern 2024-09-26 13:18:07 -04:00
Jacob Fletcher
7291adc3c2 threads field state through props 2024-09-26 12:24:12 -04:00
Jacob Fletcher
3c71e2880e renders fields server side 2024-09-26 10:53:46 -04:00
Jacob Fletcher
b840bea4cf caches client config 2024-09-26 10:48:34 -04:00
Jacob Fletcher
a5f82d8a16 bootable edit view 2024-09-25 16:31:25 -04:00
Jacob Fletcher
0d109be224 rendering list view 2024-09-25 13:40:55 -04:00
Jacob Fletcher
c36b6a43a4 rendering admin 2024-09-25 12:50:12 -04:00
Jacob Fletcher
a154a86350 properly exports render component and achives bootable state 2024-09-25 12:49:39 -04:00
Jacob Fletcher
e9815e6ec7 Merge branch 'beta' into feat/on-demand-rsc 2024-09-25 09:48:26 -04:00
Jacob Fletcher
cdde8d729d cleanup 2024-09-25 09:40:37 -04:00
Jacob Fletcher
487599e2ee rewrites RenderComponent, removing all getCreateMappedComponent instances 2024-09-24 23:38:45 -04:00
Jacob Fletcher
c7f3278d93 rewrites client config 2024-09-24 19:44:51 -04:00
Jacob Fletcher
4d8159e9aa Merge branch 'beta' into feat/on-demand-rsc 2024-09-24 13:58:21 -04:00
Jacob Fletcher
e5956051f2 renders lite configs at root and successfully builds 2024-09-24 11:47:24 -04:00
Jacob Fletcher
1155c0aa22 reworks list info provider 2024-09-23 18:07:36 -04:00
Jacob Fletcher
6ca2f1d28b fixes nav 2024-09-23 14:30:15 -04:00
Jacob Fletcher
5d3193a164 fixes add new 2024-09-23 13:45:48 -04:00
Jacob Fletcher
d0af4f2271 collection labels 2024-09-23 12:37:18 -04:00
Jacob Fletcher
69c74ecbbc fixes duplicative render 2024-09-23 12:07:19 -04:00
Jacob Fletcher
76cc178d36 threads field state to server field components 2024-09-23 10:40:44 -04:00
Jacob Fletcher
b63e18573e adjusts config loading hierarchy 2024-09-23 10:02:25 -04:00
Jacob Fletcher
b61d271bd5 successfully threads data through server components 2024-09-23 00:35:10 -04:00
Jacob Fletcher
ddc57dd5cf works 2024-09-22 23:39:24 -04:00
Jacob Fletcher
f53ef13f4b moves views to ui 2024-09-22 23:14:26 -04:00
Jacob Fletcher
168a8c5317 executes server actions client-side 2024-09-22 13:23:55 -04:00
Jacob Fletcher
5d496c60fa achieves rendering state 2024-09-21 18:24:35 -04:00
Jacob Fletcher
72c206551b poc server actions pattern 2024-09-20 23:26:23 -04:00
391 changed files with 21723 additions and 11499 deletions

View File

@@ -1,8 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import configPromise from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts'
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
import type { ServerFunctionClient } from 'payload'
import config from '@payload-config'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
@@ -12,8 +14,17 @@ type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={configPromise} importMap={importMap}>
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)

View File

@@ -0,0 +1,180 @@
---
title: Server Functions
label: Server Functions
order: 100
desc: Execute custom server-side logic from client-side code using Server Functions in Payload.
keywords: server functions, server-side functions, server-side logic, server-side code, server-side, functions, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
---
The Payload [Admin Panel] supports [React Server Functions](https://react.dev/reference/rsc/server-actions) directly through the Payload Config. Server Functions are functions that are defined on the server, which may use server-only modules, but are called by the client. This is a way to execute server-side logic through a client-side action.
Server Functions are a good alternative to traditional [REST API Endpoints](../rest-api#custom-endpoints), but with a few key differences. While they behave similarly, Server Functions:
1. are simpler to define, not requiring a specified route or method
2. are easier to consume, not requiring the Fetch API
3. are able to return React and/or JSX
Server Functions do not necessarily need to be defined in the Payload Config. It is possible to write your own Server Functions and thread them to your client accordingly. You will, however, be responsible for authenticating those requests yourself. All Server Functions defined through the Payload Config will automatically receive a `req` argument, containing the `user`, `payload`, and more.
<Banner type="info">
<strong>Note:</strong>
Server Functions defined through the Payload Config are only available within the Admin Panel, not your public-facing application. For public-facing server-side logic, you can write your own Server Functions directly into your application.
</Banner>
## Admin Options
To add a new Server Function, use the `admin.serverFunctions` property in your Payload config:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
// highlight-start
serverFunctions: [
{
name: 'my-server-action',
fn: ({ req, value }) => `The value is: "${value}"`
}
]
// highlight-end
}
})
```
The following options are available:
| Option | Type | Description |
| --- | --- | --- |
| **name** | `string` | The name of the Server Function. |
| **fn** | `Function` | The function to execute. [More details](#function-arguments) |
### Function Arguments
The function receives an object with the following properties:
| Property | Type | Description |
| --- | --- | --- |
| **req** | `PayloadRequest` | The request object, containing `payload`, `user`, and `config` properties. |
| **importMap** | `Record<string, any>` | The import map object. |
## Client-side Usage
To execute a Server Function from the client, use the `useServerFunctions` hook, passing the `name` of your Server Function:
```tsx
'use client'
import React, { useCallback } from 'react'
import { useServerFunctions } from '@payloadcms/ui'
const MyComponent = () => {
const { serverFunction } = useServerFunctions()
const [result, setResult] = React.useState<string | null>(null)
const callServerAction = useCallback(async () => {
const result = await serverFunction({
name: 'my-server-action',
args: {
value: 'Hello, world!'
}
}) as string
setResult(result)
}, [serverFunction])
return (
<button onClick={callServerAction} type="button">
{result || 'Call Server Action'}
</button>
)
}
```
## How it works
In order for Payload to support Sever Functions through the Payload Config, a single handler is placed at the root of the application:
```ts
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { ServerFunctionClient } from 'payload'
import config from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts'
import { handleServerFunctions } from '@payloadcms/next/utilities' // highlight-line
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
// highlight-start
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
// highlight-end
const Layout = ({ children }: Args) => (
<RootLayout
config={config}
importMap={importMap}
serverFunction={serverFunction} // highlight-line
>
{children}
</RootLayout>
)
export default Layout
```
The Server Function Handler is a necessary pattern for Server Functions to have access to the Payload Config, as well as any other server-only modules that may be required. This is because all server-only modules _must_ be imported in the closure as the Server Function, wherever the `use server` directive is used. [More details](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#closures-and-encryption).
## Troubleshooting
#### "Unknown Server Function: 'my-server-function'"
Ensure the `name` property of your Server Function matches the name you are passing through the `serverFunction` args.
#### "Error: Client Functions cannot be passed directly to Server Functions. Only Functions passed from the Server can be passed back again"
Non-serializable values cannot cross the server / client boundary. Ensure that the args your sending through your Server Function are serializable, i.e. not containing any functions, classes, etc.
#### "Body exceeded _n_ limit"
By default, Next.js places a 1mb limit on the body size of incoming requests. However, this can be increased by setting the `bodySizeLimit` option in your `next.config.ts` file. [More details](https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit).
```ts
{
// ...
experimental: {
serverActions: {
bodySizeLimit: '2mb',
}
}
}
```
## TypeScript
You can import the Payload `ServerFunction` type as well as other common types from the `payload` package. [More details](../typescript/overview).
```ts
import type {
ServerFunction,
ServerFunctionArgs,
ServerFunctionClient,
ServerFunctionClientArgs,
ServerFunctionConfig,
DefaultServerFunctionArgs,
} from 'payload'
```

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import configPromise from '@payload-config'
import config from '@payload-config'
import '@payloadcms/next/css'
import { RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
@@ -13,7 +13,7 @@ type Args = {
}
const Layout = ({ children }: Args) => (
<RootLayout config={configPromise} importMap={importMap}>
<RootLayout config={config} importMap={importMap}>
{children}
</RootLayout>
)

View File

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

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import configPromise from '@payload-config'
import config from '@payload-config'
import '@payloadcms/next/css'
import { RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
@@ -13,7 +13,7 @@ type Args = {
}
const Layout = ({ children }: Args) => (
<RootLayout config={configPromise} importMap={importMap}>
<RootLayout config={config} importMap={importMap}>
{children}
</RootLayout>
)

View File

@@ -19,6 +19,11 @@ const config = withBundleAnalyzer(
typescript: {
ignoreBuildErrors: true,
},
experimental: {
serverActions: {
bodySizeLimit: '5mb',
},
},
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),

View File

@@ -0,0 +1,73 @@
@import '../../scss/styles.scss';
@layer payload-default {
.doc-drawer {
&__header {
width: 100%;
margin-top: base(2.5);
display: flex;
flex-direction: column;
gap: base(0.5);
align-items: flex-start;
}
&__header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
&__header-text {
margin: 0;
}
&__toggler {
background: transparent;
border: 0;
margin: 0;
padding: 0;
cursor: pointer;
color: inherit;
&:focus,
&:focus-within {
outline: none;
}
&:disabled {
pointer-events: none;
}
}
&__header-close {
border: 0;
background-color: transparent;
padding: 0;
cursor: pointer;
overflow: hidden;
width: base(2);
height: base(2);
svg {
width: base(2);
height: base(2);
position: relative;
.stroke {
stroke-width: 2px;
vector-effect: non-scaling-stroke;
}
}
}
@include mid-break {
&__header {
margin-top: base(1.5);
margin-bottom: base(0.5);
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
}
}
}
}

View File

@@ -0,0 +1,34 @@
'use client'
import { Gutter, RenderTitle, useModal, useTranslation, XIcon } from '@payloadcms/ui'
import './index.scss'
const baseClass = 'doc-drawer'
export const DocumentDrawerHeader: React.FC<{
drawerSlug?: string
Header?: React.ReactNode
}> = ({ drawerSlug, Header }) => {
const { toggleModal } = useModal()
const { t } = useTranslation()
return (
<Gutter className={`${baseClass}__header`}>
<div className={`${baseClass}__header-content`}>
<h2 className={`${baseClass}__header-text`}>{Header || <RenderTitle element="span" />}</h2>
{/* TODO: the `button` HTML element breaks CSS transitions on the drawer for some reason...
i.e. changing to a `div` element will fix the animation issue but will break accessibility
*/}
<button
aria-label={t('general:close')}
className={`${baseClass}__header-close`}
onClick={() => toggleModal(drawerSlug)}
type="button"
>
<XIcon />
</button>
</div>
{/* <DocumentTitle /> */}
</Gutter>
)
}

View File

@@ -1,7 +1,7 @@
import type { DocumentTabConfig, DocumentTabProps } from 'payload'
import type React from 'react'
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
import React, { Fragment } from 'react'
import { Fragment } from 'react'
import './index.scss'
import { DocumentTabLink } from './TabLink.js'
@@ -59,17 +59,6 @@ export const DocumentTab: React.FC<
})
: label
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
payload,
permissions,
},
})
const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill')
return (
<DocumentTabLink
adminRoute={routes.admin}
@@ -82,12 +71,19 @@ export const DocumentTab: React.FC<
>
<span className={`${baseClass}__label`}>
{labelToRender}
{mappedPin && (
<Fragment>
&nbsp;
<RenderComponent mappedComponent={mappedPin} />
</Fragment>
)}
<Fragment>
&nbsp;
{/* <RenderServerComponent
Component={Pill}
Fallback={Pill_Component}
importMap={payload.importMap}
serverProps={{
i18n,
payload,
permissions,
}}
/> */}
</Fragment>
</span>
</DocumentTabLink>
)

View File

@@ -6,9 +6,9 @@ import type {
SanitizedGlobalConfig,
} from 'payload'
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
import React from 'react'
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
import { getCustomViews } from './getCustomViews.js'
import { getViewConfig } from './getViewConfig.js'
import './index.scss'
@@ -80,33 +80,24 @@ export const DocumentTabs: React.FC<{
const { path, tab } = CustomView
if (tab.Component) {
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
payload,
permissions,
...props,
key: `tab-custom-${index}`,
path,
},
})
const mappedTab = createMappedComponent(
tab.Component,
undefined,
undefined,
'tab.Component',
)
return (
<RenderComponent
<RenderServerComponent
clientProps={{
key: `tab-custom-${index}`,
path,
}}
Component={tab.Component}
importMap={payload.importMap}
key={`tab-custom-${index}`}
mappedComponent={mappedTab}
serverProps={{
collectionConfig,
globalConfig,
i18n,
key: `tab-custom-${index}`,
path,
payload,
permissions,
}}
/>
)
}
@@ -121,6 +112,7 @@ export const DocumentTabs: React.FC<{
/>
)
}
return null
})}
</ul>

View File

@@ -8,7 +8,7 @@ export const VersionsPill: React.FC = () => {
const { versions } = useDocumentInfo()
// don't count snapshots
const totalVersions = versions?.docs.filter((version) => !version.snapshot).length || 0
const totalVersions = versions?.docs?.filter((version) => !version.snapshot).length || 0
if (!versions?.totalDocs) {
return null

View File

@@ -1,114 +0,0 @@
'use client'
import type { FieldPermissions, LoginWithUsernameOptions } from 'payload'
import { EmailField, RenderFields, TextField, useTranslation } from '@payloadcms/ui'
import { email, username } from 'payload/shared'
import React from 'react'
type Props = {
readonly loginWithUsername?: false | LoginWithUsernameOptions
}
function EmailFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation()
const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail)
const showEmailField =
!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
if (showEmailField) {
return (
<EmailField
autoComplete="off"
field={{
name: 'email',
label: t('general:email'),
required: requireEmail,
}}
validate={email}
/>
)
}
return null
}
function UsernameFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation()
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
const showUsernameField = Boolean(loginWithUsername)
if (showUsernameField) {
return (
<TextField
field={{
name: 'username',
label: t('authentication:username'),
required: requireUsername,
}}
validate={username}
/>
)
}
return null
}
type RenderEmailAndUsernameFieldsProps = {
className?: string
loginWithUsername?: false | LoginWithUsernameOptions
operation?: 'create' | 'update'
permissions?: {
[fieldName: string]: FieldPermissions
}
readOnly: boolean
}
export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFieldsProps) {
const { className, loginWithUsername, operation, permissions, readOnly } = props
return (
<RenderFields
className={className}
fields={[
{
name: 'email',
type: 'text',
admin: {
autoComplete: 'off',
components: {
Field: {
type: 'client',
Component: null,
RenderedComponent: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
},
},
},
localized: false,
},
{
name: 'username',
type: 'text',
admin: {
components: {
Field: {
type: 'client',
Component: null,
RenderedComponent: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
},
},
},
localized: false,
},
]}
forceRender
operation={operation}
path=""
permissions={permissions}
readOnly={readOnly}
schemaPath=""
/>
)
}

View File

@@ -39,6 +39,11 @@
flex-shrink: 0;
}
&__create-new-button {
all: unset;
cursor: pointer;
}
&__toggler {
background: transparent;
border: 0;
@@ -90,16 +95,6 @@
margin-top: base(1);
}
&__first-cell {
border: 0;
background-color: transparent;
padding: 0;
cursor: pointer;
text-decoration: underline;
text-align: left;
white-space: nowrap;
}
@include mid-break {
.collection-list__header {
margin-bottom: base(0.5);

View File

@@ -0,0 +1,89 @@
'use client'
import type { StaticDescription, StaticLabel } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import {
FieldLabel,
Pill,
ReactSelect,
useModal,
useTranslation,
ViewDescription,
XIcon,
} from '@payloadcms/ui'
import React from 'react'
import './index.scss'
const baseClass = 'list-drawer'
export const ListDrawerHeader: React.FC<{
CustomDescription?: React.ReactNode
customHeader?: string
description?: StaticDescription
documentDrawerSlug: string
drawerSlug: string
hasCreatePermission: boolean
pluralLabel: StaticLabel
}> = ({
CustomDescription,
customHeader,
description,
documentDrawerSlug,
drawerSlug,
hasCreatePermission,
pluralLabel,
}) => {
const { i18n, t } = useTranslation()
const { closeModal, openModal } = useModal()
return (
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header-content`}>
<h2 className={`${baseClass}__header-text`}>
{!customHeader ? getTranslation(pluralLabel, i18n) : customHeader}
</h2>
{hasCreatePermission && (
<button
className={`${baseClass}__create-new-button`}
onClick={() => openModal(documentDrawerSlug)}
type="button"
>
<Pill>{t('general:createNew')}</Pill>
</button>
)}
</div>
<button
aria-label={t('general:close')}
className={`${baseClass}__header-close`}
onClick={() => {
closeModal(drawerSlug)
}}
type="button"
>
<XIcon />
</button>
</div>
{description || CustomDescription ? (
<div className={`${baseClass}__sub-header`}>
{CustomDescription ?? <ViewDescription description={description} />}
</div>
) : null}
{/* {moreThanOneAvailableCollection && (
<div className={`${baseClass}__select-collection-wrap`}>
<FieldLabel label={t('upload:selectCollectionToBrowse')} />
<ReactSelect
className={`${baseClass}__select-collection`}
onChange={setSelectedOption} // this is only changing the options which is not rerunning my effect
options={enabledCollectionConfigs.map((coll) => ({
label: getTranslation(coll.labels.singular, i18n),
value: coll.slug,
}))}
value={selectedOption}
/>
</div>
)} */}
</header>
)
}

View File

@@ -1,8 +1,10 @@
import type { ServerProps } from 'payload'
import { getCreateMappedComponent, PayloadLogo, RenderComponent } from '@payloadcms/ui/shared'
import { PayloadLogo } from '@payloadcms/ui/shared'
import React from 'react'
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
export const Logo: React.FC<ServerProps> = (props) => {
const { i18n, locale, params, payload, permissions, searchParams, user } = props
@@ -16,20 +18,20 @@ export const Logo: React.FC<ServerProps> = (props) => {
} = {},
} = payload.config
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo')
return <RenderComponent mappedComponent={mappedCustomLogo} />
return (
<RenderServerComponent
Component={CustomLogo}
Fallback={PayloadLogo}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
)
}

View File

@@ -1,32 +1,23 @@
'use client'
import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { groupNavItems } from '@payloadcms/ui/shared'
import { getTranslation } from '@payloadcms/translations'
import {
NavGroup,
useAuth,
useConfig,
useEntityVisibility,
useNav,
useTranslation,
} from '@payloadcms/ui'
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
import { NavGroup, useConfig, useNav, useTranslation } from '@payloadcms/ui'
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
import LinkWithDefault from 'next/link.js'
import { usePathname } from 'next/navigation.js'
import React, { Fragment } from 'react'
const baseClass = 'nav'
export const DefaultNavClient: React.FC = () => {
const { permissions } = useAuth()
const { isEntityVisible } = useEntityVisibility()
export const DefaultNavClient: React.FC<{
groups: ReturnType<typeof groupNavItems>
}> = ({ groups }) => {
const pathname = usePathname()
const {
config: {
collections,
globals,
routes: { admin: adminRoute },
},
} = useConfig()
@@ -34,53 +25,23 @@ export const DefaultNavClient: React.FC = () => {
const { i18n } = useTranslation()
const { navOpen } = useNav()
const groups = groupNavItems(
[
...collections
.filter(({ slug }) => isEntityVisible({ collectionSlug: slug }))
.map((collection) => {
const entityToGroup: EntityToGroup = {
type: EntityType.collection,
entity: collection,
}
return entityToGroup
}),
...globals
.filter(({ slug }) => isEntityVisible({ globalSlug: slug }))
.map((global) => {
const entityToGroup: EntityToGroup = {
type: EntityType.global,
entity: global,
}
return entityToGroup
}),
],
permissions,
i18n,
)
return (
<Fragment>
{groups.map(({ entities, label }, key) => {
return (
<NavGroup key={key} label={label}>
{entities.map(({ type, entity }, i) => {
let entityLabel: string
{entities.map(({ slug, type, label }, i) => {
let href: string
let id: string
if (type === EntityType.collection) {
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
entityLabel = getTranslation(entity.labels.plural, i18n)
id = `nav-${entity.slug}`
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
id = `nav-${slug}`
}
if (type === EntityType.global) {
href = formatAdminURL({ adminRoute, path: `/globals/${entity.slug}` })
entityLabel = getTranslation(entity.label, i18n)
id = `nav-global-${entity.slug}`
href = formatAdminURL({ adminRoute, path: `/globals/${slug}` })
id = `nav-global-${slug}`
}
const Link = (LinkWithDefault.default ||
@@ -101,7 +62,7 @@ export const DefaultNavClient: React.FC = () => {
tabIndex={!navOpen ? -1 : undefined}
>
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
</LinkElement>
)
})}

View File

@@ -1,9 +1,11 @@
import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { ServerProps } from 'payload'
import { Logout } from '@payloadcms/ui'
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
import React from 'react'
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
import './index.scss'
import { NavHamburger } from './NavHamburger/index.js'
import { NavWrapper } from './NavWrapper/index.js'
@@ -15,7 +17,7 @@ import { DefaultNavClient } from './index.client.js'
export type NavProps = ServerProps
export const DefaultNav: React.FC<NavProps> = (props) => {
const { i18n, locale, params, payload, permissions, searchParams, user } = props
const { i18n, locale, params, payload, permissions, searchParams, user, visibleEntities } = props
if (!payload?.config) {
return null
@@ -25,40 +27,65 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
admin: {
components: { afterNavLinks, beforeNavLinks },
},
collections,
globals,
} = payload.config
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
const mappedBeforeNavLinks = createMappedComponent(
beforeNavLinks,
undefined,
undefined,
'beforeNavLinks',
)
const mappedAfterNavLinks = createMappedComponent(
afterNavLinks,
undefined,
undefined,
'afterNavLinks',
const groups = groupNavItems(
[
...collections
.filter(({ slug }) => visibleEntities.collections.includes(slug))
.map(
(collection) =>
({
type: EntityType.collection,
entity: collection,
}) satisfies EntityToGroup,
),
...globals
.filter(({ slug }) => visibleEntities.globals.includes(slug))
.map(
(global) =>
({
type: EntityType.global,
entity: global,
}) satisfies EntityToGroup,
),
],
permissions,
i18n,
)
return (
<NavWrapper baseClass={baseClass}>
<nav className={`${baseClass}__wrap`}>
<RenderComponent mappedComponent={mappedBeforeNavLinks} />
<DefaultNavClient />
<RenderComponent mappedComponent={mappedAfterNavLinks} />
<RenderServerComponent
Component={beforeNavLinks}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
<DefaultNavClient groups={groups} />
<RenderServerComponent
Component={afterNavLinks}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
<div className={`${baseClass}__controls`}>
<Logout />
</div>

View File

@@ -1 +1,2 @@
export { metadata, RootLayout } from '../layouts/Root/index.js'
export { handleServerFunctions } from '../utilities/handleServerFunctions.js'

View File

@@ -1,3 +1,4 @@
// NOTICE: Server-only utilities, do not import anything client-side here.
export { addDataAndFileToRequest } from '../utilities/addDataAndFileToRequest.js'
export { addLocalesToRequestFromData, sanitizeLocales } from '../utilities/addLocalesToRequest.js'
export { createPayloadRequest } from '../utilities/createPayloadRequest.js'

View File

@@ -1,4 +1,2 @@
export { DefaultEditView as EditView } from '../views/Edit/Default/index.js'
export { DefaultListView as ListView } from '../views/List/Default/index.js'
export { NotFoundPage } from '../views/NotFound/index.js'
export { generatePageMetadata, type GenerateViewMetadata, RootPage } from '../views/Root/index.js'

View File

@@ -1,20 +1,18 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { CustomVersionParser, ImportMap, SanitizedConfig } from 'payload'
import type { CustomVersionParser, ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
import { rtlLanguages } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui'
import '@payloadcms/ui/scss/app.scss'
import { createClientConfig } from '@payloadcms/ui/utilities/createClientConfig'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { checkDependencies, parseCookies } from 'payload'
import React from 'react'
import { getClientConfig } from '../../utilities/getClientConfig.js'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
import { initReq } from '../../utilities/initReq.js'
import { DefaultEditView } from '../../views/Edit/Default/index.js'
import { DefaultListView } from '../../views/List/Default/index.js'
export const metadata = {
description: 'Generated by Next.js',
@@ -41,11 +39,12 @@ let checkedDependencies = false
export const RootLayout = async ({
children,
config: configPromise,
importMap,
serverFunction,
}: {
readonly children: React.ReactNode
readonly config: Promise<SanitizedConfig>
readonly importMap: ImportMap
readonly serverFunction: ServerFunctionClient
}) => {
if (
process.env.NODE_ENV !== 'production' &&
@@ -103,16 +102,6 @@ export const RootLayout = async ({
const { i18n, permissions, req, user } = await initReq(config)
const { clientConfig, render } = await createClientConfig({
children,
config,
DefaultEditView,
DefaultListView,
i18n,
importMap,
payload,
})
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
: 'LTR'
@@ -174,23 +163,29 @@ export const RootLayout = async ({
const isNavOpen = navPreferences?.value?.open ?? true
const clientConfig = await getClientConfig({
config,
i18n,
})
return (
<html data-theme={theme} dir={dir} lang={languageCode}>
<body>
<RootProvider
config={clientConfig}
dateFNSKey={i18n.dateFNSKey}
fallbackLang={clientConfig.i18n.fallbackLanguage}
fallbackLang={config.i18n.fallbackLanguage}
isNavOpen={isNavOpen}
languageCode={languageCode}
languageOptions={languageOptions}
permissions={permissions}
serverFunction={serverFunction}
switchLanguageServerAction={switchLanguageServerAction}
theme={theme}
translations={i18n.translations}
user={user}
>
{render}
{children}
</RootProvider>
<div id="portal" />
</body>

View File

@@ -1,50 +0,0 @@
import type { PayloadRequest } from 'payload'
import { buildFormState as buildFormStateFn } from '@payloadcms/ui/utilities/buildFormState'
import httpStatus from 'http-status'
import { headersWithCors } from '../../utilities/headersWithCors.js'
import { routeError } from './routeError.js'
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
const headers = headersWithCors({
headers: new Headers(),
req,
})
try {
const result = await buildFormStateFn({ req })
return Response.json(result, {
headers,
status: httpStatus.OK,
})
} catch (err) {
req.payload.logger.error({ err, msg: `There was an error building form state` })
if (err.message === 'Could not find field schema for given path') {
return Response.json(
{
message: err.message,
},
{
headers,
status: httpStatus.BAD_REQUEST,
},
)
}
if (err.message === 'Unauthorized') {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
return routeError({
config: req.payload.config,
err,
req,
})
}
}

View File

@@ -26,7 +26,6 @@ import { registerFirstUser } from './auth/registerFirstUser.js'
import { resetPassword } from './auth/resetPassword.js'
import { unlock } from './auth/unlock.js'
import { verifyEmail } from './auth/verifyEmail.js'
import { buildFormState } from './buildFormState.js'
import { endpointsAreDisabled } from './checkEndpoints.js'
import { count } from './collections/count.js'
import { create } from './collections/create.js'
@@ -110,9 +109,6 @@ const endpoints = {
access,
og: generateOGImage,
},
POST: {
'form-state': buildFormState,
},
},
}
@@ -575,10 +571,6 @@ export const POST =
res = new Response('Route Not Found', { status: 404 })
}
}
} else if (slug.length === 1 && slug1 in endpoints.root.POST) {
await addDataAndFileToRequest(req)
addLocalesToRequestFromData(req)
res = await endpoints.root.POST[slug1]({ req })
}
if (res instanceof Response) {

View File

@@ -1,15 +1,26 @@
import type { MappedComponent } from 'payload'
import type { ImportMap, PayloadComponent } from 'payload'
import { RenderComponent } from '@payloadcms/ui/shared'
import React from 'react'
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
export const OGImage: React.FC<{
description?: string
Fallback: React.ComponentType
fontFamily?: string
Icon: MappedComponent
Icon: PayloadComponent
importMap: ImportMap
leader?: string
title?: string
}> = ({ description, fontFamily = 'Arial, sans-serif', Icon, leader, title }) => {
}> = ({
description,
Fallback,
fontFamily = 'Arial, sans-serif',
Icon,
importMap,
leader,
title,
}) => {
return (
<div
style={{
@@ -85,11 +96,13 @@ export const OGImage: React.FC<{
width: '38px',
}}
>
<RenderComponent
<RenderServerComponent
clientProps={{
fill: 'white',
}}
mappedComponent={Icon}
Component={Icon}
Fallback={Fallback}
importMap={importMap}
/>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import type { PayloadRequest } from 'payload'
import { getCreateMappedComponent, PayloadIcon } from '@payloadcms/ui/shared'
import { PayloadIcon } from '@payloadcms/ui/shared'
import fs from 'fs/promises'
import { ImageResponse } from 'next/og.js'
import { NextResponse } from 'next/server.js'
@@ -8,6 +8,7 @@ import path from 'path'
import React from 'react'
import { fileURLToPath } from 'url'
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
import { OGImage } from './image.js'
const filename = fileURLToPath(import.meta.url)
@@ -33,18 +34,6 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
const leader = hasLeader ? searchParams.get('leader')?.slice(0, 100).replace('-', ' ') : ''
const description = searchParams.has('description') ? searchParams.get('description') : ''
const createMappedComponent = getCreateMappedComponent({
importMap: req.payload.importMap,
serverProps: {},
})
const mappedIcon = createMappedComponent(
config.admin?.components?.graphics?.Icon,
undefined,
PayloadIcon,
'config.admin.components.graphics.Icon',
)
let fontData
try {
@@ -62,8 +51,10 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
(
<OGImage
description={description}
Fallback={PayloadIcon}
fontFamily={fontFamily}
Icon={mappedIcon}
Icon={config.admin?.components?.graphics?.Icon}
importMap={req.payload.importMap}
leader={leader}
title={title}
/>

View File

@@ -1,73 +1,12 @@
import type { Collection, ErrorResult, PayloadRequest, SanitizedConfig } from 'payload'
import httpStatus from 'http-status'
import { APIError, APIErrorName, ValidationErrorName } from 'payload'
import { APIError, formatErrors } from 'payload'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { headersWithCors } from '../../utilities/headersWithCors.js'
import { mergeHeaders } from '../../utilities/mergeHeaders.js'
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResult => {
if (incoming) {
// Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965
// Instead, get the prototype of the incoming error and check its constructor name
const proto = Object.getPrototypeOf(incoming)
// Payload 'ValidationError' and 'APIError'
if (
(proto.constructor.name === ValidationErrorName || proto.constructor.name === APIErrorName) &&
incoming.data
) {
return {
errors: [
{
name: incoming.name,
data: incoming.data,
message: incoming.message,
},
],
}
}
// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
if (proto.constructor.name === ValidationErrorName && 'errors' in incoming && incoming.errors) {
return {
errors: Object.keys(incoming.errors).reduce((acc, key) => {
acc.push({
field: incoming.errors[key].path,
message: incoming.errors[key].message,
})
return acc
}, []),
}
}
if (Array.isArray(incoming.message)) {
return {
errors: incoming.message,
}
}
if (incoming.name) {
return {
errors: [
{
message: incoming.message,
},
],
}
}
}
return {
errors: [
{
message: 'An unknown error occurred.',
},
],
}
}
export const routeError = async ({
collection,
config: configArg,

View File

@@ -1,7 +1,13 @@
import type { MappedComponent, ServerProps, VisibleEntities } from 'payload'
import type { CustomComponent, ServerProps, VisibleEntities } from 'payload'
import { AppHeader, BulkUploadProvider, EntityVisibilityProvider, NavToggler } from '@payloadcms/ui'
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
import {
ActionsProvider,
AppHeader,
BulkUploadProvider,
EntityVisibilityProvider,
NavToggler,
} from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import React from 'react'
import { DefaultNav } from '../../elements/Nav/index.js'
@@ -14,6 +20,7 @@ const baseClass = 'template-default'
export type DefaultTemplateProps = {
children?: React.ReactNode
className?: string
viewActions?: CustomComponent[]
visibleEntities: VisibleEntities
} & ServerProps
@@ -27,6 +34,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
permissions,
searchParams,
user,
viewActions,
visibleEntities,
}) => {
const {
@@ -38,54 +46,77 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
} = {},
} = payload.config || {}
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
const MappedDefaultNav: MappedComponent = createMappedComponent(
CustomNav,
undefined,
DefaultNav,
'CustomNav',
)
const MappedCustomHeader = createMappedComponent(
CustomHeader,
undefined,
undefined,
'CustomHeader',
)
return (
<EntityVisibilityProvider visibleEntities={visibleEntities}>
<BulkUploadProvider>
<RenderComponent mappedComponent={MappedCustomHeader} />
<div style={{ position: 'relative' }}>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
<NavToggler className={`${baseClass}__nav-toggler`}>
<NavHamburger />
</NavToggler>
</div>
</div>
<Wrapper baseClass={baseClass} className={className}>
<RenderComponent mappedComponent={MappedDefaultNav} />
<ActionsProvider
Actions={
viewActions
? viewActions.reduce((acc, action, i) => {
if (action) {
if (typeof action === 'object') {
acc[action.path] = (
<RenderServerComponent Component={action} importMap={payload.importMap} />
)
} else {
acc[action] = (
<RenderServerComponent Component={action} importMap={payload.importMap} />
)
}
}
<div className={`${baseClass}__wrap`}>
<AppHeader />
{children}
return acc
}, {})
: undefined
}
>
<RenderServerComponent
clientProps={{ clientProps: { visibleEntities } }}
Component={CustomHeader}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
visibleEntities,
}}
/>
<div style={{ position: 'relative' }}>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
<NavToggler className={`${baseClass}__nav-toggler`}>
<NavHamburger />
</NavToggler>
</div>
</div>
</Wrapper>
</div>
<Wrapper baseClass={baseClass} className={className}>
<RenderServerComponent
clientProps={{ clientProps: { visibleEntities } }}
Component={CustomNav}
Fallback={DefaultNav}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
visibleEntities,
}}
/>
<div className={`${baseClass}__wrap`}>
<AppHeader />
{children}
</div>
</Wrapper>
</div>
</ActionsProvider>
</BulkUploadProvider>
</EntityVisibilityProvider>
)

View File

@@ -0,0 +1,18 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientConfig, SanitizedConfig } from 'payload'
import { createClientConfig } from 'payload'
import { cache } from 'react'
export const getClientConfig = cache(
async (args: { config: SanitizedConfig; i18n: I18nClient }): Promise<ClientConfig> => {
const { config, i18n } = args
const clientConfig = createClientConfig({
config,
i18n,
})
return clientConfig
},
)

View File

@@ -0,0 +1,45 @@
import type { ServerFunction, ServerFunctionHandler } from 'payload'
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
import { buildTableState } from '@payloadcms/ui/utilities/buildTableState'
import { initReq } from './initReq.js'
import { renderDocumentFn } from './renderDocument.js'
import { renderListFn } from './renderList.js'
const defaultFunctions = {
'form-state': buildFormState as any as ServerFunction,
'render-document': renderDocumentFn as any as ServerFunction,
'render-list': renderListFn as any as ServerFunction,
'table-state': buildTableState as any as ServerFunction,
}
export const handleServerFunctions: ServerFunctionHandler = async (args) => {
const { name: fnKey, args: fnArgs, config: configPromise, importMap } = args
const { req } = await initReq(configPromise)
const augmentedArgs: Parameters<ServerFunction>[0] = {
...fnArgs,
importMap,
req,
}
const serverFunctions: {
[key: string]: ServerFunction
} = {
...defaultFunctions,
...req.payload?.config?.admin?.serverFunctions?.reduce((acc, fnConfig) => {
acc[fnConfig.name] = fnConfig.fn
return acc
}, {}),
}
const fn = serverFunctions[fnKey]
if (!fn) {
throw new Error(`Unknown Server Function: ${fnKey}`)
}
return fn(augmentedArgs)
}

View File

@@ -1,4 +1,5 @@
import type { InitPageResult, Locale, PayloadRequest, VisibleEntities } from 'payload'
import type { I18n } from '@payloadcms/translations'
import type { InitPageResult, Locale, VisibleEntities } from 'payload'
import { findLocaleFromCode } from '@payloadcms/ui/shared'
import { headers as getHeaders } from 'next/headers.js'
@@ -46,13 +47,13 @@ export const initPage = async ({
req: {
headers,
host: headers.get('host'),
i18n,
i18n: i18n as I18n,
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
} as PayloadRequest,
},
},
payload,
)

View File

@@ -1,4 +1,4 @@
import type { I18nClient } from '@payloadcms/translations'
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
import { initI18n } from '@payloadcms/translations'
@@ -16,7 +16,10 @@ type Result = {
user: User
}
export const initReq = cache(async function (config: SanitizedConfig): Promise<Result> {
export const initReq = cache(async function (
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
): Promise<Result> {
const config = await configPromise
const payload = await getPayloadHMR({ config })
const headers = await getHeaders()
@@ -40,9 +43,9 @@ export const initReq = cache(async function (config: SanitizedConfig): Promise<R
req: {
headers,
host: headers.get('host'),
i18n,
i18n: i18n as I18n,
url: `${payload.config.serverURL}`,
} as PayloadRequest,
},
},
payload,
)

View File

@@ -0,0 +1,190 @@
import type { I18nClient } from '@payloadcms/translations'
import type {
ClientConfig,
Data,
DocumentPreferences,
PayloadRequest,
SanitizedConfig,
VisibleEntities,
} from 'payload'
import { headers as getHeaders } from 'next/headers.js'
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { renderDocument } from '../views/Document/index.js'
let cachedClientConfig = global._payload_clientConfig
if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}
export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
}): ClientConfig => {
const { config, i18n } = args
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
}
cachedClientConfig = createClientConfig({
config,
i18n,
})
return cachedClientConfig
}
type RenderDocumentResult = {
docID: string
Document: React.ReactNode
preferences: DocumentPreferences
}
export const renderDocumentFn = async (args: {
collectionSlug: string
disableActions?: boolean
docID: string
drawerSlug?: string
initialData?: Data
redirectAfterDelete: boolean
redirectAfterDuplicate: boolean
req: PayloadRequest
}): Promise<RenderDocumentResult> => {
const {
collectionSlug,
disableActions,
docID,
drawerSlug,
initialData,
redirectAfterDelete,
redirectAfterDuplicate,
req,
req: {
i18n,
payload,
payload: { config },
user,
},
} = args
const headers = await getHeaders()
const cookies = parseCookies(headers)
const incomingUserSlug = user?.collection
const adminUserSlug = config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
const clientConfig = getClientConfig({
config,
i18n,
})
// get prefs, then set update them using the columns that we just received
const preferencesKey = `${collectionSlug}-list`
const preferences = await payload
.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
and: [
{
key: {
equals: preferencesKey,
},
},
{
'user.relationTo': {
equals: user.collection,
},
},
{
'user.value': {
equals: user.id,
},
},
],
},
})
.then((res) => res.docs[0]?.value as DocumentPreferences)
const visibleEntities: VisibleEntities = {
collections: payload.config.collections
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
globals: payload.config.globals
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
}
const permissions = await getAccessResults({
req,
})
const { data, Document } = await renderDocument({
clientConfig,
disableActions,
drawerSlug,
importMap: payload.importMap,
initialData,
initPageResult: {
collectionConfig: payload.config.collections.find(
(collection) => collection.slug === collectionSlug,
),
cookies,
docID,
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),
languageOptions: undefined, // TODO
permissions,
req,
translations: undefined, // TODO
visibleEntities,
},
params: {
segments: ['collections', collectionSlug, docID],
},
redirectAfterDelete,
redirectAfterDuplicate,
searchParams: {},
})
return {
docID: data.id,
Document,
preferences,
}
}

View File

@@ -0,0 +1,97 @@
import type {
DocumentSlots,
ImportMap,
Payload,
Permissions,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
} from 'payload'
import React from 'react'
import { RenderServerComponent } from '../../../ui/src/elements/RenderServerComponent/index.js'
export const renderDocumentSlots: (args: {
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
hasSavePermission: boolean
importMap: ImportMap
payload: Payload
permissions: Permissions
}) => DocumentSlots = (args) => {
const { collectionConfig, globalConfig, hasSavePermission, importMap } = args
const components: DocumentSlots = {} as DocumentSlots
const unsavedDraftWithValidations = undefined
if (
(collectionConfig?.admin?.preview || globalConfig?.admin?.preview) &&
(collectionConfig?.admin?.components?.edit?.PreviewButton ||
globalConfig?.admin?.components?.elements?.PreviewButton)
) {
components.PreviewButton = (
<RenderServerComponent
Component={
collectionConfig?.admin?.components?.edit?.PreviewButton ||
globalConfig?.admin?.components?.elements?.PreviewButton
}
importMap={importMap}
/>
)
}
if (hasSavePermission) {
if (collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts) {
if (
collectionConfig?.admin?.components?.edit?.PublishButton ||
globalConfig?.admin?.components?.elements?.PublishButton
) {
components.PublishButton = (
<RenderServerComponent
Component={
collectionConfig?.admin?.components?.edit?.PublishButton ||
globalConfig?.admin?.components?.elements?.PublishButton
}
importMap={importMap}
/>
)
}
if (
((collectionConfig?.versions?.drafts && !collectionConfig?.versions?.drafts?.autosave) ||
unsavedDraftWithValidations ||
(globalConfig?.versions?.drafts && !globalConfig?.versions?.drafts?.autosave)) &&
(collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
globalConfig?.admin?.components?.elements?.SaveDraftButton)
) {
components.SaveDraftButton = (
<RenderServerComponent
Component={
collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
globalConfig?.admin?.components?.elements?.SaveDraftButton
}
importMap={importMap}
/>
)
}
} else {
if (
collectionConfig?.admin?.components?.edit?.SaveButton ||
globalConfig?.admin?.components?.elements?.SaveButton
) {
components.SaveButton = (
<RenderServerComponent
Component={
collectionConfig?.admin?.components?.edit?.SaveButton ||
globalConfig?.admin?.components?.elements?.SaveButton
}
importMap={importMap}
/>
)
}
}
}
return components
}

View File

@@ -0,0 +1,194 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ListPreferences } from '@payloadcms/ui'
import type {
ClientConfig,
Data,
DocumentPreferences,
PayloadRequest,
SanitizedConfig,
VisibleEntities,
} from 'payload'
import { headers as getHeaders } from 'next/headers.js'
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { renderListView } from '../views/List/index.js'
let cachedClientConfig = global._payload_clientConfig
if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}
export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
}): ClientConfig => {
const { config, i18n } = args
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
}
cachedClientConfig = createClientConfig({
config,
i18n,
})
return cachedClientConfig
}
type RenderListResult = {
List: React.ReactNode
preferences: ListPreferences
}
export const renderListFn = async (args: {
collectionSlug: string
disableActions?: boolean
disableBulkDelete?: boolean
disableBulkEdit?: boolean
documentDrawerSlug: string
drawerSlug?: string
enableRowSelections: boolean
redirectAfterDelete: boolean
redirectAfterDuplicate: boolean
req: PayloadRequest
}): Promise<RenderListResult> => {
const {
collectionSlug,
disableActions,
disableBulkDelete,
disableBulkEdit,
documentDrawerSlug,
drawerSlug,
enableRowSelections,
redirectAfterDelete,
redirectAfterDuplicate,
req,
req: {
i18n,
payload,
payload: { config },
user,
},
} = args
const headers = await getHeaders()
const cookies = parseCookies(headers)
const incomingUserSlug = user?.collection
const adminUserSlug = config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
const clientConfig = getClientConfig({
config,
i18n,
})
const preferencesKey = `${collectionSlug}-list`
const preferences = await payload
.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
and: [
{
key: {
equals: preferencesKey,
},
},
{
'user.relationTo': {
equals: user.collection,
},
},
{
'user.value': {
equals: user.id,
},
},
],
},
})
.then((res) => res.docs[0]?.value as ListPreferences)
const visibleEntities: VisibleEntities = {
collections: payload.config.collections
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
globals: payload.config.globals
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
}
const permissions = await getAccessResults({
req,
})
const { List } = await renderListView({
clientConfig,
disableActions,
disableBulkDelete,
disableBulkEdit,
documentDrawerSlug,
drawerSlug,
enableRowSelections,
importMap: payload.importMap,
initPageResult: {
collectionConfig: payload.config.collections.find(
(collection) => collection.slug === collectionSlug,
),
cookies,
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),
languageOptions: undefined, // TODO
permissions,
req,
translations: undefined, // TODO
visibleEntities,
},
params: {
segments: ['collections', collectionSlug],
},
redirectAfterDelete,
redirectAfterDuplicate,
searchParams: {},
})
return {
List,
preferences,
}
}

View File

@@ -9,7 +9,7 @@ import {
Gutter,
MinimizeMaximizeIcon,
NumberField,
SetViewActions,
SetDocumentStepNav,
useConfig,
useDocumentInfo,
useLocale,
@@ -19,7 +19,6 @@ import { useSearchParams } from 'next/navigation.js'
import * as React from 'react'
import { toast } from 'sonner'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import './index.scss'
import { LocaleSelector } from './LocaleSelector/index.js'
import { RenderJSON } from './RenderJSON/index.js'
@@ -42,8 +41,8 @@ export const APIViewClient: React.FC = () => {
getEntityConfig,
} = useConfig()
const collectionClientConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const globalClientConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
const localeOptions =
localization &&
@@ -52,13 +51,13 @@ export const APIViewClient: React.FC = () => {
let draftsEnabled: boolean = false
let docEndpoint: string = ''
if (collectionClientConfig) {
draftsEnabled = Boolean(collectionClientConfig.versions?.drafts)
if (collectionConfig) {
draftsEnabled = Boolean(collectionConfig.versions?.drafts)
docEndpoint = `/${collectionSlug}/${id}`
}
if (globalClientConfig) {
draftsEnabled = Boolean(globalClientConfig.versions?.drafts)
if (globalConfig) {
draftsEnabled = Boolean(globalConfig.versions?.drafts)
docEndpoint = `/globals/${globalSlug}`
}
@@ -111,19 +110,13 @@ export const APIViewClient: React.FC = () => {
>
<SetDocumentStepNav
collectionSlug={collectionSlug}
globalLabel={globalClientConfig?.label}
globalLabel={globalConfig?.label}
globalSlug={globalSlug}
id={id}
pluralLabel={collectionClientConfig ? collectionClientConfig?.labels?.plural : undefined}
useAsTitle={collectionClientConfig ? collectionClientConfig?.admin?.useAsTitle : undefined}
pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
view="API"
/>
<SetViewActions
actions={
(collectionClientConfig || globalClientConfig)?.admin?.components?.views?.edit?.api
?.actions
}
/>
<div className={`${baseClass}__configuration`}>
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>

View File

@@ -22,7 +22,7 @@ export const Settings: React.FC<{
<div className={[baseClass, className].filter(Boolean).join(' ')}>
<h3>{i18n.t('general:payloadSettings')}</h3>
<div className={`${baseClass}__language`}>
<FieldLabel field={null} htmlFor="language-select" label={i18n.t('general:language')} />
<FieldLabel htmlFor="language-select" label={i18n.t('general:language')} />
<LanguageSelector languageOptions={languageOptions} />
</div>
{theme === 'all' && <ToggleTheme />}

View File

@@ -1,12 +1,7 @@
import type { AdminViewProps } from 'payload'
import {
DocumentInfoProvider,
EditDepthProvider,
HydrateAuthProvider,
RenderComponent,
} from '@payloadcms/ui'
import { getCreateMappedComponent } from '@payloadcms/ui/shared'
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { notFound } from 'next/navigation.js'
import React from 'react'
@@ -20,6 +15,7 @@ import { Settings } from './Settings/index.js'
export { generateAccountMetadata } from './meta.js'
export const Account: React.FC<AdminViewProps> = async ({
importMap,
initPageResult,
params,
searchParams,
@@ -61,32 +57,11 @@ export const Account: React.FC<AdminViewProps> = async ({
const { data, formState } = await getDocumentData({
id: user.id,
collectionConfig,
importMap,
locale,
req,
})
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
initPageResult,
locale,
params,
payload,
permissions,
routeSegments: [],
searchParams,
user,
},
})
const mappedAccountComponent = createMappedComponent(
CustomAccountComponent?.Component,
undefined,
EditView,
'CustomAccountComponent.Component',
)
return (
<DocumentInfoProvider
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
@@ -100,7 +75,7 @@ export const Account: React.FC<AdminViewProps> = async ({
initialState={formState}
isEditing
>
<EditDepthProvider depth={1}>
<EditDepthProvider>
<DocumentHeader
collectionConfig={collectionConfig}
hideTabs
@@ -109,7 +84,22 @@ export const Account: React.FC<AdminViewProps> = async ({
permissions={permissions}
/>
<HydrateAuthProvider permissions={permissions} />
<RenderComponent mappedComponent={mappedAccountComponent} />
<RenderServerComponent
Component={CustomAccountComponent}
importMap={payload.importMap}
serverProps={{
i18n,
initPageResult,
locale,
params,
payload,
permissions,
routeSegments: [],
searchParams,
user,
}}
/>
<EditView />
<AccountClient />
</EditDepthProvider>
</DocumentInfoProvider>

View File

@@ -4,19 +4,17 @@ import type { ClientCollectionConfig, FormState, LoginWithUsernameOptions } from
import {
ConfirmPasswordField,
EmailAndUsernameFields,
Form,
FormSubmit,
PasswordField,
RenderFields,
useAuth,
useConfig,
useServerFunctions,
useTranslation,
} from '@payloadcms/ui'
import { getFormState } from '@payloadcms/ui/shared'
import React from 'react'
import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
export const CreateFirstUserClient: React.FC<{
initialState: FormState
loginWithUsername?: false | LoginWithUsernameOptions
@@ -30,6 +28,8 @@ export const CreateFirstUserClient: React.FC<{
getEntityConfig,
} = useConfig()
const { getFormState } = useServerFunctions()
const { t } = useTranslation()
const { setUser } = useAuth()
@@ -38,18 +38,15 @@ export const CreateFirstUserClient: React.FC<{
const onChange: FormProps['onChange'][0] = React.useCallback(
async ({ formState: prevFormState }) => {
const { state } = await getFormState({
apiRoute,
body: {
collectionSlug: userSlug,
formState: prevFormState,
operation: 'create',
schemaPath: `_${userSlug}.auth`,
},
serverURL,
collectionSlug: userSlug,
formState: prevFormState,
operation: 'create',
schemaPath: [`_${userSlug}`, 'auth'],
})
return state
},
[apiRoute, userSlug, serverURL],
[userSlug, getFormState],
)
const handleFirstRegister = (data: UserWithToken) => {
@@ -66,11 +63,12 @@ export const CreateFirstUserClient: React.FC<{
redirect={admin}
validationOperation="create"
>
<RenderEmailAndUsernameFields
<EmailAndUsernameFields
className="emailAndUsername"
loginWithUsername={loginWithUsername}
operation="create"
readOnly={false}
t={t}
/>
<PasswordField
autoComplete={'off'}
@@ -81,14 +79,7 @@ export const CreateFirstUserClient: React.FC<{
}}
/>
<ConfirmPasswordField />
<RenderFields
fields={collectionConfig.fields}
forceRender
operation="create"
path=""
readOnly={false}
schemaPath={userSlug}
/>
{/* Fields Here */}
<FormSubmit size="large">{t('general:create')}</FormSubmit>
</Form>
)

View File

@@ -8,7 +8,10 @@ import './index.scss'
export { generateCreateFirstUserMetadata } from './meta.js'
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageResult }) => {
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({
importMap,
initPageResult,
}) => {
const {
locale,
req,
@@ -28,9 +31,10 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
const { formState } = await getDocumentData({
collectionConfig,
importMap,
locale,
req,
schemaPath: `_${collectionConfig.slug}.auth`,
schemaPath: [`_${collectionConfig.slug}`, 'auth'],
})
return (

View File

@@ -2,13 +2,9 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { Button, Card, Gutter, Locked, SetStepNav, SetViewActions } from '@payloadcms/ui'
import {
EntityType,
formatAdminURL,
getCreateMappedComponent,
RenderComponent,
} from '@payloadcms/ui/shared'
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
import React, { Fragment } from 'react'
import './index.scss'
@@ -46,41 +42,25 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
user,
} = props
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
const mappedBeforeDashboards = createMappedComponent(
beforeDashboard,
undefined,
undefined,
'beforeDashboard',
)
const mappedAfterDashboards = createMappedComponent(
afterDashboard,
undefined,
undefined,
'afterDashboard',
)
return (
<div className={baseClass}>
<SetStepNav nav={[]} />
<SetViewActions actions={[]} />
<Gutter className={`${baseClass}__wrap`}>
<RenderComponent mappedComponent={mappedBeforeDashboards} />
{beforeDashboard && (
<RenderServerComponent
Component={beforeDashboard}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
)}
<Fragment>
<SetViewActions actions={[]} />
{!navGroups || navGroups?.length === 0 ? (
<p>no nav groups....</p>
) : (
@@ -89,8 +69,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
<div className={`${baseClass}__group`} key={groupIndex}>
<h2 className={`${baseClass}__label`}>{label}</h2>
<ul className={`${baseClass}__card-list`}>
{entities.map(({ type, entity }, entityIndex) => {
let title: string
{entities.map(({ slug, type, label }, entityIndex) => {
let buttonAriaLabel: string
let createHREF: string
let href: string
@@ -99,37 +78,30 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
let userEditing = null
if (type === EntityType.collection) {
title = getTranslation(entity.labels.plural, i18n)
buttonAriaLabel = t('general:showAllLabel', { label })
buttonAriaLabel = t('general:showAllLabel', { label: title })
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
createHREF = formatAdminURL({
adminRoute,
path: `/collections/${entity.slug}/create`,
path: `/collections/${slug}/create`,
})
hasCreatePermission =
permissions?.collections?.[entity.slug]?.create?.permission
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
}
if (type === EntityType.global) {
title = getTranslation(entity.label, i18n)
buttonAriaLabel = t('general:editLabel', {
label: getTranslation(entity.label, i18n),
label: getTranslation(label, i18n),
})
href = formatAdminURL({
adminRoute,
path: `/globals/${entity.slug}`,
path: `/globals/${slug}`,
})
// Find the lock status for the global
const globalLockData = globalData.find(
(global) => global.slug === entity.slug,
)
const globalLockData = globalData.find((global) => global.slug === slug)
if (globalLockData) {
lockStatus = globalLockData.data._isLocked
userEditing = globalLockData.data._userEditing
@@ -145,7 +117,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
) : hasCreatePermission && type === EntityType.collection ? (
<Button
aria-label={t('general:createNewLabel', {
label: getTranslation(entity.labels.singular, i18n),
label,
})}
buttonStyle="icon-label"
el="link"
@@ -159,9 +131,9 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
}
buttonAriaLabel={buttonAriaLabel}
href={href}
id={`card-${entity.slug}`}
id={`card-${slug}`}
Link={Link}
title={title}
title={getTranslation(label, i18n)}
titleAs="h3"
/>
</li>
@@ -173,7 +145,21 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
})
)}
</Fragment>
<RenderComponent mappedComponent={mappedAfterDashboards} />
{afterDashboard && (
<RenderServerComponent
Component={afterDashboard}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
)}
</Gutter>
</div>
)

View File

@@ -2,15 +2,11 @@ import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { AdminViewProps } from 'payload'
import { HydrateAuthProvider } from '@payloadcms/ui'
import {
EntityType,
getCreateMappedComponent,
groupNavItems,
RenderComponent,
} from '@payloadcms/ui/shared'
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js'
import React, { Fragment } from 'react'
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
import { DefaultDashboard } from './Default/index.js'
export { generateDashboardMetadata } from './meta.js'
@@ -94,39 +90,30 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
i18n,
)
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
globalData,
i18n,
Link,
locale,
navGroups,
params,
payload,
permissions,
searchParams,
user,
visibleEntities,
},
})
const mappedDashboardComponent = createMappedComponent(
CustomDashboardComponent?.Component,
undefined,
DefaultDashboard,
'CustomDashboardComponent.Component',
)
return (
<Fragment>
<HydrateAuthProvider permissions={permissions} />
<RenderComponent
<RenderServerComponent
clientProps={{
Link,
locale,
}}
mappedComponent={mappedDashboardComponent}
Component={CustomDashboardComponent}
Fallback={DefaultDashboard}
importMap={payload.importMap}
serverProps={{
globalData,
i18n,
Link,
locale,
navGroups,
params,
payload,
permissions,
searchParams,
user,
visibleEntities,
}}
/>
</Fragment>
)

View File

@@ -1,58 +1,63 @@
import type {
Data,
FormState,
ImportMap,
Locale,
PayloadRequest,
SanitizedCollectionConfig,
SanitizedGlobalConfig,
} from 'payload'
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
import { buildFormStateFn as buildFormState } from '@payloadcms/ui/utilities/buildFormState'
import { reduceFieldsToValues } from 'payload/shared'
export const getDocumentData = async (args: {
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
id?: number | string
importMap: ImportMap
locale: Locale
req: PayloadRequest
schemaPath?: string
schemaPath?: string[]
}): Promise<{
data: Data
formState: FormState
}> => {
const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args
const schemaPath = schemaPathFromProps || collectionConfig?.slug || globalConfig?.slug
const schemaPath = schemaPathFromProps?.length
? schemaPathFromProps
: collectionConfig?.slug
? [collectionConfig.slug]
: [globalConfig?.slug]
try {
const { state: formState } = await buildFormState({
req: {
...req,
data: {
id,
collectionSlug: collectionConfig?.slug,
globalSlug: globalConfig?.slug,
locale: locale?.code,
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
schemaPath,
},
},
const result = await buildFormState({
id,
collectionSlug: collectionConfig?.slug,
globalSlug: globalConfig?.slug,
locale: locale?.code,
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
renderFields: true,
req,
schemaPath,
})
const data = reduceFieldsToValues(formState, true)
const data = reduceFieldsToValues(result.state, true)
return {
data,
formState,
formState: result.state,
}
} catch (error) {
console.error('Error getting document data', error) // eslint-disable-line no-console
return {
data: null,
formState: {
fields: {
initialValue: undefined,
schemaPath: [],
valid: false,
value: undefined,
},

View File

@@ -23,7 +23,7 @@ import { getCustomViewByRoute } from './getCustomViewByRoute.js'
export type ViewFromConfig<TProps extends object> = {
Component?: React.FC<TProps>
payloadComponent?: PayloadComponent<TProps>
ComponentConfig?: PayloadComponent<TProps>
}
export const getViewsFromConfig = ({
@@ -94,7 +94,7 @@ export const getViewsFromConfig = ({
docPermissions?.create?.permission
) {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'default'),
ComponentConfig: getCustomViewByKey(views, 'default'),
}
DefaultView = {
Component: DefaultEditView,
@@ -132,11 +132,11 @@ export const getViewsFromConfig = ({
viewKey = customViewKey
CustomView = {
payloadComponent: CustomViewComponent,
ComponentConfig: CustomViewComponent,
}
} else {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'default'),
ComponentConfig: getCustomViewByKey(views, 'default'),
}
DefaultView = {
@@ -156,7 +156,7 @@ export const getViewsFromConfig = ({
case 'api': {
if (collectionConfig?.admin?.hideAPIURL !== true) {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'api'),
ComponentConfig: getCustomViewByKey(views, 'api'),
}
DefaultView = {
Component: DefaultAPIView,
@@ -177,7 +177,7 @@ export const getViewsFromConfig = ({
case 'versions': {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'versions'),
ComponentConfig: getCustomViewByKey(views, 'versions'),
}
DefaultView = {
Component: DefaultVersionsView,
@@ -215,7 +215,7 @@ export const getViewsFromConfig = ({
viewKey = customViewKey
CustomView = {
payloadComponent: CustomViewComponent,
ComponentConfig: CustomViewComponent,
}
}
@@ -230,7 +230,7 @@ export const getViewsFromConfig = ({
if (segment4 === 'versions') {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'version'),
ComponentConfig: getCustomViewByKey(views, 'version'),
}
DefaultView = {
Component: DefaultVersionView,
@@ -266,7 +266,7 @@ export const getViewsFromConfig = ({
viewKey = customViewKey
CustomView = {
payloadComponent: CustomViewComponent,
ComponentConfig: CustomViewComponent,
}
}
}
@@ -286,7 +286,7 @@ export const getViewsFromConfig = ({
switch (routeSegments.length) {
case 2: {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'default'),
ComponentConfig: getCustomViewByKey(views, 'default'),
}
DefaultView = {
Component: DefaultEditView,
@@ -300,7 +300,7 @@ export const getViewsFromConfig = ({
case 'api': {
if (globalConfig?.admin?.hideAPIURL !== true) {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'api'),
ComponentConfig: getCustomViewByKey(views, 'api'),
}
DefaultView = {
Component: DefaultAPIView,
@@ -321,7 +321,7 @@ export const getViewsFromConfig = ({
case 'versions': {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'versions'),
ComponentConfig: getCustomViewByKey(views, 'versions'),
}
DefaultView = {
@@ -356,7 +356,7 @@ export const getViewsFromConfig = ({
viewKey = customViewKey
CustomView = {
payloadComponent: CustomViewComponent,
ComponentConfig: CustomViewComponent,
}
} else {
DefaultView = {
@@ -379,7 +379,7 @@ export const getViewsFromConfig = ({
if (segment3 === 'versions') {
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
CustomView = {
payloadComponent: getCustomViewByKey(views, 'version'),
ComponentConfig: getCustomViewByKey(views, 'version'),
}
DefaultView = {
Component: DefaultVersionView,
@@ -410,7 +410,7 @@ export const getViewsFromConfig = ({
viewKey = customViewKey
CustomView = {
payloadComponent: CustomViewComponent,
ComponentConfig: CustomViewComponent,
}
}
}

View File

@@ -1,23 +1,23 @@
import type {
AdminViewProps,
EditViewComponent,
MappedComponent,
Data,
PayloadComponent,
ServerProps,
ServerSideEditViewProps,
} from 'payload'
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
import {
formatAdminURL,
getCreateMappedComponent,
isEditing as getIsEditing,
RenderComponent,
} from '@payloadcms/ui/shared'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL, isEditing as getIsEditing } from '@payloadcms/ui/shared'
import { notFound, redirect } from 'next/navigation.js'
import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import type { ViewFromConfig } from './getViewsFromConfig.js'
import { DocumentDrawerHeader } from '../../elements/DocumentDrawerHeader/index.js'
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
import { renderDocumentSlots } from '../../utilities/renderDocumentSlots.js'
import { NotFoundView } from '../NotFound/index.js'
import { getDocumentData } from './getDocumentData.js'
import { getDocumentPermissions } from './getDocumentPermissions.js'
@@ -26,12 +26,20 @@ import { getViewsFromConfig } from './getViewsFromConfig.js'
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
export const Document: React.FC<AdminViewProps> = async ({
export const renderDocument = async ({
disableActions,
drawerSlug,
importMap,
initialData,
initPageResult,
params,
redirectAfterDelete,
redirectAfterDuplicate,
searchParams,
}) => {
}: AdminViewProps): Promise<{
data: Data
Document: React.ReactNode
}> => {
const {
collectionConfig,
docID: id,
@@ -55,15 +63,17 @@ export const Document: React.FC<AdminViewProps> = async ({
} = initPageResult
const segments = Array.isArray(params?.segments) ? params.segments : []
const collectionSlug = collectionConfig?.slug || undefined
const globalSlug = globalConfig?.slug || undefined
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
let RootViewOverride: MappedComponent<ServerSideEditViewProps>
let CustomView: MappedComponent<ServerSideEditViewProps>
let DefaultView: MappedComponent<ServerSideEditViewProps>
let ErrorView: MappedComponent<AdminViewProps>
let RootViewOverride: PayloadComponent
let CustomView: ViewFromConfig<ServerSideEditViewProps>
let DefaultView: ViewFromConfig<ServerSideEditViewProps>
let ErrorView: ViewFromConfig<AdminViewProps>
let apiURL: string
@@ -71,12 +81,13 @@ export const Document: React.FC<AdminViewProps> = async ({
id,
collectionConfig,
globalConfig,
importMap,
locale,
req,
})
if (!data) {
notFound()
throw new Error('not-found')
}
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
@@ -87,24 +98,21 @@ export const Document: React.FC<AdminViewProps> = async ({
req,
})
const createMappedComponent = getCreateMappedComponent({
importMap,
serverProps: {
i18n,
initPageResult,
locale,
params,
payload,
permissions,
routeSegments: segments,
searchParams,
user,
},
})
const serverProps: ServerProps = {
i18n,
initPageResult,
locale,
params,
payload,
permissions,
routeSegments: segments,
searchParams,
user,
}
if (collectionConfig) {
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
notFound()
throw new Error('not-found')
}
const params = new URLSearchParams()
@@ -122,12 +130,7 @@ export const Document: React.FC<AdminViewProps> = async ({
RootViewOverride =
collectionConfig?.admin?.components?.views?.edit?.root &&
'Component' in collectionConfig.admin.components.views.edit.root
? createMappedComponent(
collectionConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
undefined,
undefined,
'collectionConfig?.admin?.components?.views?.edit?.root',
)
? collectionConfig?.admin?.components?.views?.edit?.root?.Component
: null
if (!RootViewOverride) {
@@ -138,36 +141,21 @@ export const Document: React.FC<AdminViewProps> = async ({
routeSegments: segments,
})
CustomView = createMappedComponent(
collectionViews?.CustomView?.payloadComponent,
undefined,
collectionViews?.CustomView?.Component,
'collectionViews?.CustomView.payloadComponent',
)
DefaultView = createMappedComponent(
collectionViews?.DefaultView?.payloadComponent,
undefined,
collectionViews?.DefaultView?.Component,
'collectionViews?.DefaultView.payloadComponent',
)
ErrorView = createMappedComponent(
collectionViews?.ErrorView?.payloadComponent,
undefined,
collectionViews?.ErrorView?.Component,
'collectionViews?.ErrorView.payloadComponent',
)
CustomView = collectionViews?.CustomView
DefaultView = collectionViews?.DefaultView
ErrorView = collectionViews?.ErrorView
}
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
ErrorView = {
Component: NotFoundView,
}
}
}
if (globalConfig) {
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
notFound()
throw new Error('not-found')
}
const params = new URLSearchParams({
@@ -189,12 +177,7 @@ export const Document: React.FC<AdminViewProps> = async ({
RootViewOverride =
globalConfig?.admin?.components?.views?.edit?.root &&
'Component' in globalConfig.admin.components.views.edit.root
? createMappedComponent(
globalConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
undefined,
undefined,
'globalConfig?.admin?.components?.views?.edit?.root',
)
? globalConfig?.admin?.components?.views?.edit?.root?.Component
: null
if (!RootViewOverride) {
@@ -205,29 +188,14 @@ export const Document: React.FC<AdminViewProps> = async ({
routeSegments: segments,
})
CustomView = createMappedComponent(
globalViews?.CustomView?.payloadComponent,
undefined,
globalViews?.CustomView?.Component,
'globalViews?.CustomView.payloadComponent',
)
DefaultView = createMappedComponent(
globalViews?.DefaultView?.payloadComponent,
undefined,
globalViews?.DefaultView?.Component,
'globalViews?.DefaultView.payloadComponent',
)
ErrorView = createMappedComponent(
globalViews?.ErrorView?.payloadComponent,
undefined,
globalViews?.ErrorView?.Component,
'globalViews?.ErrorView.payloadComponent',
)
CustomView = globalViews?.CustomView
DefaultView = globalViews?.DefaultView
ErrorView = globalViews?.ErrorView
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
ErrorView = {
Component: NotFoundView,
}
}
}
}
@@ -240,13 +208,14 @@ export const Document: React.FC<AdminViewProps> = async ({
hasSavePermission &&
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
const validateDraftData =
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
const doc = await payload.create({
collection: collectionSlug,
data: {},
data: initialData || {},
depth: 0,
draft: true,
fallbackLocale: null,
@@ -263,57 +232,93 @@ export const Document: React.FC<AdminViewProps> = async ({
})
redirect(redirectURL)
} else {
notFound()
throw new Error('not-found')
}
}
return (
<DocumentInfoProvider
apiURL={apiURL}
collectionSlug={collectionConfig?.slug}
disableActions={false}
docPermissions={docPermissions}
globalSlug={globalConfig?.slug}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={id}
initialData={data}
initialState={formState}
isEditing={isEditing}
key={locale?.code}
>
{!RootViewOverride && (
<DocumentHeader
collectionConfig={collectionConfig}
globalConfig={globalConfig}
i18n={i18n}
payload={payload}
permissions={permissions}
/>
)}
<HydrateAuthProvider permissions={permissions} />
{/**
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
*
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
*/}
<EditDepthProvider
depth={1}
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
const documentSlots = renderDocumentSlots({
collectionConfig,
globalConfig,
hasSavePermission,
importMap,
payload,
permissions,
})
const clientProps = { formState, ...documentSlots }
return {
data,
Document: (
<DocumentInfoProvider
apiURL={apiURL}
BeforeDocument={
drawerSlug ? (
<DocumentDrawerHeader
drawerSlug={drawerSlug}
Header={null} // TODO
/>
) : undefined
}
collectionSlug={collectionConfig?.slug}
disableActions={disableActions ?? false}
docPermissions={docPermissions}
globalSlug={globalConfig?.slug}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={id}
initialData={data}
initialState={formState}
isEditing={isEditing}
key={locale?.code}
redirectAfterDelete={redirectAfterDelete}
redirectAfterDuplicate={redirectAfterDuplicate}
>
{ErrorView ? (
<RenderComponent mappedComponent={ErrorView} />
) : (
<RenderComponent
mappedComponent={
RootViewOverride ? RootViewOverride : CustomView ? CustomView : DefaultView
}
{!RootViewOverride && !drawerSlug && (
<DocumentHeader
collectionConfig={collectionConfig}
globalConfig={globalConfig}
i18n={i18n}
payload={payload}
permissions={permissions}
/>
)}
</EditDepthProvider>
</DocumentInfoProvider>
)
<HydrateAuthProvider permissions={permissions} />
<EditDepthProvider>
{ErrorView ? (
<RenderServerComponent
clientProps={clientProps}
Component={ErrorView.ComponentConfig || ErrorView.Component}
importMap={importMap}
serverProps={serverProps}
/>
) : (
<RenderServerComponent
clientProps={clientProps}
Component={
RootViewOverride
? RootViewOverride
: CustomView?.ComponentConfig || CustomView?.Component
? CustomView?.ComponentConfig || CustomView?.Component
: DefaultView?.ComponentConfig || DefaultView?.Component
}
importMap={importMap}
serverProps={serverProps}
/>
)}
</EditDepthProvider>
</DocumentInfoProvider>
),
}
}
export const Document: React.FC<AdminViewProps> = async (args) => {
try {
const { Document: RenderedDocument } = await renderDocument(args)
return RenderedDocument
} catch (error) {
if (error.message === 'not-found') {
notFound()
}
}
}

View File

@@ -1,33 +0,0 @@
'use client'
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
import { RenderComponent, SetViewActions, useConfig, useDocumentInfo } from '@payloadcms/ui'
import React, { Fragment } from 'react'
export const EditViewClient: React.FC = () => {
const { collectionSlug, globalSlug } = useDocumentInfo()
const { getEntityConfig } = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
const Edit = (collectionConfig || globalConfig)?.admin?.components?.views?.edit?.default
?.Component
if (!Edit) {
return null
}
return (
<Fragment>
<SetViewActions
actions={
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.default?.actions
}
/>
<RenderComponent mappedComponent={Edit} />
</Fragment>
)
}

View File

@@ -1,9 +1,10 @@
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
'use client'
import type { ClientSideEditViewProps } from 'payload'
import { DefaultEditView } from '@payloadcms/ui'
import React from 'react'
import { EditViewClient } from './index.client.js'
export const EditView: PayloadServerReactComponent<EditViewComponent> = () => {
return <EditViewClient />
export const EditView: React.FC<ClientSideEditViewProps> = (props) => {
return <DefaultEditView {...props} />
}

View File

@@ -1,259 +0,0 @@
'use client'
import type { ClientCollectionConfig } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import {
Button,
DeleteMany,
EditMany,
Gutter,
ListControls,
ListHeader,
ListSelection,
Pagination,
PerPage,
PublishMany,
RelationshipProvider,
RenderComponent,
SelectionProvider,
SetViewActions,
StaggeredShimmers,
Table,
UnpublishMany,
useAuth,
useBulkUpload,
useConfig,
useEditDepth,
useListInfo,
useListQuery,
useModal,
useStepNav,
useTranslation,
useWindowInfo,
ViewDescription,
} from '@payloadcms/ui'
import LinkImport from 'next/link.js'
import { useRouter } from 'next/navigation.js'
import { formatFilesize, isNumber } from 'payload/shared'
import React, { Fragment, useEffect } from 'react'
import './index.scss'
const baseClass = 'collection-list'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const DefaultListView: React.FC = () => {
const { user } = useAuth()
const {
beforeActions,
collectionSlug,
disableBulkDelete,
disableBulkEdit,
hasCreatePermission,
Header,
newDocumentURL,
} = useListInfo()
const router = useRouter()
const { data, defaultLimit, handlePageChange, handlePerPageChange, params } = useListQuery()
const { openModal } = useModal()
const { setCollectionSlug, setOnSuccess } = useBulkUpload()
const { drawerSlug } = useBulkUpload()
const { getEntityConfig } = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
const {
admin: {
components: {
afterList,
afterListTable,
beforeList,
beforeListTable,
Description,
views: {
list: { actions },
},
},
description,
},
fields,
labels,
} = collectionConfig
const { i18n, t } = useTranslation()
const drawerDepth = useEditDepth()
const { setStepNav } = useStepNav()
const {
breakpoints: { s: smallBreak },
} = useWindowInfo()
let docs = data.docs || []
const isUploadCollection = Boolean(collectionConfig.upload)
if (isUploadCollection) {
docs = docs?.map((doc) => {
return {
...doc,
filesize: formatFilesize(doc.filesize),
}
})
}
const openBulkUpload = React.useCallback(() => {
setCollectionSlug(collectionSlug)
openModal(drawerSlug)
setOnSuccess(() => router.refresh())
}, [router, collectionSlug, drawerSlug, openModal, setCollectionSlug, setOnSuccess])
useEffect(() => {
if (drawerDepth <= 1) {
setStepNav([
{
label: labels?.plural,
},
])
}
}, [setStepNav, labels, drawerDepth])
const isBulkUploadEnabled = isUploadCollection && collectionConfig.upload.bulkUpload
return (
<div className={`${baseClass} ${baseClass}--${collectionSlug}`}>
<SetViewActions actions={actions} />
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs} user={user}>
<RenderComponent mappedComponent={beforeList} />
<Gutter className={`${baseClass}__wrap`}>
{Header || (
<ListHeader heading={getTranslation(labels?.plural, i18n)}>
{hasCreatePermission && (
<>
<Button
aria-label={i18n.t('general:createNewLabel', {
label: getTranslation(labels?.singular, i18n),
})}
buttonStyle="pill"
el={'link'}
Link={Link}
size="small"
to={newDocumentURL}
>
{i18n.t('general:createNew')}
</Button>
{isBulkUploadEnabled && (
<Button
aria-label={t('upload:bulkUpload')}
buttonStyle="pill"
onClick={openBulkUpload}
size="small"
>
{t('upload:bulkUpload')}
</Button>
)}
</>
)}
{!smallBreak && (
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
)}
{(description || Description) && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription Description={Description} description={description} />
</div>
)}
</ListHeader>
)}
<ListControls collectionConfig={collectionConfig} fields={fields} />
<RenderComponent mappedComponent={beforeListTable} />
{!data.docs && (
<StaggeredShimmers
className={[`${baseClass}__shimmer`, `${baseClass}__shimmer--rows`].join(' ')}
count={6}
/>
)}
{data.docs && data.docs.length > 0 && (
<RelationshipProvider>
<Table
customCellContext={{
collectionSlug,
uploadConfig: collectionConfig.upload,
}}
data={docs}
fields={fields}
/>
</RelationshipProvider>
)}
{data.docs && data.docs.length === 0 && (
<div className={`${baseClass}__no-results`}>
<p>{i18n.t('general:noResults', { label: getTranslation(labels?.plural, i18n) })}</p>
{hasCreatePermission && newDocumentURL && (
<Button el="link" Link={Link} to={newDocumentURL}>
{i18n.t('general:createNewLabel', {
label: getTranslation(labels?.singular, i18n),
})}
</Button>
)}
</div>
)}
<RenderComponent mappedComponent={afterListTable} />
{data.docs && data.docs.length > 0 && (
<div className={`${baseClass}__page-controls`}>
<Pagination
hasNextPage={data.hasNextPage}
hasPrevPage={data.hasPrevPage}
limit={data.limit}
nextPage={data.nextPage}
numberOfNeighbors={1}
onChange={(page) => void handlePageChange(page)}
page={data.page}
prevPage={data.prevPage}
totalPages={data.totalPages}
/>
{data?.totalDocs > 0 && (
<Fragment>
<div className={`${baseClass}__page-info`}>
{data.page * data.limit - (data.limit - 1)}-
{data.totalPages > 1 && data.totalPages !== data.page
? data.limit * data.page
: data.totalDocs}{' '}
{i18n.t('general:of')} {data.totalDocs}
</div>
<PerPage
handleChange={(limit) => void handlePerPageChange(limit)}
limit={isNumber(params?.limit) ? Number(params.limit) : defaultLimit}
limits={collectionConfig?.admin?.pagination?.limits}
resetPage={data.totalDocs <= data.pagingCounter}
/>
{smallBreak && (
<div className={`${baseClass}__list-selection`}>
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
<div className={`${baseClass}__list-selection-actions`}>
{beforeActions && beforeActions}
{!disableBulkEdit && (
<Fragment>
<EditMany collection={collectionConfig} fields={fields} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
</Fragment>
)}
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
</div>
</div>
)}
</Fragment>
)}
</div>
)}
</Gutter>
<RenderComponent mappedComponent={afterList} />
</SelectionProvider>
</div>
)
}

View File

@@ -1,32 +1,58 @@
import type { AdminViewProps, ClientCollectionConfig, Where } from 'payload'
import type { ListPreferences, ListViewClientProps } from '@payloadcms/ui'
import type { AdminViewProps, Where } from 'payload'
import {
DefaultListView,
HydrateAuthProvider,
ListInfoProvider,
ListQueryProvider,
TableColumnsProvider,
} from '@payloadcms/ui'
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
import { createClientCollectionConfig } from '@payloadcms/ui/utilities/createClientConfig'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import { deepCopyObjectSimple, mergeListSearchAndWhere } from 'payload'
import { filterFields } from 'packages/ui/src/elements/TableColumns/filterFields.js'
import { getInitialColumns } from 'packages/ui/src/elements/TableColumns/getInitialColumns.js'
import { renderFilters, renderTable } from 'packages/ui/src/utilities/renderTable.js'
import { mergeListSearchAndWhere } from 'payload'
import { isNumber } from 'payload/shared'
import React, { Fragment } from 'react'
import type { ListPreferences } from './Default/types.js'
import { DefaultEditView } from '../Edit/Default/index.js'
import { DefaultListView } from './Default/index.js'
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
import { ListDrawerHeader } from '../../elements/ListDrawerHeader/index.js'
export { generateListMetadata } from './meta.js'
export const ListView: React.FC<AdminViewProps> = async ({
initPageResult,
params,
searchParams,
}) => {
type ListViewArgs = {
disableBulkDelete?: boolean
disableBulkEdit?: boolean
documentDrawerSlug: string
enableRowSelections: boolean
} & AdminViewProps
export const renderListView = async (
args: ListViewArgs,
): Promise<{
List: React.ReactNode
}> => {
const {
clientConfig,
disableBulkDelete,
disableBulkEdit,
documentDrawerSlug,
drawerSlug,
enableRowSelections,
initPageResult,
params,
searchParams,
} = args
const {
collectionConfig,
collectionConfig: {
slug: collectionSlug,
admin: { defaultColumns, useAsTitle },
defaultSort,
fields,
},
locale: fullLocale,
permissions,
req,
@@ -41,10 +67,8 @@ export const ListView: React.FC<AdminViewProps> = async ({
visibleEntities,
} = initPageResult
const collectionSlug = collectionConfig?.slug
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
notFound()
throw new Error('not-found')
}
let listPreferences: ListPreferences
@@ -79,7 +103,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
},
})
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
} catch (error) {} // eslint-disable-line no-empty
} catch (_err) {} // eslint-disable-line no-empty
const {
routes: { admin: adminRoute },
@@ -87,7 +111,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
if (collectionConfig) {
if (!visibleEntities.collections.includes(collectionSlug)) {
return notFound()
throw new Error('not-found')
}
const page = isNumber(query?.page) ? Number(query.page) : 0
@@ -98,9 +122,11 @@ export const ListView: React.FC<AdminViewProps> = async ({
where: (query?.where as Where) || undefined,
},
})
const limit = isNumber(query?.limit)
? Number(query.limit)
: listPreferences?.limit || collectionConfig.admin.pagination.defaultLimit
const sort =
query?.sort && typeof query.sort === 'string'
? query.sort
@@ -125,89 +151,124 @@ export const ListView: React.FC<AdminViewProps> = async ({
where: whereQuery || {},
})
const createMappedComponent = getCreateMappedComponent({
const initialColumns = getInitialColumns(filterFields(fields), useAsTitle, defaultColumns)
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
const { columnState, Table } = renderTable({
clientFields: clientCollectionConfig?.fields,
collectionSlug,
columnPreferences: listPreferences?.columns,
columns: initialColumns,
docs: data.docs,
drawerSlug,
enableRowSelections,
fields,
importMap: payload.importMap,
serverProps: {
collectionConfig,
collectionSlug,
data,
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
i18n,
limit,
listPreferences,
listSearchableFields: collectionConfig.admin.listSearchableFields,
locale: fullLocale,
newDocumentURL: formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/create`,
}),
params,
payload,
permissions,
searchParams,
user,
},
useAsTitle,
})
const ListComponent = createMappedComponent(
collectionConfig?.admin?.components?.views?.list?.Component,
undefined,
DefaultListView,
'collectionConfig?.admin?.components?.views?.list?.Component',
)
const renderedFilters = renderFilters(fields, req.payload.importMap)
let clientCollectionConfig = deepCopyObjectSimple(
collectionConfig,
) as unknown as ClientCollectionConfig
clientCollectionConfig = createClientCollectionConfig({
clientCollection: clientCollectionConfig,
collection: collectionConfig,
createMappedComponent,
DefaultEditView,
DefaultListView,
i18n,
importMap: payload.importMap,
payload,
})
const clientProps: ListViewClientProps = {
collectionSlug,
columnState,
listPreferences,
renderedFilters,
Table,
}
return (
<Fragment>
<HydrateAuthProvider permissions={permissions} />
<ListInfoProvider
collectionConfig={clientCollectionConfig}
collectionSlug={collectionSlug}
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
newDocumentURL={formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/create`,
})}
>
<ListQueryProvider
data={data}
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
defaultSort={sort}
modifySearchParams
preferenceKey={preferenceKey}
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create?.permission
return {
List: (
<Fragment>
<HydrateAuthProvider permissions={permissions} />
<ListInfoProvider
// beforeActions={
// enableRowSelections
// ? [<SelectMany key="select-many" onClick={onBulkSelect} />]
// : undefined
// }
collectionSlug={collectionSlug}
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
hasCreatePermission={hasCreatePermission}
Header={
drawerSlug ? (
<ListDrawerHeader
CustomDescription={
collectionConfig?.admin?.components?.Description ? (
<RenderServerComponent
Component={collectionConfig.admin.components.Description}
importMap={payload.importMap}
/>
) : undefined
}
description={clientCollectionConfig?.admin?.description}
documentDrawerSlug={documentDrawerSlug}
drawerSlug={drawerSlug}
hasCreatePermission={hasCreatePermission}
pluralLabel={clientCollectionConfig?.labels?.plural}
/>
) : null
}
newDocumentURL={formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/create`,
})}
>
<TableColumnsProvider
collectionSlug={collectionSlug}
enableRowSelections
listPreferences={listPreferences}
<ListQueryProvider
data={data}
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
defaultSort={sort}
modifySearchParams
preferenceKey={preferenceKey}
>
<RenderComponent
clientProps={{
<RenderServerComponent
clientProps={clientProps}
Component={collectionConfig?.admin?.components?.views?.list.Component}
Fallback={DefaultListView}
importMap={payload.importMap}
serverProps={{
collectionConfig,
collectionSlug,
listSearchableFields: collectionConfig?.admin?.listSearchableFields,
data,
hasCreatePermission:
permissions?.collections?.[collectionSlug]?.create?.permission,
i18n,
limit,
listPreferences,
listSearchableFields: collectionConfig.admin.listSearchableFields,
locale: fullLocale,
newDocumentURL: formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/create`,
}),
params,
payload,
permissions,
searchParams,
user,
}}
mappedComponent={ListComponent}
/>
</TableColumnsProvider>
</ListQueryProvider>
</ListInfoProvider>
</Fragment>
)
</ListQueryProvider>
</ListInfoProvider>
</Fragment>
),
}
}
return notFound()
throw new Error('not-found')
}
export const ListView: React.FC<ListViewArgs> = async (args) => {
try {
const { List: RenderedList } = await renderListView(args)
return RenderedList
} catch (error) {
if (error.message === 'not-found') {
notFound()
}
}
}

View File

@@ -15,27 +15,22 @@ import {
DocumentFields,
Form,
OperationProvider,
SetViewActions,
SetDocumentStepNav,
SetDocumentTitle,
useAuth,
useConfig,
useDocumentEvents,
useDocumentInfo,
useServerFunctions,
useTranslation,
} from '@payloadcms/ui'
import {
getFormState,
handleBackToDashboard,
handleGoBack,
handleTakeOver,
} from '@payloadcms/ui/shared'
import { handleBackToDashboard, handleGoBack, handleTakeOver } from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { DocumentLocked } from '../../elements/DocumentLocked/index.js'
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
import { useLivePreviewContext } from './Context/context.js'
import { LivePreviewProvider } from './Context/index.js'
import './index.scss'
@@ -55,13 +50,11 @@ type Props = {
}
const PreviewView: React.FC<Props> = ({
apiRoute,
collectionConfig,
config,
fields,
globalConfig,
schemaPath,
serverURL,
}) => {
const {
id,
@@ -92,6 +85,8 @@ const PreviewView: React.FC<Props> = ({
updateDocumentEditor,
} = useDocumentInfo()
const { getFormState } = useServerFunctions()
const operation = id ? 'update' : 'create'
const {
@@ -115,6 +110,8 @@ const PreviewView: React.FC<Props> = ({
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
const abortControllerRef = useRef(new AbortController())
const documentLockStateRef = useRef<{
hasShownLockedModal: boolean
isLocked: boolean
@@ -169,6 +166,17 @@ const PreviewView: React.FC<Props> = ({
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
if (abortControllerRef.current) {
try {
abortControllerRef.current.abort()
} catch (_err) {
// swallow error
}
}
const abortController = new AbortController()
abortControllerRef.current = abortController
const currentTime = Date.now()
const timeSinceLastUpdate = currentTime - lastUpdateTime
@@ -181,19 +189,16 @@ const PreviewView: React.FC<Props> = ({
const docPreferences = await getDocPreferences()
const { lockedState, state } = await getFormState({
apiRoute,
body: {
id,
collectionSlug,
docPreferences,
formState: prevFormState,
globalSlug,
operation,
returnLockStatus: isLockingEnabled ? true : false,
schemaPath,
updateLastEdited,
},
serverURL,
id,
collectionSlug,
docPreferences,
formState: prevFormState,
globalSlug,
operation,
returnLockStatus: isLockingEnabled ? true : false,
schemaPath: schemaPath ? schemaPath.split('.') : [],
signal: abortController.signal,
updateLastEdited,
})
setDocumentIsLocked(true)
@@ -202,8 +207,13 @@ const PreviewView: React.FC<Props> = ({
const previousOwnerId = documentLockStateRef.current?.user?.id
if (lockedState) {
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
const lockedUserID =
typeof lockedState.user === 'string' || typeof lockedState.user === 'number'
? lockedState.user
: lockedState.user.id
if (!documentLockStateRef.current || lockedUserID !== previousOwnerId) {
if (previousOwnerId === user.id && lockedUserID !== user.id) {
setShowTakeOverModal(true)
documentLockStateRef.current.hasShownLockedModal = true
}
@@ -211,9 +221,10 @@ const PreviewView: React.FC<Props> = ({
documentLockStateRef.current = documentLockStateRef.current = {
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
isLocked: true,
user: lockedState.user,
user: lockedState.user as ClientUser,
}
setCurrentEditor(lockedState.user)
setCurrentEditor(lockedState.user as ClientUser)
}
}
}
@@ -223,8 +234,6 @@ const PreviewView: React.FC<Props> = ({
[
collectionSlug,
globalSlug,
serverURL,
apiRoute,
id,
isLockingEnabled,
lastUpdateTime,
@@ -234,12 +243,21 @@ const PreviewView: React.FC<Props> = ({
setCurrentEditor,
setDocumentIsLocked,
user,
getFormState,
],
)
// Clean up when the component unmounts or when the document is unlocked
useEffect(() => {
return () => {
if (abortControllerRef.current) {
try {
abortControllerRef.current.abort()
} catch (_err) {
// swallow error
}
}
if (!isLockingEnabled) {
return
}
@@ -404,7 +422,6 @@ const PreviewView: React.FC<Props> = ({
fields={fields}
forceSidebarWrap
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
schemaPath={collectionSlug || globalSlug}
/>
{AfterDocument}
</div>
@@ -445,11 +462,6 @@ export const LivePreviewClient: React.FC<{
return (
<Fragment>
<SetViewActions
actions={
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.livePreview?.actions
}
/>
<LivePreviewProvider
breakpoints={breakpoints}
fieldSchema={collectionConfig?.fields || globalConfig?.fields}

View File

@@ -1,10 +1,10 @@
import type { AdminViewProps } from 'payload'
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
import { redirect } from 'next/navigation.js'
import React, { Fragment } from 'react'
import { Logo } from '../../elements/Logo/index.js'
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
import './index.scss'
import { LoginForm } from './LoginForm/index.js'
@@ -28,23 +28,6 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
routes: { admin },
} = config
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
const mappedBeforeLogins = createMappedComponent(beforeLogin, undefined, undefined, 'beforeLogin')
const mappedAfterLogins = createMappedComponent(afterLogin, undefined, undefined, 'afterLogin')
if (user) {
redirect(admin)
}
@@ -82,7 +65,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
user={user}
/>
</div>
<RenderComponent mappedComponent={mappedBeforeLogins} />
<RenderServerComponent
Component={beforeLogin}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
{!collectionConfig?.auth?.disableLocalStrategy && (
<LoginForm
prefillEmail={prefillEmail}
@@ -91,7 +86,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
searchParams={searchParams}
/>
)}
<RenderComponent mappedComponent={mappedAfterLogins} />
<RenderServerComponent
Component={afterLogin}
importMap={payload.importMap}
serverProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
</Fragment>
)
}

View File

@@ -1,4 +1,11 @@
import type { AdminViewComponent, AdminViewProps, ImportMap, SanitizedConfig } from 'payload'
import type {
AdminViewComponent,
AdminViewProps,
CustomComponent,
EditConfig,
ImportMap,
SanitizedConfig,
} from 'payload'
import type React from 'react'
import { formatAdminURL } from '@payloadcms/ui/shared'
@@ -46,6 +53,20 @@ const oneSegmentViews: OneSegmentViews = {
unauthorized: UnauthorizedView,
}
function getViewActions({
editConfig,
viewKey,
}: {
editConfig: EditConfig
viewKey: keyof EditConfig
}): CustomComponent[] | undefined {
if (viewKey in editConfig && 'actions' in editConfig[viewKey]) {
return editConfig[viewKey].actions
}
return undefined
}
export const getViewFromConfig = ({
adminRoute,
config,
@@ -65,8 +86,10 @@ export const getViewFromConfig = ({
}): {
DefaultView: ViewFromConfig
initPageOptions: Parameters<typeof initPage>[0]
serverProps: Record<string, unknown>
templateClassName: string
templateType: 'default' | 'minimal'
viewActions?: CustomComponent[]
} => {
let ViewToRender: ViewFromConfig = null
let templateClassName: string
@@ -79,10 +102,30 @@ export const getViewFromConfig = ({
searchParams,
}
const [segmentOne, segmentTwo] = segments
const viewActions: CustomComponent[] = config?.admin?.components?.actions || []
const [segmentOne, segmentTwo, segmentThree, segmentFour, segmentFive] = segments
const isGlobal = segmentOne === 'globals'
const isCollection = segmentOne === 'collections'
let matchedCollection: SanitizedConfig['collections'][number] = undefined
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
let serverProps = {}
if (isCollection) {
matchedCollection = config.collections.find(({ slug }) => slug === segmentTwo)
serverProps = {
collectionConfig: matchedCollection,
}
}
if (isGlobal) {
matchedGlobal = config.globals.find(({ slug }) => slug === segmentTwo)
serverProps = {
globalConfig: matchedGlobal,
}
}
switch (segments.length) {
case 0: {
@@ -146,7 +189,7 @@ export const getViewFromConfig = ({
templateType = 'minimal'
}
if (isCollection) {
if (isCollection && matchedCollection) {
// --> /collections/:collectionSlug
ViewToRender = {
@@ -155,7 +198,8 @@ export const getViewFromConfig = ({
templateClassName = `${segmentTwo}-list`
templateType = 'default'
} else if (isGlobal) {
viewActions.unshift(...(matchedCollection.admin.components?.views?.list?.actions || []))
} else if (isGlobal && matchedGlobal) {
// --> /globals/:globalSlug
ViewToRender = {
@@ -176,13 +220,13 @@ export const getViewFromConfig = ({
templateClassName = 'verify'
templateType = 'minimal'
} else if (isCollection) {
} else if (isCollection && matchedCollection) {
// Custom Views
// --> /collections/:collectionSlug/:id
// --> /collections/:collectionSlug/:id/api
// --> /collections/:collectionSlug/:id/preview
// --> /collections/:collectionSlug/:id/versions
// --> /collections/:collectionSlug/:id/versions/:versionId
// --> /collections/:collectionSlug/:id/api
ViewToRender = {
Component: DocumentView,
@@ -190,7 +234,65 @@ export const getViewFromConfig = ({
templateClassName = `collection-default-edit`
templateType = 'default'
} else if (isGlobal) {
// Adds view actions to the current collection view
if (matchedCollection.admin?.components?.views?.edit) {
if ('root' in matchedCollection.admin.components.views.edit) {
viewActions.unshift(
...getViewActions({
editConfig: matchedCollection.admin.components.views.edit,
viewKey: 'root',
}),
)
} else {
if (segmentFive) {
if (segmentFour === 'versions') {
// add version view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedCollection.admin.components.views.edit,
viewKey: 'version',
}),
)
}
} else if (segmentFour) {
if (segmentFour === 'versions') {
// add versions view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedCollection.admin.components.views.edit,
viewKey: 'versions',
}),
)
} else if (segmentFour === 'preview') {
// add livePreview view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedCollection.admin.components.views.edit,
viewKey: 'livePreview',
}),
)
} else if (segmentFour === 'api') {
// add api view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedCollection.admin.components.views.edit,
viewKey: 'api',
}),
)
}
} else if (segmentThree) {
// add default view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedCollection.admin.components.views.edit,
viewKey: 'default',
}),
)
}
}
}
} else if (isGlobal && matchedGlobal) {
// Custom Views
// --> /globals/:globalSlug/versions
// --> /globals/:globalSlug/preview
@@ -203,6 +305,64 @@ export const getViewFromConfig = ({
templateClassName = `global-edit`
templateType = 'default'
// Adds view actions to the current global view
if (matchedGlobal.admin?.components?.views?.edit) {
if ('root' in matchedGlobal.admin.components.views.edit) {
viewActions.unshift(
...getViewActions({
editConfig: matchedGlobal.admin.components.views.edit,
viewKey: 'root',
}),
)
} else {
if (segmentFour) {
if (segmentThree === 'versions') {
// add version view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedGlobal.admin.components.views.edit,
viewKey: 'version',
}),
)
}
} else if (segmentThree) {
if (segmentThree === 'versions') {
// add versions view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedGlobal.admin.components.views.edit,
viewKey: 'versions',
}),
)
} else if (segmentThree === 'preview') {
// add livePreview view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedGlobal.admin.components.views.edit,
viewKey: 'livePreview',
}),
)
} else if (segmentThree === 'api') {
// add api view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedGlobal.admin.components.views.edit,
viewKey: 'api',
}),
)
} else if (segmentTwo) {
// add default view actions
viewActions.unshift(
...getViewActions({
editConfig: matchedGlobal.admin.components.views.edit,
viewKey: 'default',
}),
)
}
}
}
}
}
break
}
@@ -214,7 +374,9 @@ export const getViewFromConfig = ({
return {
DefaultView: ViewToRender,
initPageOptions,
serverProps,
templateClassName,
templateType,
viewActions,
}
}

View File

@@ -1,13 +1,15 @@
import type { I18nClient } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { ImportMap, MappedComponent, SanitizedConfig } from 'payload'
import type { ImportMap, SanitizedConfig } from 'payload'
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { notFound, redirect } from 'next/navigation.js'
import React, { Fragment } from 'react'
import { DefaultTemplate } from '../../templates/Default/index.js'
import { MinimalTemplate } from '../../templates/Minimal/index.js'
import { getClientConfig } from '../../utilities/getClientConfig.js'
import { initPage } from '../../utilities/initPage/index.js'
import { getViewFromConfig } from './getViewFromConfig.js'
@@ -55,7 +57,14 @@ export const RootPage = async ({
const searchParams = await searchParamsPromise
const { DefaultView, initPageOptions, templateClassName, templateType } = getViewFromConfig({
const {
DefaultView,
initPageOptions,
serverProps,
templateClassName,
templateType,
viewActions,
} = getViewFromConfig({
adminRoute,
config,
currentRoute,
@@ -66,7 +75,7 @@ export const RootPage = async ({
let dbHasUser = false
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
if (!DefaultView) {
notFound()
}
@@ -102,27 +111,30 @@ export const RootPage = async ({
}
}
const createMappedView = getCreateMappedComponent({
importMap,
serverProps: {
i18n: initPageResult?.req.i18n,
importMap,
initPageResult,
params,
payload: initPageResult?.req.payload,
searchParams,
},
const clientConfig = await getClientConfig({
config,
i18n: initPageResult?.req.i18n,
})
const MappedView: MappedComponent = createMappedView(
DefaultView.payloadComponent,
undefined,
DefaultView.Component,
'createMappedView',
const RenderedView = (
<RenderServerComponent
clientProps={{ clientConfig }}
Component={DefaultView.payloadComponent}
Fallback={DefaultView.Component}
importMap={importMap}
serverProps={{
...serverProps,
clientConfig,
i18n: initPageResult?.req.i18n,
importMap,
initPageResult,
params,
payload: initPageResult?.req.payload,
searchParams,
}}
/>
)
const RenderedView = <RenderComponent mappedComponent={MappedView} />
return (
<Fragment>
{!templateType && <Fragment>{RenderedView}</Fragment>}
@@ -138,6 +150,7 @@ export const RootPage = async ({
permissions={initPageResult?.permissions}
searchParams={searchParams}
user={initPageResult?.req.user}
viewActions={viewActions}
visibleEntities={{
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
// which this caused as soon as initPageResult.visibleEntities is passed in

View File

@@ -1,14 +1,7 @@
'use client'
import type { ClientCollectionConfig, ClientGlobalConfig, OptionObject } from 'payload'
import {
Gutter,
SetViewActions,
useConfig,
useDocumentInfo,
usePayloadAPI,
useTranslation,
} from '@payloadcms/ui'
import { Gutter, useConfig, useDocumentInfo, usePayloadAPI, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import React, { useState } from 'react'
@@ -80,11 +73,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
return (
<main className={baseClass}>
<SetViewActions
actions={
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.version?.actions
}
/>
<SetStepNav
collectionConfig={collectionConfig}
collectionSlug={collectionSlug}

View File

@@ -6,7 +6,6 @@ import {
LoadingOverlayToggle,
Pagination,
PerPage,
SetViewActions,
Table,
useConfig,
useDocumentInfo,
@@ -41,11 +40,6 @@ export const VersionsViewClient: React.FC<{
return (
<React.Fragment>
<SetViewActions
actions={
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.versions?.actions
}
/>
<LoadingOverlayToggle name="versions" show={!data} />
{versionCount === 0 && (
<div className={`${baseClass}__no-versions`}>

View File

@@ -1,11 +1,10 @@
import type { EditViewComponent, PaginatedDocs, PayloadServerReactComponent } from 'payload'
import { Gutter, ListQueryProvider } from '@payloadcms/ui'
import { Gutter, ListQueryProvider, SetDocumentStepNav } from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
import { isNumber } from 'payload/shared'
import React from 'react'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import { buildVersionColumns } from './buildColumns.js'
import { getLatestVersion } from './getLatestVersion.js'
import { VersionsViewClient } from './index.client.js'

View File

@@ -19,7 +19,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"src/withPayload.js" /* Include the withPayload.js file in the build */
],
, "../ui/src/utilities/renderFields.tsx" ],
"references": [
{ "path": "../payload" },
{ "path": "../ui" },

View File

@@ -4,8 +4,8 @@ import type { JSONSchema4 } from 'json-schema'
import type { ImportMap } from '../bin/generateImportMap/index.js'
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
import type { ValidationFieldError } from '../errors/ValidationError.js'
import type {
Field,
FieldAffectingData,
RichTextField,
RichTextFieldClient,
@@ -14,7 +14,7 @@ import type {
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { JsonObject, Payload, PayloadRequest, RequestContext } from '../types/index.js'
import type { RichTextFieldClientProps } from './fields/RichText.js'
import type { CreateMappedComponent } from './types.js'
import type { CreateMappedComponent, FieldSchemaMap } from './types.js'
export type AfterReadRichTextHookArgs<
TData extends TypeWithID = any,
@@ -89,7 +89,7 @@ export type BeforeChangeRichTextHookArgs<
duplicate?: boolean
errors?: { field: string; message: string }[]
errors?: ValidationFieldError[]
/** Only available in `beforeChange` field hooks */
mergeLocaleActions?: (() => Promise<void>)[]
/** A string relating to which operation the field type is currently executing within. */
@@ -205,9 +205,9 @@ type RichTextAdapterBase<
config: SanitizedConfig
field: RichTextField
i18n: I18n<any, any>
schemaMap: Map<string, Field[]>
schemaPath: string
}) => Map<string, Field[]>
schemaMap: FieldSchemaMap
schemaPath: string[]
}) => FieldSchemaMap
/**
* Like an afterRead hook, but runs only for the GraphQL resolver. For populating data, this should be used, as afterRead hooks do not have a depth in graphQL.
*

View File

@@ -3,8 +3,15 @@ import type { ClientField } from '../../fields/config/client.js'
export type RowData = Record<string, any>
export type CellComponentProps<TField extends ClientField = ClientField> = {
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
readonly cellData: TCellData
// readonly cellProps?: Partial<CellComponentProps>
readonly className?: string
readonly columnIndex?: number
readonly customCellContext?: {
collectionSlug?: SanitizedCollectionConfig['slug']
uploadConfig?: SanitizedCollectionConfig['upload']
}
readonly field: TField
readonly link?: boolean
readonly onClick?: (args: {
@@ -12,13 +19,5 @@ export type CellComponentProps<TField extends ClientField = ClientField> = {
collectionSlug: SanitizedCollectionConfig['slug']
rowData: RowData
}) => void
}
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
readonly cellData: TCellData
readonly customCellContext?: {
collectionSlug?: SanitizedCollectionConfig['slug']
uploadConfig?: SanitizedCollectionConfig['upload']
}
readonly rowData: RowData
} & CellComponentProps<TField>
}

View File

@@ -1,6 +1,6 @@
import type { MarkOptional } from 'ts-essentials'
import type { ArrayField, ArrayFieldClient } from '../../fields/config/types.js'
import type { ArrayField, ArrayFieldClient, ClientField } from '../../fields/config/types.js'
import type { ArrayFieldValidation } from '../../fields/validations.js'
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
import type {
@@ -14,13 +14,13 @@ import type {
FieldDescriptionServerComponent,
FieldLabelClientComponent,
FieldLabelServerComponent,
MappedComponent,
} from '../types.js'
type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
type ArrayFieldBaseClientProps = {
readonly CustomRowLabel?: MappedComponent
readonly CustomRowLabel?: React.ReactNode
readonly path?: string
readonly validate?: ArrayFieldValidation
}

View File

@@ -1,6 +1,6 @@
import type { MarkOptional } from 'ts-essentials'
import type { BlocksField, BlocksFieldClient } from '../../fields/config/types.js'
import type { BlocksField, BlocksFieldClient, ClientField } from '../../fields/config/types.js'
import type { BlocksFieldValidation } from '../../fields/validations.js'
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
import type {
@@ -19,6 +19,7 @@ import type {
type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
type BlocksFieldBaseClientProps = {
readonly path?: string
readonly validate?: BlocksFieldValidation
}

View File

@@ -24,6 +24,7 @@ type CheckboxFieldBaseClientProps = {
readonly id?: string
readonly onChange?: (value: boolean) => void
readonly partialChecked?: boolean
readonly path?: string
readonly validate?: CheckboxFieldValidation
}

View File

@@ -20,7 +20,8 @@ type CodeFieldClientWithoutType = MarkOptional<CodeFieldClient, 'type'>
type CodeFieldBaseClientProps = {
readonly autoComplete?: string
readonly valiCode?: CodeFieldValidation
readonly path?: string
readonly validate?: CodeFieldValidation
}
export type CodeFieldClientProps = ClientFieldBase<CodeFieldClientWithoutType> &

View File

@@ -15,9 +15,14 @@ import type {
FieldLabelServerComponent,
} from '../types.js'
type CollapsibleFieldBaseClientProps = {
readonly path?: string
}
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType>
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType> &
CollapsibleFieldBaseClientProps
export type CollapsibleFieldServerProps = ServerFieldBase<
CollapsibleField,
@@ -29,8 +34,10 @@ export type CollapsibleFieldServerComponent = FieldServerComponent<
CollapsibleFieldClientWithoutType
>
export type CollapsibleFieldClientComponent =
FieldClientComponent<CollapsibleFieldClientWithoutType>
export type CollapsibleFieldClientComponent = FieldClientComponent<
CollapsibleFieldClientWithoutType,
CollapsibleFieldBaseClientProps
>
export type CollapsibleFieldLabelServerComponent = FieldLabelServerComponent<
CollapsibleField,

View File

@@ -19,6 +19,7 @@ import type {
type DateFieldClientWithoutType = MarkOptional<DateFieldClient, 'type'>
type DateFieldBaseClientProps = {
readonly path?: string
readonly validate?: DateFieldValidation
}

View File

@@ -20,6 +20,7 @@ type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
type EmailFieldBaseClientProps = {
readonly autoComplete?: string
readonly path?: string
readonly validate?: EmailFieldValidation
}

View File

@@ -17,7 +17,12 @@ import type {
type GroupFieldClientWithoutType = MarkOptional<GroupFieldClient, 'type'>
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType>
export type GroupFieldBaseClientProps = {
readonly path?: string
}
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType> &
GroupFieldBaseClientProps
export type GroupFieldServerProps = ServerFieldBase<GroupField, GroupFieldClientWithoutType>
@@ -26,7 +31,10 @@ export type GroupFieldServerComponent = FieldServerComponent<
GroupFieldClientWithoutType
>
export type GroupFieldClientComponent = FieldClientComponent<GroupFieldClientWithoutType>
export type GroupFieldClientComponent = FieldClientComponent<
GroupFieldClientWithoutType,
GroupFieldBaseClientProps
>
export type GroupFieldLabelServerComponent = FieldLabelServerComponent<
GroupField,

View File

@@ -5,7 +5,7 @@ export type HiddenFieldProps = {
readonly disableModifyingForm?: false
readonly field?: {
readonly name?: string
} & Pick<ClientField, '_path'>
} & ClientField
readonly forceUsePathFromProps?: boolean
readonly value?: unknown
} & FormFieldBase

View File

@@ -19,6 +19,7 @@ import type {
type JSONFieldClientWithoutType = MarkOptional<JSONFieldClient, 'type'>
type JSONFieldBaseClientProps = {
readonly path?: string
readonly validate?: JSONFieldValidation
}

View File

@@ -17,7 +17,9 @@ import type {
type JoinFieldClientWithoutType = MarkOptional<JoinFieldClient, 'type'>
export type JoinFieldClientProps = ClientFieldBase<JoinFieldClientWithoutType>
export type JoinFieldClientProps = {
path?: string
} & ClientFieldBase<JoinFieldClientWithoutType>
export type JoinFieldServerProps = ServerFieldBase<JoinField>

View File

@@ -20,6 +20,7 @@ type NumberFieldClientWithoutType = MarkOptional<NumberFieldClient, 'type'>
type NumberFieldBaseClientProps = {
readonly onChange?: (e: number) => void
readonly path?: string
readonly validate?: NumberFieldValidation
}

View File

@@ -19,6 +19,7 @@ import type {
type PointFieldClientWithoutType = MarkOptional<PointFieldClient, 'type'>
type PointFieldBaseClientProps = {
readonly path?: string
readonly validate?: PointFieldValidation
}

View File

@@ -24,6 +24,7 @@ type RadioFieldBaseClientProps = {
*/
readonly disableModifyingForm?: boolean
readonly onChange?: OnChange
readonly path?: string
readonly validate?: RadioFieldValidation
readonly value?: string
}

View File

@@ -19,6 +19,7 @@ import type {
type RelationshipFieldClientWithoutType = MarkOptional<RelationshipFieldClient, 'type'>
type RelationshipFieldBaseClientProps = {
readonly path?: string
readonly validate?: RelationshipFieldValidation
}

View File

@@ -23,6 +23,7 @@ type RichTextFieldBaseClientProps<
TAdapterProps = any,
TExtraProperties = object,
> = {
readonly path?: string
readonly validate?: RichTextFieldValidation
}

View File

@@ -20,7 +20,6 @@ type RowFieldClientWithoutType = MarkOptional<RowFieldClient, 'type'>
type RowFieldBaseClientProps = {
readonly forceRender?: boolean
readonly indexPath: string
}
export type RowFieldClientProps = ClientFieldBase<RowFieldClientWithoutType> &

View File

@@ -20,6 +20,7 @@ type SelectFieldClientWithoutType = MarkOptional<SelectFieldClient, 'type'>
type SelectFieldBaseClientProps = {
readonly onChange?: (e: string | string[]) => void
readonly path?: string
readonly validate?: SelectFieldValidation
readonly value?: string
}

View File

@@ -22,8 +22,8 @@ import type {
} from '../types.js'
export type ClientTab =
| ({ fields: ClientField[] } & Omit<NamedTab, 'fields'>)
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
| ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>)
type TabsFieldClientWithoutType = MarkOptional<TabsFieldClient, 'type'>

View File

@@ -22,6 +22,7 @@ type TextFieldClientWithoutType = MarkOptional<TextFieldClient, 'type'>
type TextFieldBaseClientProps = {
readonly inputRef?: React.RefObject<HTMLInputElement>
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
readonly path?: string
readonly validate?: TextFieldValidation
}

View File

@@ -22,6 +22,7 @@ type TextareaFieldClientWithoutType = MarkOptional<TextareaFieldClient, 'type'>
type TextareaFieldBaseClientProps = {
readonly inputRef?: React.Ref<HTMLInputElement>
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
readonly path?: string
readonly validate?: TextareaFieldValidation
}

View File

@@ -19,6 +19,7 @@ import type {
type UploadFieldClientWithoutType = MarkOptional<UploadFieldClient, 'type'>
type UploadFieldBaseClientProps = {
readonly path?: string
readonly validate?: UploadFieldValidation
}

View File

@@ -1,6 +1,5 @@
import type { LabelFunction, ServerProps } from '../../config/types.js'
import type { Field } from '../../fields/config/types.js'
import type { MappedComponent } from '../types.js'
import type { ClientFieldWithOptionalType } from './Field.js'
export type DescriptionFunction = LabelFunction
@@ -20,9 +19,9 @@ export type Description = DescriptionFunction | StaticDescription
export type GenericDescriptionProps = {
readonly className?: string
readonly Description?: MappedComponent
readonly description?: StaticDescription
readonly marginPlacement?: 'bottom' | 'top'
readonly path: string
}
export type FieldDescriptionServerProps<

View File

@@ -1,11 +1,9 @@
import type { ServerProps } from '../../config/types.js'
import type { Field } from '../../fields/config/types.js'
import type { MappedComponent } from '../types.js'
import type { ClientFieldWithOptionalType } from './Field.js'
export type GenericErrorProps = {
readonly alignCaret?: 'center' | 'left' | 'right'
readonly CustomError?: MappedComponent
readonly message?: string
readonly path?: string
readonly showError?: boolean

View File

@@ -1,50 +1,48 @@
import type { I18nClient } from '@payloadcms/translations'
import type { MarkOptional } from 'ts-essentials'
import type { User } from '../../auth/types.js'
import type { Locale, ServerProps } from '../../config/types.js'
import type { ClientField, Field, Validate } from '../../fields/config/types.js'
import type { DocumentPreferences } from '../../preferences/types.js'
import type { FieldDescriptionClientProps, FieldDescriptionServerProps } from './Description.js'
import type { FieldErrorClientProps, FieldErrorServerProps } from './Error.js'
import type { FieldLabelClientProps, FieldLabelServerProps } from './Label.js'
import type { FieldPermissions } from '../../auth/types.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { ClientBlock, ClientField, Field } from '../../fields/config/types.js'
import type { Payload } from '../../types/index.js'
import type { ClientTab, FormField, RenderedField } from '../types.js'
export type ClientFieldWithOptionalType = MarkOptional<ClientField, 'type'>
export type ClientComponentProps = {
field: ClientBlock | ClientField | ClientTab
fieldState: FormField
forceRender?: boolean
path: string
permissions: FieldPermissions
readOnly?: boolean
renderedBlocks?: RenderedField[]
rowLabels?: React.ReactNode[]
schemaPath: string
}
export type ServerComponentProps = {
clientField: ClientBlock | ClientField | ClientTab
config: SanitizedConfig
field: Field
i18n: I18nClient
payload: Payload
}
export type ClientFieldBase<
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
> = {
readonly descriptionProps?: FieldDescriptionClientProps<TFieldClient>
readonly errorProps?: FieldErrorClientProps<TFieldClient>
readonly field: TFieldClient
readonly labelProps?: FieldLabelClientProps<TFieldClient>
} & FormFieldBase
} & Omit<ClientComponentProps, 'field'>
export type ServerFieldBase<
TFieldServer extends Field = Field,
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
> = {
readonly clientField: TFieldClient
readonly descriptionProps?: FieldDescriptionServerProps<TFieldServer, TFieldClient>
readonly errorProps?: FieldErrorServerProps<TFieldServer, TFieldClient>
readonly field: TFieldServer
readonly labelProps?: FieldLabelServerProps<TFieldServer, TFieldClient>
} & FormFieldBase &
Partial<ServerProps>
export type FormFieldBase = {
readonly docPreferences?: DocumentPreferences
/**
* `forceRender` is added by RenderField automatically.
*/
readonly forceRender?: boolean
readonly locale?: Locale
/**
* `readOnly` is added by RenderField automatically. This should be used instead of `field.admin.readOnly`.
*/
readonly readOnly?: boolean
readonly user?: User
readonly validate?: Validate
}
} & Omit<ClientComponentProps, 'field'> &
Omit<ServerComponentProps, 'clientField' | 'field'>
export type FieldClientComponent<
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,

View File

@@ -1,5 +1,8 @@
import type { Field, Validate } from '../../fields/config/types.js'
import type { Where } from '../../types/index.js'
import { type SupportedLanguages } from '@payloadcms/translations'
import type { Field } from '../../fields/config/types.js'
import type { DocumentPreferences } from '../../preferences/types.js'
import type { PayloadRequest, Where } from '../../types/index.js'
export type Data = {
[key: string]: any
@@ -15,20 +18,71 @@ export type FilterOptionsResult = {
[relation: string]: boolean | Where
}
export type FormField = {
export type FieldState = {
customComponents?: {
AfterInput?: React.ReactNode
BeforeInput?: React.ReactNode
Description?: React.ReactNode
Error?: React.ReactNode
Field?: React.ReactNode
Label?: React.ReactNode
}
disableFormData?: boolean
errorMessage?: string
errorPaths?: string[]
fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue: unknown
isSidebar?: boolean
passesCondition?: boolean
rows?: Row[]
schemaPath: string[]
valid: boolean
validate?: Validate
value: unknown
}
export type FieldStateWithoutComponents = Omit<FieldState, 'customComponents'>
export type FormState = {
[path: string]: FormField
[path: string]: FieldState
}
export type FormStateWithoutComponents = {
[path: string]: FieldStateWithoutComponents
}
export type BuildFormStateArgs = {
data?: Data
docPreferences?: DocumentPreferences
formState?: FormState
id?: number | string
/*
If not i18n was passed, the language can be passed to init i18n
*/
language?: keyof SupportedLanguages
locale?: string
operation?: 'create' | 'update'
/*
Used as a "base path" when adding form state to nested fields
*/
path?: (number | string)[]
/*
If true, will render field components within their state object
*/
renderFields?: boolean
req: PayloadRequest
returnLockStatus?: boolean
schemaPath: string[]
updateLastEdited?: boolean
} & (
| {
collectionSlug: string
// Do not type it as never. This still makes it so that either collectionSlug or globalSlug is required, but makes it easier to provide both collectionSlug and globalSlug if it's
// unclear which one is actually available.
globalSlug?: string
}
| {
collectionSlug?: string
globalSlug: string
}
)

View File

@@ -1,15 +1,14 @@
import type { ServerProps, StaticLabel } from '../../config/types.js'
import type { Field } from '../../fields/config/types.js'
import type { MappedComponent } from '../types.js'
import type { ClientFieldWithOptionalType } from './Field.js'
export type GenericLabelProps = {
readonly as?: 'label' | 'span'
readonly hideLocale?: boolean
readonly htmlFor?: string
readonly Label?: MappedComponent
readonly label?: StaticLabel
readonly localized?: boolean
readonly path?: string
readonly required?: boolean
readonly unstyled?: boolean
}

View File

@@ -0,0 +1,44 @@
import type { ImportMap } from '../../bin/generateImportMap/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import { PaginatedDocs } from '../../database/types.js'
import type { PayloadRequest } from '../../types/index.js'
export type DefaultServerFunctionArgs = {
importMap: ImportMap
req: PayloadRequest
}
export type ServerFunctionArgs = {
args: Record<string, unknown>
name: string
}
export type ServerFunctionClientArgs = {
args: Record<string, unknown>
name: string
}
export type ServerFunctionClient = (args: ServerFunctionClientArgs) => Promise<unknown> | unknown
export type ServerFunction = (
args: DefaultServerFunctionArgs & ServerFunctionClientArgs['args'],
) => Promise<unknown> | unknown
export type ServerFunctionConfig = {
fn: ServerFunction
name: string
}
export type ServerFunctionHandler = (
args: {
config: Promise<SanitizedConfig> | SanitizedConfig
importMap: ImportMap
} & ServerFunctionClientArgs,
) => Promise<unknown>
export type BuildTableStateArgs = {
collectionSlug: string
columns?: any[] // TODO: type this (comes from ui pkg)
docs: PaginatedDocs['docs']
req: PayloadRequest
}

View File

@@ -1,9 +1,24 @@
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
import type React from 'react'
import type { PayloadComponent } from '../config/types.js'
import type { ImportMap } from '../bin/generateImportMap/index.js'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { Block, ClientField, Field, FieldTypes, Tab } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { JsonObject } from '../types/index.js'
import type {
BuildFormStateArgs,
Data,
FieldState,
FieldStateWithoutComponents,
FilterOptionsResult,
FormState,
FormStateWithoutComponents,
Row,
} from './forms/Form.js'
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
export type { DefaultCellComponentProps } from './elements/Cell.js'
export type { ConditionalDateProps } from './elements/DatePicker.js'
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
export type { CustomPreviewButton } from './elements/PreviewButton.js'
@@ -318,9 +333,26 @@ export type {
GenericErrorProps,
} from './forms/Error.js'
export type { FormFieldBase, ServerFieldBase } from './forms/Field.js'
export type {
ClientComponentProps,
ClientFieldBase,
ClientFieldWithOptionalType,
FieldClientComponent,
FieldServerComponent,
ServerComponentProps,
ServerFieldBase,
} from './forms/Field.js'
export type { Data, FilterOptionsResult, FormField, FormState, Row } from './forms/Form.js'
export type {
BuildFormStateArgs,
Data,
FieldState as FormField,
FieldStateWithoutComponents as FormFieldWithoutComponents,
FilterOptionsResult,
FormState,
FormStateWithoutComponents,
Row,
}
export type {
FieldLabelClientComponent,
@@ -333,25 +365,19 @@ export type {
export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
export type {
BuildTableStateArgs,
DefaultServerFunctionArgs,
ServerFunction,
ServerFunctionArgs,
ServerFunctionClient,
ServerFunctionClientArgs,
ServerFunctionConfig,
ServerFunctionHandler,
} from './functions/index.js'
export type { LanguageOptions } from './LanguageOptions.js'
export type {
RichTextAdapter,
RichTextAdapterProvider,
RichTextGenerateComponentMap,
RichTextHooks,
} from './RichText.js'
export type {
AdminViewComponent,
AdminViewConfig,
AdminViewProps,
EditViewProps,
InitPageResult,
ServerSideEditViewProps,
VisibleEntities,
} from './views/types.js'
export type MappedServerComponent<TComponentClientProps extends JsonObject = JsonObject> = {
Component?: React.ComponentType<TComponentClientProps>
props?: Partial<any>
@@ -370,30 +396,91 @@ export type MappedEmptyComponent = {
type: 'empty'
}
export type MappedComponent<TComponentClientProps extends JsonObject = JsonObject> =
| MappedClientComponent<TComponentClientProps>
| MappedEmptyComponent
| MappedServerComponent<TComponentClientProps>
| undefined
export type CreateMappedComponent = {
<T extends JsonObject>(
component: { Component: React.FC<T> } | null | PayloadComponent<T>,
props: {
clientProps?: JsonObject
serverProps?: object
},
fallback: React.FC,
identifier: string,
): MappedComponent<T>
<T extends JsonObject>(
components: ({ Component: React.FC<T> } | PayloadComponent<T>)[],
props: {
clientProps?: JsonObject
serverProps?: object
},
fallback: React.FC,
identifier: string,
): MappedComponent<T>[]
export enum Action {
RenderConfig = 'render-config',
}
export type RenderEntityConfigArgs = {
collectionSlug?: string
data?: Data
globalSlug?: string
}
export type RenderRootConfigArgs = {}
export type RenderFieldConfigArgs = {
collectionSlug?: string
formState?: FormState
globalSlug?: string
schemaPath: string
}
export type RenderConfigArgs = {
action: Action.RenderConfig
config: Promise<SanitizedConfig> | SanitizedConfig
i18n: I18nClient
importMap: ImportMap
languageCode: AcceptedLanguages
serverProps?: any
} & (RenderEntityConfigArgs | RenderFieldConfigArgs | RenderRootConfigArgs)
export type PayloadServerAction = (
args:
| {
[key: string]: any
action: Action
i18n: I18nClient
}
| RenderConfigArgs,
) => Promise<string>
export type RenderedField = {
Field: React.ReactNode
indexPath?: string
initialSchemaPath?: string
isSidebar: boolean
path: string
schemaPath: string
type: FieldTypes
}
export type FieldRow = {
RowLabel?: React.ReactNode
}
export type DocumentSlots = {
PreviewButton?: React.ReactNode
PublishButton?: React.ReactNode
SaveButton?: React.ReactNode
SaveDraftButton?: React.ReactNode
Upload?: React.ReactNode
}
export type {
RichTextAdapter,
RichTextAdapterProvider,
RichTextGenerateComponentMap,
RichTextHooks,
} from './RichText.js'
export type {
AdminViewComponent,
AdminViewConfig,
AdminViewProps,
ClientSideEditViewProps,
EditViewProps,
InitPageResult,
ServerSideEditViewProps,
VisibleEntities,
} from './views/types.js'
type SchemaPath = {} & string
export type FieldSchemaMap = Map<
SchemaPath,
| {
fields: Field[]
}
| Block
| Field
| Tab
>

View File

@@ -8,7 +8,7 @@ import type { Locale, MetaConfig, PayloadComponent } from '../../config/types.js
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
import type { PayloadRequest } from '../../types/index.js'
import type { LanguageOptions } from '../LanguageOptions.js'
import type { MappedComponent } from '../types.js'
import type { Data, DocumentSlots, PayloadServerAction } from '../types.js'
export type AdminViewConfig = {
Component: AdminViewComponent
@@ -20,17 +20,17 @@ export type AdminViewConfig = {
strict?: boolean
}
export type MappedView = {
actions?: MappedComponent[]
Component: MappedComponent
}
export type AdminViewProps = {
readonly clientConfig: ClientConfig
readonly disableActions?: boolean
readonly drawerSlug?: string
readonly importMap: ImportMap
readonly initialData?: Data
readonly initPageResult: InitPageResult
readonly params?: { [key: string]: string | string[] | undefined }
readonly searchParams: { [key: string]: string | string[] | undefined }
readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean
}
export type AdminViewComponent = PayloadComponent<AdminViewProps>
@@ -62,6 +62,9 @@ export type InitPageResult = {
export type ServerSideEditViewProps = {
readonly initPageResult: InitPageResult
readonly params: { [key: string]: string | string[] | undefined }
readonly payloadServerAction: PayloadServerAction
readonly routeSegments: string[]
readonly searchParams: { [key: string]: string | string[] | undefined }
}
} & ClientSideEditViewProps
export type ClientSideEditViewProps = {} & DocumentSlots

View File

@@ -1,15 +1,14 @@
import type { Payload } from '../../../index.js'
import type { PayloadRequest } from '../../../types/index.js'
import type { AuthArgs, AuthResult } from '../auth.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js'
import { auth as authOperation } from '../auth.js'
export const auth = async (payload: Payload, options: AuthArgs): Promise<AuthResult> => {
const { headers } = options
const { headers, req } = options
return await authOperation({
headers,
req: await createLocalReq({ req: options.req as PayloadRequest }, payload),
req: await createLocalReq({ req }, payload),
})
}

View File

@@ -1,14 +1,14 @@
import type { PayloadComponent } from '../../config/types.js'
export function parsePayloadComponent(payloadComponent: PayloadComponent): {
export function parsePayloadComponent(PayloadComponent: PayloadComponent): {
exportName: string
path: string
} {
if (!payloadComponent) {
if (!PayloadComponent) {
return null
}
const pathAndMaybeExport =
typeof payloadComponent === 'string' ? payloadComponent : payloadComponent.path
typeof PayloadComponent === 'string' ? PayloadComponent : PayloadComponent.path
let path = ''
let exportName = 'default'
@@ -19,8 +19,8 @@ export function parsePayloadComponent(payloadComponent: PayloadComponent): {
path = pathAndMaybeExport
}
if (typeof payloadComponent === 'object' && payloadComponent.exportName) {
exportName = payloadComponent.exportName
if (typeof PayloadComponent === 'object' && PayloadComponent.exportName) {
exportName = PayloadComponent.exportName
}
return { exportName, path }

View File

@@ -1,9 +1,18 @@
import type { MappedComponent, StaticDescription } from '../../admin/types.js'
import type { MappedView } from '../../admin/views/types.js'
import type { LivePreviewConfig, ServerOnlyLivePreviewProperties } from '../../config/types.js'
import type { I18nClient } from '@payloadcms/translations'
import type { StaticDescription } from '../../admin/types.js'
import type {
LivePreviewConfig,
ServerOnlyLivePreviewProperties,
StaticLabel,
} from '../../config/types.js'
import type { ClientField } from '../../fields/config/client.js'
import type { Payload } from '../../types/index.js'
import type { SanitizedCollectionConfig } from './types.js'
import { createClientFields } from '../../fields/config/client.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
export type ServerOnlyCollectionProperties = keyof Pick<
SanitizedCollectionConfig,
'access' | 'custom' | 'endpoints' | 'hooks' | 'joins'
@@ -26,34 +35,7 @@ export type ServerOnlyUploadProperties = keyof Pick<
export type ClientCollectionConfig = {
_isPreviewEnabled?: true
admin: {
components: {
afterList: MappedComponent[]
afterListTable: MappedComponent[]
beforeList: MappedComponent[]
beforeListTable: MappedComponent[]
Description: MappedComponent
edit: {
PreviewButton: MappedComponent
PublishButton: MappedComponent
SaveButton: MappedComponent
SaveDraftButton: MappedComponent
Upload: MappedComponent
}
views: {
edit: {
[key: string]: MappedView
api: MappedView
default: MappedView
livePreview: MappedView
version: MappedView
versions: MappedView
}
list: {
actions: MappedComponent[]
Component: MappedComponent
}
}
}
components: null
description?: StaticDescription
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
} & Omit<
@@ -61,4 +43,156 @@ export type ClientCollectionConfig = {
'components' | 'description' | 'joins' | 'livePreview' | ServerOnlyCollectionAdminProperties
>
fields: ClientField[]
} & Omit<SanitizedCollectionConfig, 'admin' | 'fields' | ServerOnlyCollectionProperties>
labels?: {
plural: StaticLabel
singular: StaticLabel
}
} & Omit<SanitizedCollectionConfig, 'admin' | 'fields' | 'labels' | ServerOnlyCollectionProperties>
const serverOnlyCollectionProperties: Partial<ServerOnlyCollectionProperties>[] = [
'hooks',
'access',
'endpoints',
'custom',
'joins',
// `upload`
// `admin`
// are all handled separately
]
const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
'adminThumbnail',
'externalFileHeaderFilter',
'handlers',
'modifyResponseHeaders',
'withMetadata',
]
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
'hidden',
'preview',
// `livePreview` is handled separately
]
export const createClientCollectionConfig = ({
collection,
defaultIDType,
i18n,
}: {
collection: SanitizedCollectionConfig
defaultIDType: Payload['config']['db']['defaultIDType']
i18n: I18nClient
}): ClientCollectionConfig => {
const clientCollection = deepCopyObjectSimple(collection) as unknown as ClientCollectionConfig
clientCollection.fields = createClientFields({
clientFields: clientCollection?.fields || [],
defaultIDType,
fields: collection.fields,
i18n,
parentSchemaPath: [collection.slug],
})
serverOnlyCollectionProperties.forEach((key) => {
if (key in clientCollection) {
delete clientCollection[key]
}
})
if ('upload' in clientCollection && typeof clientCollection.upload === 'object') {
serverOnlyUploadProperties.forEach((key) => {
if (key in clientCollection.upload) {
delete clientCollection.upload[key]
}
})
if ('imageSizes' in clientCollection.upload && clientCollection.upload.imageSizes.length) {
clientCollection.upload.imageSizes = clientCollection.upload.imageSizes.map((size) => {
const sanitizedSize = { ...size }
if ('generateImageName' in sanitizedSize) {
delete sanitizedSize.generateImageName
}
return sanitizedSize
})
}
}
if ('auth' in clientCollection && typeof clientCollection.auth === 'object') {
delete clientCollection.auth.strategies
delete clientCollection.auth.forgotPassword
delete clientCollection.auth.verify
}
if (collection.labels) {
Object.entries(collection.labels).forEach(([labelType, collectionLabel]) => {
if (typeof collectionLabel === 'function') {
clientCollection.labels[labelType] = collectionLabel({ t: i18n.t })
}
})
}
if (collection.admin.preview) {
clientCollection._isPreviewEnabled = true
}
if (!clientCollection.admin) {
clientCollection.admin = {} as ClientCollectionConfig['admin']
}
serverOnlyCollectionAdminProperties.forEach((key) => {
if (key in clientCollection.admin) {
delete clientCollection.admin[key]
}
})
clientCollection.admin.components = null
let description = undefined
if (collection.admin?.description) {
if (
typeof collection.admin?.description === 'string' ||
typeof collection.admin?.description === 'object'
) {
description = collection.admin.description
} else if (typeof collection.admin?.description === 'function') {
description = collection.admin?.description({ t: i18n.t })
}
}
clientCollection.admin.description = description
if (
'livePreview' in clientCollection.admin &&
clientCollection.admin.livePreview &&
'url' in clientCollection.admin.livePreview
) {
delete clientCollection.admin.livePreview.url
}
return clientCollection
}
export const createClientCollectionConfigs = ({
collections,
defaultIDType,
i18n,
}: {
collections: SanitizedCollectionConfig[]
defaultIDType: Payload['config']['db']['defaultIDType']
i18n: I18nClient
}): ClientCollectionConfig[] => {
const clientCollections = new Array(collections.length)
for (let i = 0; i < collections.length; i++) {
const collection = collections[i]
clientCollections[i] = createClientCollectionConfig({
collection,
defaultIDType,
i18n,
})
}
return clientCollections
}

View File

@@ -1,12 +1,18 @@
import type { MappedComponent } from '../admin/types.js'
import type { ClientCollectionConfig } from '../collections/config/client.js'
import type { ClientGlobalConfig } from '../globals/config/client.js'
import type { I18nClient } from '@payloadcms/translations'
import type {
LivePreviewConfig,
SanitizedConfig,
ServerOnlyLivePreviewProperties,
} from './types.js'
import {
type ClientCollectionConfig,
createClientCollectionConfigs,
} from '../collections/config/client.js'
import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/config/client.js'
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
export type ServerOnlyRootProperties = keyof Pick<
SanitizedConfig,
| 'bin'
@@ -27,20 +33,15 @@ export type ServerOnlyRootProperties = keyof Pick<
| 'typescript'
>
export type ServerOnlyRootAdminProperties = keyof Pick<SanitizedConfig['admin'], 'components'>
export type ServerOnlyRootAdminProperties = keyof Pick<
SanitizedConfig['admin'],
'components' | 'serverFunctions'
>
export type ClientConfig = {
admin: {
components: {
actions?: MappedComponent[]
Avatar: MappedComponent
graphics: {
Icon: MappedComponent
Logo: MappedComponent
}
LogoutButton?: MappedComponent
}
dependencies?: Record<string, MappedComponent>
components: null
dependencies?: Record<string, React.ReactNode>
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
collections: ClientCollectionConfig[]
@@ -48,6 +49,10 @@ export type ClientConfig = {
globals: ClientGlobalConfig[]
} & Omit<SanitizedConfig, 'admin' | 'collections' | 'globals' | ServerOnlyRootProperties>
export const serverOnlyAdminConfigProperties: readonly Partial<ServerOnlyRootAdminProperties>[] = [
'serverFunctions',
]
export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperties>[] = [
'endpoints',
'db',
@@ -64,6 +69,57 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
'email',
'custom',
'graphQL',
'logger'
'logger',
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
]
export const createClientConfig = ({
config,
i18n,
}: {
config: SanitizedConfig
i18n: I18nClient
}): ClientConfig => {
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
const clientConfig = deepCopyObjectSimple(config) as unknown as ClientConfig
for (const key of serverOnlyConfigProperties) {
if (key in clientConfig) {
delete clientConfig[key]
}
}
if ('localization' in clientConfig && clientConfig.localization) {
for (const locale of clientConfig.localization.locales) {
delete locale.toString
}
}
if (!clientConfig.admin) {
clientConfig.admin = {} as ClientConfig['admin']
}
clientConfig.admin.components = null
if (
'livePreview' in clientConfig.admin &&
clientConfig.admin.livePreview &&
'url' in clientConfig.admin.livePreview
) {
delete clientConfig.admin.livePreview.url
}
clientConfig.collections = createClientCollectionConfigs({
collections: config.collections,
defaultIDType: config.db.defaultIDType,
i18n,
})
clientConfig.globals = createClientGlobalConfigs({
defaultIDType: config.db.defaultIDType,
globals: config.globals,
i18n,
})
return clientConfig
}

View File

@@ -14,8 +14,12 @@ import type { default as sharp } from 'sharp'
import type { DeepRequired } from 'ts-essentials'
import type { RichTextAdapterProvider } from '../admin/RichText.js'
import type { DocumentTabConfig, RichTextAdapter } from '../admin/types.js'
import type { AdminViewConfig, ServerSideEditViewProps } from '../admin/views/types.js'
import type { DocumentTabConfig, RichTextAdapter, ServerFunctionConfig } from '../admin/types.js'
import type {
AdminViewConfig,
ServerSideEditViewProps,
VisibleEntities,
} from '../admin/views/types.js'
import type { Permissions } from '../auth/index.js'
import type {
AddToImportMap,
@@ -384,16 +388,20 @@ export type EditViewConfig = {
}
)
type ClientProps = {
readonly [key: string]: unknown
}
export type ServerProps = {
readonly i18n: I18nClient
readonly locale?: Locale
readonly params?: { [key: string]: string | string[] | undefined }
readonly payload: Payload
readonly permissions?: Permissions
readonly [key: string]: unknown
readonly searchParams?: { [key: string]: string | string[] | undefined }
readonly user?: TypedUser
}
readonly visibleEntities?: VisibleEntities
} & ClientProps
export const serverProps: (keyof ServerProps)[] = [
'payload',
@@ -818,6 +826,7 @@ export type Config = {
/** The route for the unauthorized page. */
unauthorized?: string
}
serverFunctions?: ServerFunctionConfig[]
/**
* Restrict the Admin Panel theme to use only one of your choice
*
@@ -1078,44 +1087,43 @@ export type SanitizedConfig = {
'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
>
export type EditConfig =
| {
[key: string]: EditViewConfig
/**
* Replace or modify individual nested routes, or add new ones:
* + `default` - `/admin/collections/:collection/:id`
* + `api` - `/admin/collections/:collection/:id/api`
* + `livePreview` - `/admin/collections/:collection/:id/preview`
* + `references` - `/admin/collections/:collection/:id/references`
* + `relationships` - `/admin/collections/:collection/:id/relationships`
* + `versions` - `/admin/collections/:collection/:id/versions`
* + `version` - `/admin/collections/:collection/:id/versions/:version`
* + `customView` - `/admin/collections/:collection/:id/:path`
*
* To override the entire Edit View including all nested views, use the `root` key.
*/
api?: Partial<EditViewConfig>
default?: Partial<EditViewConfig>
livePreview?: Partial<EditViewConfig>
root?: never
version?: Partial<EditViewConfig>
versions?: Partial<EditViewConfig>
// TODO: uncomment these as they are built
// references?: EditView
// relationships?: EditView
}
| {
api?: never
default?: never
livePreview?: never
/**
* Replace or modify _all_ nested document views and routes, including the document header, controls, and tabs. This cannot be used in conjunction with other nested views.
* + `root` - `/admin/collections/:collection/:id/**\/*`
*/
root: Partial<EditViewConfig>
version?: never
versions?: never
}
export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot
export type EditConfigWithRoot = {
api?: never
default?: never
livePreview?: never
/**
* Replace or modify _all_ nested document views and routes, including the document header, controls, and tabs. This cannot be used in conjunction with other nested views.
* + `root` - `/admin/collections/:collection/:id/**\/*`
*/
root: Partial<EditViewConfig>
version?: never
versions?: never
}
export type EditConfigWithoutRoot = {
[key: string]: EditViewConfig
/**
* Replace or modify individual nested routes, or add new ones:
* + `default` - `/admin/collections/:collection/:id`
* + `api` - `/admin/collections/:collection/:id/api`
* + `livePreview` - `/admin/collections/:collection/:id/preview`
* + `references` - `/admin/collections/:collection/:id/references`
* + `relationships` - `/admin/collections/:collection/:id/relationships`
* + `versions` - `/admin/collections/:collection/:id/versions`
* + `version` - `/admin/collections/:collection/:id/versions/:version`
* + `customView` - `/admin/collections/:collection/:id/:path`
*
* To override the entire Edit View including all nested views, use the `root` key.
*/
api?: Partial<EditViewConfig>
default?: Partial<EditViewConfig>
livePreview?: Partial<EditViewConfig>
root?: never
version?: Partial<EditViewConfig>
versions?: Partial<EditViewConfig>
}
export type EntityDescriptionComponent = CustomComponent

View File

@@ -9,8 +9,8 @@ import { APIError } from './APIError.js'
export let ValidationErrorName = 'ValidationError'
export type ValidationFieldError = {
// The field path, i.e. "textField", "groupField.subTextField", etc.
field: string
fieldPath: (number | string)[]
fieldSchemaPath: string[]
// The error message to display for this field
message: string
}
@@ -36,7 +36,7 @@ export class ValidationError extends APIError<{
: en.translations.error.followingFieldsInvalid_other
super(
`${message} ${results.errors.map((f) => f.field).join(', ')}`,
`${message} ${results.errors.map((f) => f.fieldPath.join('.')).join(', ')}`,
httpStatus.BAD_REQUEST,
results,
)

View File

@@ -28,6 +28,9 @@ export {
tabHasName,
valueIsValueWithRelation,
} from '../fields/config/types.js'
export { getFieldPaths } from '../fields/getFieldPaths.js'
export * from '../fields/validations.js'
export { validOperators } from '../types/constants.js'

View File

@@ -1,4 +1,24 @@
import type { ClientField, FieldBase } from '../../fields/config/types.js'
import type { I18nClient } from '@payloadcms/translations'
import type { Payload } from '../../types/index.js'
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
import {
type AdminClient,
type BlocksFieldClient,
type ClientBlock,
type ClientField,
type Field,
fieldAffectsData,
type FieldBase,
fieldIsPresentationalOnly,
type LabelsClient,
type RadioFieldClient,
type RowFieldClient,
type SelectFieldClient,
type TabsFieldClient,
} from '../../fields/config/types.js'
import { getFieldPaths } from '../getFieldPaths.js'
// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
// to accidentally use it instead of ClientField and get confused
@@ -16,3 +36,332 @@ export type ServerOnlyFieldProperties =
| keyof Pick<FieldBase, 'access' | 'custom' | 'defaultValue' | 'hooks'>
export type ServerOnlyFieldAdminProperties = keyof Pick<FieldBase['admin'], 'condition'>
export const createClientField = ({
clientField = {} as ClientField,
defaultIDType,
field: incomingField,
i18n,
schemaPath,
}: {
clientField?: ClientField
defaultIDType: Payload['config']['db']['defaultIDType']
field: Field
i18n: I18nClient
schemaPath: string[]
}): ClientField => {
const serverOnlyFieldProperties: Partial<ServerOnlyFieldProperties>[] = [
'hooks',
'access',
'validate',
'defaultValue',
'filterOptions', // This is a `relationship` and `upload` only property
'editor', // This is a `richText` only property
'custom',
'typescriptSchema',
'dbName', // can be a function
'enumName', // can be a function
// the following props are handled separately (see below):
// `label`
// `fields`
// `blocks`
// `tabs`
// `admin`
]
clientField._schemaPath = schemaPath
clientField.admin = clientField.admin || {}
// clientField.admin.readOnly = true
serverOnlyFieldProperties.forEach((key) => {
if (key in clientField) {
delete clientField[key]
}
})
if (fieldIsPresentationalOnly(incomingField)) {
clientField._isPresentational = true
}
const isHidden = 'hidden' in incomingField && incomingField?.hidden
const disabledFromAdmin =
incomingField?.admin && 'disabled' in incomingField.admin && incomingField.admin.disabled
if (fieldAffectsData(clientField) && (isHidden || disabledFromAdmin)) {
return null
}
if (
'label' in clientField &&
'label' in incomingField &&
typeof incomingField.label === 'function'
) {
clientField.label = incomingField.label({ t: i18n.t })
}
if (!(clientField.admin instanceof Object)) {
clientField.admin = {} as AdminClient
}
if ('admin' in incomingField && 'width' in incomingField.admin) {
clientField.admin.style = {
...clientField.admin.style,
'--field-width': clientField.admin.width,
width: undefined, // avoid needlessly adding this to the element's style attribute
}
} else {
if (!(clientField.admin.style instanceof Object)) {
clientField.admin.style = {}
}
clientField.admin.style.flex = '1 1 auto'
}
switch (incomingField.type) {
case 'array':
case 'group':
case 'collapsible':
case 'row': {
const field = clientField as unknown as RowFieldClient
if (!field.fields) {
field.fields = []
}
field.fields = createClientFields({
clientFields: field.fields,
defaultIDType,
disableAddingID: incomingField.type !== 'array',
fields: incomingField.fields,
i18n,
parentSchemaPath: schemaPath,
})
break
}
case 'blocks': {
const field = clientField as unknown as BlocksFieldClient
if (incomingField.blocks?.length) {
for (let i = 0; i < incomingField.blocks.length; i++) {
const block = incomingField.blocks[i]
const clientBlock: ClientBlock = {
slug: block.slug,
admin: {
components: {},
custom: block.admin?.custom,
},
fields: field.blocks?.[i]?.fields || [],
imageAltText: block.imageAltText,
imageURL: block.imageURL,
}
if (block.labels) {
clientBlock.labels = {} as unknown as LabelsClient
if (block.labels.singular) {
if (typeof block.labels.singular === 'function') {
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
} else {
clientBlock.labels.singular = block.labels.singular
}
if (typeof block.labels.plural === 'function') {
clientBlock.labels.plural = block.labels.plural({ t: i18n.t })
} else {
clientBlock.labels.plural = block.labels.plural
}
}
}
clientBlock.fields = createClientFields({
clientFields: clientBlock.fields,
defaultIDType,
fields: block.fields,
i18n,
parentSchemaPath: [...schemaPath, block.slug],
})
if (!field.blocks) {
field.blocks = []
}
field.blocks[i] = clientBlock
}
}
break
}
case 'richText': {
if (!incomingField?.editor) {
throw new MissingEditorProp(incomingField) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof incomingField?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
break
}
case 'tabs': {
const field = clientField as unknown as TabsFieldClient
if (incomingField.tabs?.length) {
for (let i = 0; i < incomingField.tabs.length; i++) {
const tab = incomingField.tabs[i]
const clientTab = field.tabs[i]
serverOnlyFieldProperties.forEach((key) => {
if (key in clientTab) {
delete clientTab[key]
}
})
clientTab.fields = createClientFields({
clientFields: clientTab.fields,
defaultIDType,
disableAddingID: true,
fields: tab.fields,
i18n,
parentSchemaPath: getFieldPaths({
field: {
...tab,
type: 'tab',
},
parentPath: [],
parentSchemaPath: schemaPath,
schemaIndex: i,
}).schemaPath,
})
}
}
break
}
case 'select':
case 'radio': {
const field = clientField as RadioFieldClient | SelectFieldClient
if (incomingField.options?.length) {
for (let i = 0; i < incomingField.options.length; i++) {
const option = incomingField.options[i]
if (typeof option === 'object' && typeof option.label === 'function') {
if (!field.options) {
field.options = []
}
field.options[i] = {
label: option.label({ t: i18n.t }),
value: option.value,
}
}
}
}
break
}
default:
break
}
const serverOnlyFieldAdminProperties: Partial<ServerOnlyFieldAdminProperties>[] = ['condition']
if (!clientField.admin) {
clientField.admin = {} as AdminClient
}
serverOnlyFieldAdminProperties.forEach((key) => {
if (key in clientField.admin) {
delete clientField.admin[key]
}
})
type FieldWithDescription = {
admin: AdminClient
} & ClientField
if (incomingField.admin && 'description' in incomingField.admin) {
if (
typeof incomingField.admin?.description === 'string' ||
typeof incomingField.admin?.description === 'object'
) {
;(clientField as FieldWithDescription).admin.description = incomingField.admin.description
} else if (typeof incomingField.admin?.description === 'function') {
;(clientField as FieldWithDescription).admin.description = incomingField.admin?.description({
t: i18n.t,
})
}
}
return clientField
}
export const createClientFields = ({
clientFields,
defaultIDType,
disableAddingID,
fields,
i18n,
parentSchemaPath = [],
}: {
clientFields: ClientField[]
defaultIDType: Payload['config']['db']['defaultIDType']
disableAddingID?: boolean
fields: Field[]
i18n: I18nClient
parentSchemaPath?: string[]
}): ClientField[] => {
const newClientFields: ClientField[] = []
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
const { schemaPath } = getFieldPaths({
field,
parentPath: [],
parentSchemaPath,
schemaIndex: i,
})
const newField = createClientField({
clientField: clientFields[i],
defaultIDType,
field,
i18n,
schemaPath,
})
if (newField) {
newClientFields.push(newField)
}
}
const hasID = newClientFields.findIndex((f) => fieldAffectsData(f) && f.name === 'id') > -1
if (!disableAddingID && !hasID) {
newClientFields.push({
name: 'id',
type: defaultIDType,
_schemaPath: getFieldPaths({
field: { name: 'id', type: 'text' },
parentPath: [],
parentSchemaPath,
schemaIndex: 0,
}).schemaPath,
admin: {
description: 'The unique identifier for this document',
disableBulkEdit: true,
hidden: true,
},
hidden: true,
label: 'ID',
localized: undefined,
})
}
return newClientFields
}

View File

@@ -14,6 +14,7 @@ import {
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
import { baseIDField } from '../baseFields/baseIDField.js'
import { getFieldPaths } from '../getFieldPaths.js'
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
import validations from '../validations.js'
import { sanitizeJoinField } from './sanitizeJoinField.js'
@@ -41,7 +42,7 @@ type Args = {
* so that you can sanitize them together, after the config has been sanitized.
*/
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>
schemaPath?: string
schemaPath?: string[]
/**
* If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array.
* This validation will be skipped if validRelationships is null.
@@ -64,7 +65,7 @@ export const sanitizeFields = async ({
parentIsLocalized,
requireFieldLevelRichTextEditor = false,
richTextSanitizationPromises,
schemaPath = '',
schemaPath = [],
validRelationships,
}: Args): Promise<Field[]> => {
if (!fields) {
@@ -260,10 +261,12 @@ export const sanitizeFields = async ({
parentIsLocalized: parentIsLocalized || field.localized,
requireFieldLevelRichTextEditor,
richTextSanitizationPromises,
schemaPath: generateSchemaPath({
name: 'name' in field ? field.name : undefined,
path: schemaPath,
}),
schemaPath: getFieldPaths({
field,
parentPath: [],
parentSchemaPath: schemaPath,
schemaIndex: i,
}).schemaPath,
validRelationships,
})
}
@@ -271,10 +274,8 @@ export const sanitizeFields = async ({
if (field.type === 'tabs') {
for (let j = 0; j < field.tabs.length; j++) {
const tab = field.tabs[j]
if (tabHasName(tab)) {
if (typeof tab.label === 'undefined') {
tab.label = toWords(tab.name)
}
if (tabHasName(tab) && typeof tab.label === 'undefined') {
tab.label = toWords(tab.name)
}
tab.fields = await sanitizeFields({
@@ -285,10 +286,15 @@ export const sanitizeFields = async ({
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
requireFieldLevelRichTextEditor,
richTextSanitizationPromises,
schemaPath: generateSchemaPath({
name: 'name' in tab ? tab.name : undefined,
path: schemaPath,
}),
schemaPath: getFieldPaths({
field: {
...tab,
type: 'tab',
},
parentPath: [],
parentSchemaPath: schemaPath,
schemaIndex: j,
}).schemaPath,
validRelationships,
})
field.tabs[j] = tab

View File

@@ -14,7 +14,7 @@ export const sanitizeJoinField = ({
config: Config
field: JoinField
joins?: SanitizedJoins
schemaPath?: string
schemaPath?: string[]
}) => {
// the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field
if (typeof joins === 'undefined') {
@@ -25,7 +25,7 @@ export const sanitizeJoinField = ({
}
const join: SanitizedJoin = {
field,
schemaPath: `${schemaPath || ''}${schemaPath ? '.' : ''}${field.name}`,
schemaPath: [...schemaPath, field.name].join('.'),
targetField: undefined,
}
const joinCollection = config.collections.find(

View File

@@ -59,7 +59,6 @@ import type {
JSONFieldErrorServerComponent,
JSONFieldLabelClientComponent,
JSONFieldLabelServerComponent,
MappedComponent,
NumberFieldClientProps,
NumberFieldErrorClientComponent,
NumberFieldErrorServerComponent,
@@ -95,6 +94,7 @@ import type {
TextareaFieldErrorServerComponent,
TextareaFieldLabelClientComponent,
TextareaFieldLabelServerComponent,
TextFieldClientProps,
TextFieldErrorClientComponent,
TextFieldErrorServerComponent,
TextFieldLabelClientComponent,
@@ -299,15 +299,6 @@ type Admin = {
export type AdminClient = {
className?: string
components?: {
Cell?: MappedComponent
Description?: MappedComponent
Field?: MappedComponent
/**
* The Filter component has to be a client component
*/
Filter?: MappedComponent
}
/** Extension point to add your custom data. Available in server and client. */
custom?: Record<string, any>
description?: StaticDescription
@@ -431,8 +422,7 @@ export interface FieldBase {
export interface FieldBaseClient {
_isPresentational?: undefined
_path?: string
_schemaPath?: string
_schemaPath: string[]
admin?: AdminClient
hidden?: boolean
index?: boolean
@@ -498,15 +488,7 @@ export type NumberField = {
Omit<FieldBase, 'validate'>
export type NumberFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<NumberField['admin'], 'autoComplete' | 'placeholder' | 'step'>
admin?: AdminClient & Pick<NumberField['admin'], 'autoComplete' | 'placeholder' | 'step'>
} & FieldBaseClient &
Pick<NumberField, 'hasMany' | 'max' | 'maxRows' | 'min' | 'minRows' | 'type'>
@@ -548,15 +530,7 @@ export type TextField = {
Omit<FieldBase, 'validate'>
export type TextFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<TextField['admin'], 'autoComplete' | 'placeholder' | 'rtl'>
admin?: AdminClient & Pick<TextField['admin'], 'autoComplete' | 'placeholder' | 'rtl'>
} & FieldBaseClient &
Pick<TextField, 'hasMany' | 'maxLength' | 'maxRows' | 'minLength' | 'minRows' | 'type'>
@@ -576,15 +550,7 @@ export type EmailField = {
} & Omit<FieldBase, 'validate'>
export type EmailFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<EmailField['admin'], 'placeholder'>
admin?: AdminClient & Pick<EmailField['admin'], 'placeholder'>
} & FieldBaseClient &
Pick<EmailField, 'type'>
@@ -607,15 +573,7 @@ export type TextareaField = {
} & Omit<FieldBase, 'validate'>
export type TextareaFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<TextareaField['admin'], 'placeholder' | 'rows' | 'rtl'>
admin?: AdminClient & Pick<TextareaField['admin'], 'placeholder' | 'rows' | 'rtl'>
} & FieldBaseClient &
Pick<TextareaField, 'maxLength' | 'minLength' | 'type'>
@@ -633,14 +591,7 @@ export type CheckboxField = {
} & Omit<FieldBase, 'validate'>
export type CheckboxFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient
admin?: AdminClient
} & FieldBaseClient &
Pick<CheckboxField, 'type'>
@@ -660,15 +611,7 @@ export type DateField = {
} & Omit<FieldBase, 'validate'>
export type DateFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<DateField['admin'], 'date' | 'placeholder'>
admin?: AdminClient & Pick<DateField['admin'], 'date' | 'placeholder'>
} & FieldBaseClient &
Pick<DateField, 'type'>
@@ -692,12 +635,7 @@ export type GroupField = {
} & Omit<FieldBase, 'required' | 'validate'>
export type GroupFieldClient = {
admin?: {
components?: {
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<GroupField['admin'], 'hideGutter'>
admin?: AdminClient & Pick<GroupField['admin'], 'hideGutter'>
fields: ClientField[]
} & Omit<FieldBaseClient, 'required'> &
Pick<GroupField, 'interfaceName' | 'type'>
@@ -745,25 +683,12 @@ export type CollapsibleField = {
Omit<FieldBase, 'label' | 'name' | 'validate' | 'virtual'>
export type CollapsibleFieldClient = {
admin?: {
initCollapsed?: boolean
} & AdminClient
fields: ClientField[]
} & (
| {
admin: {
components: {
RowLabel: MappedComponent
} & AdminClient['components']
initCollapsed?: boolean
} & AdminClient
label?: Required<FieldBaseClient['label']>
}
| {
admin?: {
initCollapsed?: boolean
} & AdminClient
label: Required<FieldBaseClient['label']>
}
) &
Omit<FieldBaseClient, 'label' | 'name' | 'validate'> &
label: StaticLabel
} & Omit<FieldBaseClient, 'label' | 'name' | 'validate'> &
Pick<CollapsibleField, 'type'>
type TabBase = {
@@ -856,14 +781,9 @@ export type UIField = {
export type UIFieldClient = {
_isPresentational?: true
// still include FieldBaseClient.admin (even if it's undefinable) so that we don't need constant type checks (e.g. if('xy' in field))
// eslint-disable-next-line perfectionist/sort-intersection-types
admin: DeepUndefinable<FieldBaseClient['admin']> & {
components?: {
Cell?: MappedComponent
Field: MappedComponent
Filter?: MappedComponent
} & AdminClient['components']
} & Pick<
admin: DeepUndefinable<FieldBaseClient['admin']> &
Pick<
UIField['admin'],
'custom' | 'disableBulkEdit' | 'disableListColumn' | 'position' | 'width'
>
@@ -933,13 +853,8 @@ type UploadAdmin = {
} & Admin['components']
isSortable?: boolean
} & Admin
type UploadAdminClient = {
components?: {
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
type UploadAdminClient = AdminClient & Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
export type PolymorphicUploadField = {
admin?: {
@@ -989,15 +904,7 @@ export type CodeField = {
} & Omit<FieldBase, 'admin' | 'validate'>
export type CodeFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<CodeField['admin'], 'editorOptions' | 'language'>
admin?: AdminClient & Pick<CodeField['admin'], 'editorOptions' | 'language'>
} & Omit<FieldBaseClient, 'admin'> &
Pick<CodeField, 'maxLength' | 'minLength' | 'type'>
@@ -1022,15 +929,7 @@ export type JSONField = {
} & Omit<FieldBase, 'admin' | 'validate'>
export type JSONFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<JSONField['admin'], 'editorOptions'>
admin?: AdminClient & Pick<JSONField['admin'], 'editorOptions'>
} & Omit<FieldBaseClient, 'admin'> &
Pick<JSONField, 'jsonSchema' | 'type'>
@@ -1069,15 +968,7 @@ export type SelectField = {
Omit<FieldBase, 'validate'>
export type SelectFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
admin?: AdminClient & Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
} & FieldBaseClient &
Pick<SelectField, 'hasMany' | 'options' | 'type'>
@@ -1142,12 +1033,7 @@ type RelationshipAdmin = {
isSortable?: boolean
} & Admin
type RelationshipAdminClient = {
components?: {
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
type RelationshipAdminClient = AdminClient &
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
export type PolymorphicRelationshipField = {
@@ -1226,13 +1112,6 @@ export type RichTextFieldClient<
TAdapterProps = any,
TExtraProperties = object,
> = {
admin?: {
components?: {
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
placeholder?: Record<string, string> | string
} & AdminClient
richTextComponentMap?: Map<string, any>
} & FieldBaseClient &
Pick<RichTextField<TValue, TAdapterProps, TExtraProperties>, 'maxDepth' | 'type'> &
@@ -1271,14 +1150,7 @@ export type ArrayField = {
} & Omit<FieldBase, 'validate'>
export type ArrayFieldClient = {
admin?: {
components?: {
Error?: MappedComponent
Label?: MappedComponent
RowLabel?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<ArrayField['admin'], 'initCollapsed' | 'isSortable'>
admin?: AdminClient & Pick<ArrayField['admin'], 'initCollapsed' | 'isSortable'>
fields: ClientField[]
labels?: LabelsClient
} & FieldBaseClient &
@@ -1306,13 +1178,7 @@ export type RadioField = {
} & Omit<FieldBase, 'validate'>
export type RadioFieldClient = {
admin?: {
components?: {
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<RadioField['admin'], 'layout'>
admin?: AdminClient & Pick<RadioField['admin'], 'layout'>
} & FieldBaseClient &
Pick<RadioField, 'options' | 'type'>
@@ -1361,10 +1227,11 @@ export type Block = {
labels?: Labels
slug: string
}
export type ClientBlock = {
admin?: {
components?: {
Label?: MappedComponent
Label?: React.ReactNode
}
} & Pick<Block['admin'], 'custom'>
fields: ClientField[]
@@ -1392,12 +1259,7 @@ export type BlocksField = {
} & Omit<FieldBase, 'validate'>
export type BlocksFieldClient = {
admin?: {
components?: {
Error?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<BlocksField['admin'], 'initCollapsed' | 'isSortable'>
admin?: AdminClient & Pick<BlocksField['admin'], 'initCollapsed' | 'isSortable'>
blocks: ClientBlock[]
labels?: LabelsClient
} & FieldBaseClient &
@@ -1419,15 +1281,7 @@ export type PointField = {
} & Omit<FieldBase, 'validate'>
export type PointFieldClient = {
admin?: {
components?: {
afterInput?: MappedComponent[]
beforeInput?: MappedComponent[]
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<PointField['admin'], 'placeholder' | 'step'>
admin?: AdminClient & Pick<PointField['admin'], 'placeholder' | 'step'>
} & FieldBaseClient &
Pick<PointField, 'type'>
@@ -1473,12 +1327,7 @@ export type JoinField = {
} & FieldBase
export type JoinFieldClient = {
admin?: {
components?: {
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<JoinField['admin'], 'disableBulkEdit' | 'readOnly'>
admin?: AdminClient & Pick<JoinField['admin'], 'disableBulkEdit' | 'readOnly'>
} & FieldBaseClient &
Pick<JoinField, 'collection' | 'index' | 'maxDepth' | 'on' | 'type'>
@@ -1550,6 +1399,7 @@ export type ClientFieldProps =
| SelectFieldClientProps
| TabsFieldClientProps
| TextareaFieldClientProps
| TextFieldClientProps
| UploadFieldClientProps
type ExtractFieldTypes<T> = T extends { type: infer U } ? U : never

View File

@@ -1,23 +1,25 @@
import type { Field, TabAsField } from './config/types.js'
import type { ClientField, Field, TabAsField } from './config/types.js'
import { tabHasName } from './config/types.js'
export function getFieldPaths({
field,
parentPath,
parentSchemaPath,
parentPath = [],
parentSchemaPath = [],
schemaIndex,
}: {
field: Field | TabAsField
field: ClientField | Field | TabAsField
parentPath: (number | string)[]
parentSchemaPath: string[]
schemaIndex: number
}): {
path: (number | string)[]
schemaPath: string[]
} {
if (field.type === 'tabs' || field.type === 'row' || field.type === 'collapsible') {
return {
path: parentPath,
schemaPath: parentSchemaPath,
path: [...parentPath, `_index-${schemaIndex}`],
schemaPath: [...parentSchemaPath, `_index-${schemaIndex}`],
}
} else if (field.type === 'tab') {
if (tabHasName(field)) {
@@ -27,8 +29,8 @@ export function getFieldPaths({
}
} else {
return {
path: parentPath,
schemaPath: parentSchemaPath,
path: [...parentPath, `_index-${schemaIndex}`],
schemaPath: [...parentSchemaPath, `_index-${schemaIndex}`],
}
}
}

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