Compare commits

...

94 Commits

Author SHA1 Message Date
Sasha
0124afea55 feat: add KV storage adapters 2024-12-24 03:10:40 +02:00
Jacob Fletcher
466f109152 chore(live-preview): strongly types message events (#10148)
Live Preview message events were typed with the generic `MessageEvent`
interface without passing any of the Live Preview specific properties,
leading to unknown types upon use. To fix this, there is a new
`LivePreviewMessageEvent` which properly extends the underlying
`MessageEvent` interface, providing much needed type safety to these
functions. In the same vein, the `UpdatedDocument` type was not being
properly shared across packages, leading to multiple independent
definitions of this type. This type is now exported from `payload`
itself and renamed to `DocumentEvent` for improved semantics. Same with
the `FieldSchemaJSON` type. This PR also adjusts where globally scoped
variables are set, putting them within the shared `_payloadLivePreview`
namespace instead of setting them individually at the top-level.
2024-12-23 18:19:52 +00:00
Marcus Forsberg
0588394a47 docs: fix broken links to csrf and e-mail verification docs (#10140)
### What?

This fixes a couple of broken links, specifically to the CSRF and the
e-mail verification doc pages, which appear to have been moved from the
root Authentication page.

### Why?

While it makes sense to familiarize one self with the Authentication
Overview page as well, if you are specifically looking for info on CSRF
protection (which I was doing while evaluting Payload for my agency),
the link should go to the right place.
2024-12-23 03:09:50 -05:00
Germán Jabloñski
3bbc5bf949 chore(examples): localization example (#10134) 2024-12-22 19:21:56 +00:00
Germán Jabloñski
154ad9d132 fix(richtext-lexical): restore selection (#10129)
Fix #9964

Now we make sure that the node for the previous selection exists before
restoring it to avoid a runtime error.

I also optimized the performance of a function in the client feature.

In the future, we should centralize the insertion of all decorator
blocks in one place. There are several things to improve. For example,
currently an additional paragraph is inserted (in addition to the one
for the selection we delete).
2024-12-22 15:18:23 +00:00
Sasha
ed0d3395c7 test: ensure data strictness (#10123)
Ensures we don't save and read additional properties to the database
with both, Local API and `payload.db`.
2024-12-22 09:37:32 +02:00
Said Akhrarov
68b5f61641 fix: ensure tab label and description are serializable (#10115)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes an issue where assigning a label or description function
to a tab would cause a runtime error due to passing a function to a
client component.

### Why?
To prevent runtime errors when using non-static designations.

### How?
By properly evaluating label and description functions prior to
assignment to their `clientTab` counterpart.

Fixes #10114

Before:

![image](https://github.com/user-attachments/assets/e003780a-2f24-4988-96ce-27c22fd61854)

After:

![image](https://github.com/user-attachments/assets/0bd185a0-415d-4414-bead-48647e244ba7)
2024-12-22 00:33:40 +00:00
Elliot DeNolf
1372f2468d ci: only label stale issues/prs, disable closing 2024-12-21 14:12:16 -05:00
Elliot DeNolf
235eade08f templates: bump for v3.11.0 (#10121)
🤖 Automated bump of templates for v3.11.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-21 08:09:28 -05:00
Elliot DeNolf
e9a330d4b9 chore(release): v3.11.0 [skip ci] 2024-12-21 07:56:14 -05:00
Sasha
b08ff88fbd fix(db-mongodb): mongodb optimizations (#10120)
There are few issues introduced in #9594 that we need to look into with
these changes.
2024-12-21 07:42:44 -05:00
Sam
08eb13d189 docs: remove stray backtick from Nested Docs Plugin page (#10118)
### What?
Remove erroneous backtick from comment in code snippet from
https://payloadcms.com/docs/plugins/nested-docs#overrides
2024-12-21 07:23:01 -05:00
Alessio Gravili
f1901f6648 chore: pin templates to latest version (#10116)
pinning to an exact version number is clearer more reliable than using
"latest".
2024-12-21 01:18:03 -07:00
Said Akhrarov
a58b9fc230 fix(ui): join table row still shows after deletion (#9783)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes an issue where deleting an entry in a `Join` via the
`Drawer` accessed through the `DrawerLink` would not update the table
until the page was refreshed.

### Why?
For a better, more reactive, deletion experience for end-users. Ideally,
the deletion is reflected in the table right away.

### How?
By passing an `onDrawerDelete` function to the `DrawerLink` which simply
filters out the existing doc according to an id.

Fixes #9580

Before:

[Editing---Post--before-Payload.webm](https://github.com/user-attachments/assets/3dd4df78-bb63-46b1-bf5f-7643935e15ad)

After:

[Editing---Post--after-Payload.webm](https://github.com/user-attachments/assets/97bb604f-41df-4cc9-8c46-9a59a19c72b7)
2024-12-20 21:47:09 -05:00
Said Akhrarov
ec853c4336 fix(ui): ensure unpublish confirmation is reachable when opened in drawers (#10109)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes an issue where the unpublish modal was unreachable due to
the high `z-index` on `Drawer` components. This makes unpublishing
documents from a drawer impossible. For example, when editting a
document from the drawer opened in a `RelationshipTable`.

### Why?
To allow editors to be able to unpublish docs regardless of drawer depth
and context.

### How?
By rendering the unpublish modal at a sufficiently high z-index, while
taking into account edit depth.

Fixes #10108

Before:

[Dashboard-unpublish-before--Payload.webm](https://github.com/user-attachments/assets/7acf1002-138e-48bd-81ec-76f5eabfb2d4)

After:

[Dashboard-unpublish-after--Payload.webm](https://github.com/user-attachments/assets/ff109ee9-5b63-43d0-931f-500ded8f6d3a)
2024-12-20 23:52:05 +00:00
zuccs
2175451480 docs: fix broken links (#10010)
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2024-12-20 15:19:45 -05:00
Jacob Fletcher
957867f6e2 fix: ensures generated IDs persist on create (#10089)
IDs that are supplied directly through the API, such as client-side
generated IDs when adding new blocks and array rows, are overwritten on
create. This is because when adding blocks or array rows on the client,
their IDs are generated first before being sent to the server for
processing. Then when the server receives this data, it incorrectly
overrides them to ensure they are unique when using relational DBs. But
this only needs to happen when no ID was supplied on create, or
specifically when duplicating documents via the `beforeDuplicate` hook.
2024-12-20 15:14:23 -05:00
Sasha
4e953530df feat(db-sqlite): add autoIncrement option (#9427)
### What?
Exposes ability to enable
[AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for Primary Keys
which ensures that the same ID cannot be reused from previously deleted
rows.

```ts
sqliteAdapter({
  autoIncrement: true
})
```

### Why?
This may be essential for some systems. Enabled `autoIncrement: true`
also for the SQLite Adapter in our tests, which can be useful when
testing whether the doc was deleted or not when you also have other
create operations.

### How?
Uses Drizzle's `autoIncrement` option.

WARNING:
This cannot be enabled in an existing project without a custom
migration, as it completely changes how primary keys are stored in the
database.
2024-12-20 20:13:28 +00:00
Elliot DeNolf
99481cbc7d templates: bump for v3.10.0 (#10107)
🤖 Automated bump of templates for v3.10.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-20 15:07:51 -05:00
Elliot DeNolf
4d50046104 chore(release): v3.10.0 [skip ci] 2024-12-20 14:42:13 -05:00
Sasha
a7109ed048 refactor: consistent caps for acronyms in variable names (#10101)
Improves consistency for variable names like `docID`, `insertedID`,
`parentID`
For example: `docId` -> `docID`
2024-12-20 18:10:34 +00:00
Shrinidhi Shastri
dec87e971a chore(templates): remove console.log that logs payload secret for security (#10095)
I noticed that payload.secret was getting logged via console.log, adding
a significant security risk.
Removed the console.log statements from three preview/route.ts files.
2024-12-20 17:01:47 +00:00
James Mikrut
a501e604d6 chore(richtext-slate): exports useElementButton, useLeafButton (#10047)
Exports `useElementButton` and `useLeafButton` from the Slate package
for custom Slate components.
2024-12-20 09:48:49 -07:00
Dan Ribbens
6bba7bec4e feat(ui): export RelationshipTable component (#10084)
Fixes https://github.com/payloadcms/payload/discussions/9049
2024-12-20 11:47:22 -05:00
Elliot DeNolf
2a6bac97f5 chore(release): eslint/3.9.0 2024-12-20 11:35:52 -05:00
Patrik
52b1a9a720 templates: removes DATABASE_URI env var from with-vercel-website template .env.example (#10098)
### What?

Previously, the `with-vercel-website` template included a `DATABASE_URI`
env var in the `.env.example` file - which was unneeded.

### Why?

The `with-vercel-website` template uses a `POSTGRES_URL` env var for the
db connection string env var instead.

### How?

Removes the `DATABASE_URI` env var from the .env.example file.

Also, updates the `DATABASE_URI` db string names in the following
templates from `payloadtests` to `your-database-name` for a more generic
/ clear name:
- with-postgres
- with-vercel-mongodb
- with-vercel-postgres
- with-vercel-website
2024-12-20 11:02:06 -05:00
Jacob Fletcher
7bedd6d822 fix(examples): awaits getHeaders in auth example (#10100)
The auth example was not properly awaiting `getHeaders` from
`next/navigation`. This was simply outdated, as this function was
changed to async over the course of the various RC versions during our
beta phase.
2024-12-20 10:38:38 -05:00
Sasha
59fc9d094e fix(ui): close copy locale modal after locale is changed (#10096)
Ensures we close the modal _only_ after locale is changed. This caused
our localization e2e's to flake:

![image](https://github.com/user-attachments/assets/41205afe-0e45-499b-9aa6-07734a7f26fc)
2024-12-20 08:29:53 -05:00
Sasha
7c4ea5b86e refactor: optimize database schema generation bin script (#10086)
* Avoids additional file system writes (1 for `await writeFile` and then
`npx prettier --write`) instead prettier now formats the javascript
string directly. Went from 650 MS to 250 MS for the prettify block.
* Disables database connection, since the `db.generateSchema` doesn't
need connection, this also disables Drizzle schema push.
* Properly exits the bin script process.
2024-12-20 14:43:11 +02:00
Jacob Fletcher
7292220109 chore(examples): updates auth example to latest (#10090)
The auth example was still on `v3.0.0-beta.24`, was missing its users
collection config, and was not yet using the component paths pattern
established here: #7246. This updates to latest and fixes these issues.
This example can still use further improvements and housekeeping which
will come in future PRs.
2024-12-19 23:54:24 -05:00
Mason Yekta
dd3c2eb42b fix(examples): add missing header component in auth example (#10088) 2024-12-20 04:16:03 +00:00
Alessio Gravili
b3308736c4 feat: jsdocs for generated types, by using admin.description (#9917)
This makes use of admin.description to generate JSDocs for field,
collection and global generated types.


![image](https://github.com/user-attachments/assets/980d825f-49a2-426d-933a-2ff3d205ea24)


![image](https://github.com/user-attachments/assets/d0b1f288-1ea1-4d80-8c05-003d59a4e41a)

For the future, we should add a dedicated property to override these
JSDocs.

You can view the effect of this PR on our test suite generated types
here:
05f552bbbc
2024-12-19 22:22:43 -05:00
Elliot DeNolf
46e50c4572 templates: bump for v3.9.0 (#10087)
🤖 Automated bump of templates for v3.9.0

Triggered by user: @paulpopus

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-19 22:50:37 +00:00
Dan Ribbens
d03658de01 feat: join field with polymorphic relationships (#9990)
### What?
The join field had a limitation imposed that prevents it from targeting
polymorphic relationship fields. With this change we can support any
relationship fields.

### Why?
Improves the functionality of join field.

### How?
Extended the database adapters and removed the config sanitization that
would throw an error when polymorphic relationships were used.

Fixes #
2024-12-19 22:34:52 +00:00
Sasha
07be617963 fix(db-postgres): relationships v2-v3 migration errors when migrating from v2 to stable v3 (#10080)
When migrating from stable v2 to v3, the provided relationships
migration fails because of not having `payload_locked_documents` and
`payload_locked_documents_rels` tables in the migration generated in v2.


![image](https://github.com/user-attachments/assets/0dc57e6a-5e6e-4b74-bcab-70e660f4e939)
2024-12-19 19:09:07 +00:00
Alessio Gravili
d8c106cb2b fix(templates): broken preview if alternative auth strategy was used, invalid error handling (#9785)
Previously, live preview did not work with oauth, as no token is present
2024-12-19 13:23:47 -05:00
Sasha
e468292039 perf(db-mongodb): improve performance of all operations, up to 50% faster (#9594)
This PR improves speed and memory efficiency across all operations with
the Mongoose adapter.

### How?

- Removes Mongoose layer from all database calls, instead uses MongoDB
directly. (this doesn't remove building mongoose schema since it's still
needed for indexes + users in theory can use it)
- Replaces deep copying of read results using
`JSON.parse(JSON.stringify(data))` with the `transform` `operation:
'read'` function which converts Date's, ObjectID's in relationships /
joins to strings. As before, it also handles transformations for write
operations.
- Faster `hasNearConstraint` for potentially large `where`'s
- `traverseFields` now can accept `flattenedFields` which we use in
`transform`. Less recursive calls with tabs/rows/collapsible

Additional fixes
- Uses current transaction for querying nested relationships properties
in `buildQuery`, previously it wasn't used which could've led to wrong
results
- Allows to clear not required point fields with passing `null` from the
Local API. Previously it didn't work in both, MongoDB and Postgres

Benchmarks using this file
https://github.com/payloadcms/payload/blob/chore/db-benchmark/test/_community/int.spec.ts

### Small Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 1170 | 844 | 27.86% |
| `payload.db.create` (ms) | 1413 | 691 | 51.12% |
| `payload.db.find` (ms) | 2856 | 2204 | 22.83% |
| `payload.db.deleteMany` (ms) | 15206 | 8439 | 44.53% |
| `payload.db.updateOne` (ms) | 21444 | 12162 | 43.30% |
| `payload.db.findOne` (ms) | 159 | 112 | 29.56% |
| `payload.db.deleteOne` (ms) | 3729 | 2578 | 30.89% |
| DB small FULL (ms) | 64473 | 46451 | 27.93% |

---

### Medium Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 9407 | 6210 | 33.99% |
| `payload.db.create` (ms) | 10270 | 4321 | 57.93% |
| `payload.db.find` (ms) | 20814 | 16036 | 22.93% |
| `payload.db.deleteMany` (ms) | 126351 | 61789 | 51.11% |
| `payload.db.updateOne` (ms) | 201782 | 99943 | 50.49% |
| `payload.db.findOne` (ms) | 1081 | 817 | 24.43% |
| `payload.db.deleteOne` (ms) | 28534 | 23363 | 18.12% |
| DB medium FULL (ms) | 519518 | 342194 | 34.13% |

---

### Large Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 26575 | 17509 | 34.14% |
| `payload.db.create` (ms) | 29085 | 12196 | 58.08% |
| `payload.db.find` (ms) | 58497 | 43838 | 25.04% |
| `payload.db.deleteMany` (ms) | 372195 | 173218 | 53.47% |
| `payload.db.updateOne` (ms) | 544089 | 288350 | 47.00% |
| `payload.db.findOne` (ms) | 3058 | 2197 | 28.14% |
| `payload.db.deleteOne` (ms) | 82444 | 64730 | 21.49% |
| DB large FULL (ms) | 1461097 | 969714 | 33.62% |
2024-12-19 13:20:39 -05:00
Sasha
034b442699 test: revert default db adapter in integration tests to mongodb (#10079) 2024-12-19 18:20:28 +00:00
Andrzej Kłapeć
6a8aecadf8 fix(richtext-lexical): incorrect string interpolation in the upload converter (#10069)
This fixes the incorrect `<source>` `media` attribute value generation
within the upload converter.

<img width="919" alt="Zrzut ekranu 2024-12-19 o 12 21 10"
src="https://github.com/user-attachments/assets/6f26de7e-26e0-446a-83c5-6e5a776fac1e"
/>
2024-12-19 11:03:51 -07:00
Sasha
23f1ed4a48 feat(db-postgres, db-sqlite): drizzle schema generation (#9953)
This PR allows to have full type safety on `payload.drizzle` with a
single command
```sh
pnpm payload generate:db-schema
```
Which generates TypeScript code with Drizzle declarations based on the
current database schema.

Example of generated file with the website template: 
https://gist.github.com/r1tsuu/b8687f211b51d9a3a7e78ba41e8fbf03

Video that shows the power:


https://github.com/user-attachments/assets/3ced958b-ec1d-49f5-9f51-d859d5fae236


We also now proxy drizzle package the same way we do for Lexical so you
don't have to install it (and you shouldn't because you may have version
mismatch).
Instead, you can import from Drizzle like this:
```ts
import {
  pgTable,
  index,
  foreignKey,
  integer,
  text,
  varchar,
  jsonb,
  boolean,
  numeric,
  serial,
  timestamp,
  uniqueIndex,
  pgEnum,
} from '@payloadcms/db-postgres/drizzle/pg-core'
import { sql } from '@payloadcms/db-postgres/drizzle'
import { relations } from '@payloadcms/db-postgres/drizzle/relations'
```


Fixes https://github.com/payloadcms/payload/discussions/4318

In the future we can also support types generation for mongoose / raw
mongodb results.
2024-12-19 11:08:17 -05:00
Elliot DeNolf
ba0e7aeee5 chore: proper docker-compose postgres url 2024-12-19 10:42:41 -05:00
Sasha
5753efb0a4 fix(db-mongodb): querying by localized polymorphic relationships using objects (#10037)
Previously, queries like this didn't work:
```ts
const res = await payload.find({
  collection: 'polymorphic-relationships',
  where: {
    polymorphicLocalized: {
      equals: {
        relationTo: 'movies',
        value: movie.id,
      },
    },
  },
})
```

This was due to the incorrectly passed path to MongoDB without
`.{locale}` suffix.
Additionally, to MongoDB now we send:
```

{
  $or: [
    {
      polymorphic: {
        $eq: {
          relationTo: formattedValue.relationTo,
          value: formattedValue.value,
        },
      },
    },
    {
      polymorphic: {
        $eq: {
          relationTo: 'movies',
          value: 'some-id',
        },
      },
    },
  ],
},
```

Instead of:
```
{
  $and: [
    {
      'polymorphic.relationTo': {
        $eq: 'movies ',
      },
    },
    {
      'polymorphic.value': {
        $eq: 'some-id ',
      },
    },
  ],
}
```

To match the _exact_ value. This is essential when we do querying by
relationships with `hasMany: true` and custom IDs that can be repeated.
`$or` is needed if for some reason keys are stored in the DB in a
different order
2024-12-19 10:42:15 -05:00
Germán Jabloñski
12dad35cf9 fix(richtext-lexical): ui bug when zooming in Safari (#10072)
Fix #10043
2024-12-19 15:00:31 +00:00
Elliot DeNolf
997aed346f templates: update dockerfiles (#10073)
- Dockerfiles needed to be updated to the Next.js `with-docker` example.
- docker-compose for postgres templates have been updated.
2024-12-19 09:48:38 -05:00
Sasha
0c57eef621 fix: unique error message regression (#10064)
Regression from https://github.com/payloadcms/payload/pull/9935, we need
to call `req.t` with `''error:valueMustBeUnique''` instead of just
`''error:valueMustBeUnique''`
2024-12-19 08:13:44 +00:00
Sasha
1d46b6d738 fix(ui): join field "add new" calculate initial drawer data with relationship inside blocks (#10057)
We merged https://github.com/payloadcms/payload/pull/9773 that adds
support for join field with relationships inside arrays, it just happens
that now it's also true for relationships inside blocks, added a test
case with blocks.
This just corrects initial data drawer calculation with the "add new"
button if we encounter a blocks field in join's `on`.
2024-12-19 08:45:22 +02:00
Sasha
03ff77544e feat(db-sqlite): add idType: 'uuid' support (#10016)
Adds `idType: 'uuid'` to the SQLite adapter support:
```ts
sqliteAdapter({
  idType: 'uuid',
})
```

Achieved through Drizzle's `$defaultFn()`
https://orm.drizzle.team/docs/latest-releases/drizzle-orm-v0283#-added-defaultfn--default-methods-to-column-builders
as SQLite doesn't have native UUID support. Added `sqlite-uuid` to CI.
2024-12-18 22:44:04 -05:00
Sasha
0e5bda9a74 feat: make req partial and optional in DB / Local API operations (#9935)
### What?
Previously, the `req` argument:
In database operations (e.g `payload.db`) was required and you needed to
pass the whole `req` with all the properties. This is confusing because
in database operations we never use its properties outside of
`req.transactionID` and `req.t`, both of which should be optional as
well.

Now, you don't have to do that cast:
```ts
payload.db.findOne({
  collection: 'posts',
  req: {} as PayloadRequest,
  where: {
    id: {
      equals: 1,
    },
  },
})
```
Becomes:
```ts
payload.db.findOne({
  collection: 'posts',
  where: {
    id: {
      equals: 1,
    },
  },
})
```

If you need to use transactions, you're not required to do the `as` cast
as well now, as the `req` not only optional but also partial -
`Partial<PayloadRequest>`.
`initTransaction`, `commitTransaction`, `killTransaction` utilities are
typed better now as well. They do not require to you pass all the
properties of `req`, but only `payload` -
`MarkRequired<Partial<PayloadRequest>, 'payload'>`
```ts
const req = { payload }
await initTransaction(req)
await payload.db.create({
  collection: "posts",
  data: {},
  req
})
await commitTransaction(req)
```

The same for the Local API. Internal operations (for example
`packages/payload/src/collections/operations/find.ts`) still accept the
whole `req`, but local ones
(`packages/payload/src/collections/operations/local/find.ts`) which are
used through `payload.` now accept `Partial<PayloadRequest>`, as they
pass it through to internal operations with `createLocalReq`.

So now, this is also valid, while previously you had to do `as` cast for
`req`.
```ts
const req = { payload }
await initTransaction(req)
await payload.create({
  collection: "posts",
  data: {},
  req
})
await commitTransaction(req)
```


Marked as deprecated `PayloadRequest['transactionIDPromise']` to remove
in the next major version. It was never used anywhere.
Refactored `withSession` that returns an object to `getSession` that
returns just `ClientSession`. Better type safety for arguments
Deduplicated in all drizzle operations to `getTransaction(this, req)`
utility:
```ts
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
```
Added fallback for throwing unique validation errors in database
operations when `req.t` is not available.

In migration `up` and `down` functions our `req` is not partial, while
we used to passed `req` with only 2 properties - `payload` and
`transactionID`. This is misleading and you can't access for example
`req.t`.
Now, to achieve "real" full `req` - we generate it with `createLocalReq`
in all migration functions.

This all is backwards compatible. In all public API places where you
expect the full `req` (like hooks) you still have it.

### Why?
Better DX, more expected types, less errors because of types casting.
2024-12-18 22:43:37 -05:00
Sasha
eee6432715 fix(db-postgres): query has many relationships nested in row fields (#9944) (#9944)
### What?
Querying by nested to rows fields in has many relationships like this:
```ts
const result = await payload.find({
  collection: 'relationship-fields',
  where: {
    'relationToRowMany.title': { equals: 'some-title' },
  },
})
```
Where the related collection:
```ts
const RowFields: CollectionConfig = {
  slug: rowFieldsSlug,
  fields: [
    {
      type: 'row',
      fields: [
        {
          name: 'title',
          label: 'Title within a row',
          type: 'text',
          required: true,
        },
      ],
    },
  ],
}
```

was broken

### Why?
We migrated to use `flattenedFields`, but not in this specific case.
This error would be caught earlier we used `noImplictAny` typescript
rule. https://www.typescriptlang.org/tsconfig/#noImplicitAny which
wouldn't allow us to create variable like this:
```ts
let relationshipFields // relationshipFields is any here
```
Instead, we should write:
```ts
let relationshipFields: FlattenedField[]
```
We should migrate to it and `strictNullChecks` as well.

Fixes https://github.com/payloadcms/payload/issues/9534
2024-12-18 22:26:08 -05:00
Elliot DeNolf
044c22de54 chore(deps): bump turbo 2024-12-18 20:47:45 -05:00
Paul
ce74f1b238 fix(storage-vercel-blob): fixes issue where files with spaces in their name would not be retrieved correctly (#10062)
URI encodes filenames so that they're retrieved correctly from Vercel's
blob storage. Sometimes spaces would cause problems only in certain file
names
2024-12-19 00:32:59 +00:00
Paul
605cf42cbe templates: add Posts to internal links in website template (#10063)
Posts were previously not selectable as part of the internal links
(reference fields) in the website template.
2024-12-19 00:28:46 +00:00
Sasha
97c120ab28 fix(richtext-*): use correct "for" attribute for label (#10036)
Fixes https://github.com/payloadcms/payload/issues/10034
2024-12-18 16:58:59 -07:00
Jacob Fletcher
97a1f4afa9 test: consolidates custom id e2e tests (#10061)
Although we have a dedicated e2e test suite for custom IDs, tests for
custom unnamed tab and row IDs were still located within the admin test
suite. This consolidates these tests into the appropriate test suite as
expected.
2024-12-18 22:44:46 +00:00
Paul
439dd04ce9 fix: commit transaction if a user isnt found in forgotPassword operation (#10055)
The forgotPassword operation exits silently if no user is found, but
because we don't throw an error the transaction never gets committed
leading to a timeout.
2024-12-18 15:53:53 -06:00
Elliot DeNolf
a3457af36d templates: bump for v3.9.0 (#10060)
🤖 Automated bump of templates for v3.9.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-18 16:20:32 -05:00
Elliot DeNolf
d0d7b51ed5 chore(release): v3.9.0 [skip ci] 2024-12-18 15:58:11 -05:00
Paul
ef90ebb395 feat(storage-*): add support for browser-based caching via etags (#10014)
This PR makes changes to every storage adapter in order to add
browser-based caching by returning etags, then checking for them into
incoming requests and responding a status code of `304` so the data
doesn't have to be returned again.

Performance improvements for cached subsequent requests:

![image](https://github.com/user-attachments/assets/e51b812c-63a0-4bdb-a396-0f172982cb07)


This respects `disableCache` in the dev tools.



Also fixes a bug with getting the latest image when using the Vercel
Blob Storage adapter.
2024-12-18 15:54:36 -05:00
Alessio Gravili
194a8c189a feat: add shouldRestore config to job queue tasks (#10059)
By default, if a task has passed previously and a workflow is re-run,
the task will not be re-run. Instead, the output from the previous task
run will be returned. This is to prevent unnecessary re-runs of tasks
that have already passed.

This PR allows you to configure this behavior through the
`retries.shouldRestore` property. This property accepts a boolean or a
function for more complex restore behaviors.
2024-12-18 13:16:13 -07:00
Patrik
1446fe4694 fix: encodes upload filename urls (#10048)
### What?

Previously, upload files urls were not being encoded.

### Why?

As a result, this could lead to discrepancies where upload filenames
with spaces - the spaces would not be encoded as %20 in the URL.

### How?

To address this issue, we simply need to encode the filename of the
upload media.

Fixes #9698
2024-12-18 14:37:29 -05:00
Elliot DeNolf
f29e6335f6 fix(payload-cloud): improve not found logging (#10058)
In Payload Cloud, an unhelpful message would be surfaced if attempting
to retrieve a non-existent file. This improves the log message and
response to be more helpful.
2024-12-18 14:34:43 -05:00
Paul
93dde52fa9 ci: add email-resend and email-* to scopes for pr-title workflow (#10053) 2024-12-18 14:33:31 -05:00
Javier
198763a24e feat(db-mongodb): allow to customize mongoose schema options with collectionsSchemaOptions (#9885)
Adds the ability to pass additional schema options for collections with:
```ts
mongooseAdapter({
  collectionsSchemaOptions: {
    posts: {
      strict: false,
    },
  },
})
``` 

This changes relates to these:

-   https://github.com/payloadcms/payload/issues/4533
-   https://github.com/payloadcms/payload/discussions/4534

It is a proposal to set custom schema options for mongoose driver. 

I understand this got introduced into `main` v2 after `beta` branch was
created so this feature got lost.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-12-18 15:46:39 +00:00
Jacob Fletcher
b8de401195 chore: fixes package license file casing (#10021)
Fixes file casing for all package licenses where applicable, i.e.
`license.md` → `LICENSE.md` to comply with standards outlined
[here](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository).
2024-12-18 10:36:42 -05:00
Sasha
2ee3e30b50 fix(db-postgres): select query on upload fields with hasMany: true (#10029)
Previously, if you selected only upload `hasMany: true` field, you would
receive an empty arrays always, because the `_rels` table wasn't joined
in this case. Fixes the condition to count `field.type === 'upload'` .
2024-12-18 06:33:12 +02:00
Patrik
61c5e0d3e0 fix(ui): properly allows configuring rows for the textarea field (#10031)
### What?

Previously, setting the `admin.rows` property did not change the height
of the `textarea` field input.

### Why?

Although `rows` was being properly set on the textarea element - it's
absolute positioning prevented the height from actually changing.

### How?

Updates the styles of the textarea field component to properly allow the
rows prop to change the height of the field.

Example w/:

```
{
  name: 'someTextArea',
  type: 'textarea',
  admin: {
    rows: 5,
  }
}
```
Before:
![Screenshot 2024-12-17 at 2 50
19 PM](https://github.com/user-attachments/assets/35d433c3-2fad-4930-9da6-b47b5e180af8)

After:
![Screenshot 2024-12-17 at 2 50
00 PM](https://github.com/user-attachments/assets/1a413639-ac29-46ce-b774-e1a30c5a21f0)

Fixes #10017
2024-12-17 15:46:11 -05:00
Elliot DeNolf
4bfa329fa4 templates: document local development (#10032)
Provide docker-compose for spinning up postgres locally along w/
relevant info in README.
2024-12-17 15:17:18 -05:00
Alessio Gravili
f5c13deb24 build: fix tsconfig monorepo setup (#10028)
Should fix messed up import suggestions and simplifies all tsconfigs
through inheritance.

One main issue was that packages were inheriting `baseURL: "."` from the
root tsconfig. This caused incorrect import suggestions that start with
"packages/...".

This PR ensures that packages do not inherit this baseURL: "." property,
while ensuring the root, non-inherited tsconfig still keeps it to get
tests to work (the importMap needs it)
2024-12-17 14:49:29 -05:00
Patrik
70666a0f7b fix(cpa): updates CPAs w/ vercel-postgres db types to use POSTGRES_URL & updates .env.example to use generic env var strings (#10027)
CPA projects generated with the `vercel-postgres` db type were not
receiving the proper DB env vars in the .env.example & .env files

With the `vercel-postgres` db type, the DB env var needs to be
`POSTGRES_URL` not `DATABASE_URI`.

Additionally, updates the generated .env.example file to show generic
env var strings.

#### Blank w/ MongoDB:
- `.env.example`:
```
DATABASE_URI=mongodb://127.0.0.1/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
DATABASE_URI=mongodb://127.0.0.1/test-cpa-blank-mongodb
PAYLOAD_SECRET=aef857429edc7f42a90bb374
```

#### Blank w/ Postgres:
- `.env.example`:
```
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/test-cpa-blank-postgres
PAYLOAD_SECRET=241bfe11fbe0a56dd9757019
```

#### Blank w/ SQLite:
- `.env.example`:
```
DATABASE_URI=file:./your-database-name.db
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
DATABASE_URI=file:./test-cpa-blank-sqlite.db
PAYLOAD_SECRET=a7808731b93240a73a11930c
```

#### Blank w/ vercel-postgres:
- `.env.example`:
```
POSTGRES_URL=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE
```
- `.env`:
```
# Added by Payload
POSTGRES_URL=postgres://postgres:<password>@127.0.0.1:5432/test-cpa-blank-vercel-postgres
PAYLOAD_SECRET=af3951e923e8e4662c9c3d9e
```

Fixes #9996
2024-12-17 14:13:00 -05:00
Dan Ribbens
b0b2fc6c47 feat: join field support relationships inside arrays (#9773)
### What?

Allow the join field to have a configuration `on` relationships inside
of an array, ie `on: 'myArray.myRelationship'`.

### Why?

This is a more powerful and expressive way to use the join field and not
be limited by usage of array data. For example, if you have a roles
array for multinant sites, you could add a join field on the sites to
show who the admins are.

### How?

This fixes the traverseFields function to allow the configuration to
pass sanitization. In addition, the function for querying the drizzle
tables needed to be ehanced.

Additional changes from https://github.com/payloadcms/payload/pull/9995:

- Significantly improves traverseFields and the 'join' case with a raw
query injection pattern, right now it's internal but we could expose it
at some point, for example for querying vectors.
- Fixes potential issues with not passed locale to traverseFields (it
was undefined always)
- Adds an empty array fallback for joins with localized relationships

Fixes #
https://github.com/payloadcms/payload/discussions/9643

---------

Co-authored-by: Because789 <thomas@because789.ch>
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-12-17 13:14:43 -05:00
Jacob Fletcher
eb037a0cc6 fix: passes field permissions to custom fields (#10024)
Fixes #9888. Field permissions were not being passed into custom
components. This led to custom components, such as arrays and blocks,
unable to render default Payload fields. This was because their props
lacked the permissions object required for rendering. For example:

```ts
'use client'
import type { ArrayFieldClientComponent } from 'payload'
import { ArrayField } from '@payloadcms/ui'

export const MyArray: ArrayFieldClientComponent = (props) => <ArrayField {...props} />
```

In this example the array field itself would render, but the fields
within each row would not, because the array field did not pass its
permissions down to the rows.
2024-12-17 12:17:42 -05:00
Jarrod Flesch
99ca1babc6 fix: beforeValidate previousValue argument (#10022)
### What?
`previousValue` was incorrect. It would always return the current value.

### Why?
It was accessing siblingData instead of siblingDoc. Other hooks use
siblingDoc, but this one was using siblingData.
2024-12-17 12:08:40 -05:00
urquico
13e050582b docs: fixes typo removeTokenFromRepsonse to removeTokenFromResponse (#10026) 2024-12-17 16:52:06 +00:00
Said Akhrarov
77871050b8 fix(ui): properly sync field values in bulk upload preventing stale data overriding old docs (#9918)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes an issue where bulk upload on an upload field with
hasMany, which had errors on sequential uploads, caused only the last
successful upload to be saved to the field value.

### Why?
To save all successful uploads to the field value and sync what was
shown in the ui to the actual field data.

### How?
By triggering a rerender that syncs `populatedDocs` to the fields
`value` on each sequential successful upload after form errors were
resolved.

Fixes #9890

Before:

[Bulk-upload-before--Post---Payload.webm](https://github.com/user-attachments/assets/6396a88b-21c2-4037-b1ef-fd7f8d16103f)

After:

[Bulk-upload-after---Payload.webm](https://github.com/user-attachments/assets/8566a022-6e86-46c7-87fe-78d01e6dd8c9)

Notes:
- The core issue was that onSuccess function was not properly syncing
the correct field values resulting in stale values that would overwrite
old docs.

---------

Co-authored-by: Patrik Kozak <patrik@payloadcms.com>
2024-12-17 10:08:57 -05:00
Elliot DeNolf
e04be4bf62 templates: improve gen-templates script (#10015)
- Allow gen templates script to take template dir name arg
- All `skipDockerCompose` and `skipConfig`
2024-12-17 10:00:17 -05:00
Hugo Knorr
7037983de0 fix(templates): prevent image priority and lazy loading incompatibility (#10023)
This PR fixes an issue in the hero banner of website templates where
`priority` was passed to `ImageMedia` component but was incompatible with
NextImage `loading="lazy"`, causing error. The fix is to add a ternary
condition to check if `priority` prop is passed before setting `loading.
2024-12-17 14:56:53 +00:00
Said Akhrarov
29ad1fcb77 fix(plugin-search): prevent error on undefined value in linkToDoc component (#9932)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes a runtime error encountered when navigating into a search
doc that had its' related collection doc deleted, but it itself remained
(if for example `deleteFromSearch` deletion failed for some reason).

### Why?
To prevent runtime errors for end-users using `plugin-search`.

### How?
By returning earlier if the field value is undefined or missing required
values in `LinkToDoc`.

Fixes #9443 (partially, see also: #9623)
2024-12-17 04:39:15 +00:00
Elliot DeNolf
2d2a52b00f templates: bump for v3.8.0 (#10013)
🤖 Automated bump of templates for v3.8.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 21:31:27 -05:00
Elliot DeNolf
3f35d36934 chore(release): v3.8.0 [skip ci] 2024-12-16 21:12:02 -05:00
Sasha
41167bfbeb feat(db-vercel-postgres): allow to use a local database using pg instead of @vercel/postgres (#9771)
### What?
This PR allows you to use a local database when using
`vercelPostgresAdapter`. This adapter doesn't work with them because it
requires an SSL connection and Neon's WS proxy. Instead we fallback to
using pool from `pg` if `hostname` is either `127.0.0.1` or `localhost`.

If you still want to use `@vercel/postgres` even locally you can pass
`disableUsePgForLocalDatabase: true` here and you'd have to spin up the
DB with a special Neon's Docker Compose setup -
https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker

### Why?
Forcing people to use a cloud database locally isn't great. Not only
they are slow but also paid.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-12-16 16:55:08 -05:00
Jacob Fletcher
ed44ec0a9c fix(ui): does not render row labels until form state returns (#10002)
Extension of #9933. Custom components are returned by form state,
meaning that if we don't wait for form state to return before rendering
row labels, the default row label component will render in briefly
before being swapped by a custom component (if applicable). Using the
new `isLoading` prop on array and block rows, we can conditionally
render them just as we currently do for the row fields themselves.
2024-12-16 21:44:19 +00:00
Elliot DeNolf
fa49e04cf8 feat(storage-vercel-blob): allow fallback to disk if token not set (#10005)
Previously with `@payloadcms/plugin-storage-blob`, if token was not set,
the plugin would throw an error. This caused a less-than-ideal developer
experience.

With this change, if the `token` value is undefined:
- Local storage will be used as a fallback
- The error will no longer be thrown.
2024-12-16 21:40:22 +00:00
Jacob Fletcher
f54e180370 fix(templates): adds priority to hero images (#10003)
Hero images should use the `priority` property so that browsers will
preload them. This is because hero images, by definition, are rendered
"above the fold" and should be treated as such, optimizing LCP. This
also means these images should _not_ define a `loading` strategy, as
this disregards the priority flag.
2024-12-16 16:21:29 -05:00
Said Akhrarov
c50f4237a4 docs: fix links in rich-text referencing old lexical sections (#9972) 2024-12-16 14:42:56 -05:00
zuccs
b0d648bf30 docs: broken lexical link (#9991) 2024-12-16 19:40:04 +00:00
Paul
8258d5c943 templates: fix missing ts-ignore in seed script causing build errors (#10001) 2024-12-16 19:24:04 +00:00
Elliot DeNolf
0f63db055b templates: bump for v3.7.0 (#10000)
🤖 Automated bump of templates for v3.7.0

Triggered by user: @paulpopus

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 13:08:50 -06:00
Paul
12fa4fd2f9 templates: add hero image field to posts instead of using the meta image for the hero as well (#9999)
Adds a hero image field instead of using the meta image for the hero on
posts collection
2024-12-16 18:29:21 +00:00
Sasha
6dea111d28 feat: expose req to defaultValue function arguments (#9937)
Rework of https://github.com/payloadcms/payload/pull/5912

### What?
Now, when `defaultValue` is defined as function you can receive the
`req` argument:
```ts
{
  name: 'defaultValueFromReq',
  type: 'text',
  defaultValue: async ({ req, user, locale }) => {
    return Promise.resolve(req.context.defaultValue)
  },
},
```

`user` and `locale` even though are repeated in `req`, this potentially
leaves some room to add more args in the future without removing them
now.
This also improves type for `defaultValue`:
```ts
type SerializableValue = boolean | number | object | string
export type DefaultValue =
  | ((args: {
      locale?: TypedLocale
      req: PayloadRequest
      user: PayloadRequest['user']
    }) => SerializableValue)
  | SerializableValue
```

### Why?
To access the current URL / search params / Local API and other things
directly in `defaultValue`.

### How?
Passes `req` through everywhere where we call `defaultValue()`
2024-12-16 13:18:11 -05:00
Sasha
26a10ed071 perf: reduce generated types for select by respecting interfaceName (#9870)
This PR can significantly reduce your `payload-types.ts` file if you
have sharable fields / blocks that use the `interfaceName` property.
Previously we didn't respect it for select types.

Before:
```ts
export interface Collection1Select<T extends boolean = true> {
  testing?: T;
  title?: T;
  meta?:
    | T
    | {
        title?: T;
        description?: T;
        id?: T;
      };
  blocks?:
    | T
    | {
        block1?:
          | T
          | {
              b1title?: T;
              b1description?: T;
              id?: T;
              blockName?: T;
            };
        block2?:
          | T
          | {
              b2title?: T;
              b2description?: T;
              id?: T;
              blockName?: T;
            };
      };
  updatedAt?: T;
  createdAt?: T;
}
```
After:
```ts
export interface Collection1Select<T extends boolean = true> {
  testing?: T;
  title?: T;
  meta?: T | SharedMetaArraySelect<T>;
  blocks?:
    | T
    | {
        block1?: T | SharedMetaBlockSelect<T>;
        block2?: T | AnotherSharedBlockSelect<T>;
      };
  updatedAt?: T;
  createdAt?: T;
}

/**
 * This interface was referenced by `Config`'s JSON-Schema
 * via the `definition` "SharedMetaArray_select".
 */
export interface SharedMetaArraySelect<T extends boolean = true> {
  title?: T;
  description?: T;
  id?: T;
}
/**
 * This interface was referenced by `Config`'s JSON-Schema
 * via the `definition` "SharedMetaBlock_select".
 */
export interface SharedMetaBlockSelect<T extends boolean = true> {
  b1title?: T;
  b1description?: T;
  id?: T;
  blockName?: T;
}
/**
 * This interface was referenced by `Config`'s JSON-Schema
 * via the `definition` "AnotherSharedBlock_select".
 */
export interface AnotherSharedBlockSelect<T extends boolean = true> {
  b2title?: T;
  b2description?: T;
  id?: T;
  blockName?: T;
}
```

Regenerated all the types in `/test`. The diff is noticeable for
`fields` -
https://github.com/payloadcms/payload/pull/9870/files#diff-95beaac24c72c7bd60933e325cdcd94a4c3630a1ce22fabad624ec80cc74fc8c
2024-12-16 17:22:17 +02:00
Sasha
727fba7b1c refactor: deduplicate and abstract SQL schema building (#9987)
### What?
Abstracts SQL schema building, significantly reducing code duplication
for SQLite / Postgres

db-sqlite lines count From:
```sh
 wc -l **/*.ts
      62 src/connect.ts
      32 src/countDistinct.ts
       9 src/createJSONQuery/convertPathToJSONTraversal.ts
      86 src/createJSONQuery/index.ts
      15 src/defaultSnapshot.ts
       6 src/deleteWhere.ts
      21 src/dropDatabase.ts
      15 src/execute.ts
     178 src/index.ts
     139 src/init.ts
      19 src/insert.ts
      19 src/requireDrizzleKit.ts
     544 src/schema/build.ts
      27 src/schema/createIndex.ts
      38 src/schema/getIDColumn.ts
      13 src/schema/idToUUID.ts
      28 src/schema/setColumnID.ts
     787 src/schema/traverseFields.ts
      18 src/schema/withDefault.ts
     248 src/types.ts
    2304 total
```

To:
```sh
wc -l **/*.ts
      62 src/connect.ts
      32 src/countDistinct.ts
       9 src/createJSONQuery/convertPathToJSONTraversal.ts
      86 src/createJSONQuery/index.ts
      15 src/defaultSnapshot.ts
       6 src/deleteWhere.ts
      21 src/dropDatabase.ts
      15 src/execute.ts
     180 src/index.ts
      39 src/init.ts
      19 src/insert.ts
      19 src/requireDrizzleKit.ts
     149 src/schema/buildDrizzleTable.ts
      32 src/schema/setColumnID.ts
     258 src/types.ts
     942 total
```

Builds abstract schema in shared drizzle package that later gets
converted by SQLite/ Postgres specific implementation into drizzle
schema.
This, apparently can also help here
https://github.com/payloadcms/payload/pull/9953. It should be very
trivial to implement a MySQL adapter with this as well.
2024-12-16 17:17:18 +02:00
Sasha
c187bff581 fix: remove localized property from RowField and CollapsibleField (#9672)
### What?
Removes the `localized` property from typescript suggestion for row and
collapsible fields.

### Why?
Currently, this property doesn't do anything for them. This may be
changed when/if we support `name` for them, but it'll work only with
`name`.

Fixes https://github.com/payloadcms/payload/issues/4720
2024-12-16 09:57:21 -05:00
Sasha
00909ec5c4 fix(db-sqlite): working point field CRUD and default value (#9989)
Previously, the point field with SQLite was incorrectly built to the
schema and not parsed to the result.
Now, it works with SQLite, just doesn't support queries (`near` etc.).
Fixes
https://github.com/payloadcms/payload/pull/9987#discussion_r1885674299
2024-12-16 07:37:47 +02:00
Dan Ribbens
2ec4d0c2ef feat: join field admin.defaultColumns (#9982)
Add the ability to specify which columns should appear in the
relationship table of a join fields

The new property is in the Join field `admin.defaultColumns` and can be
set to an array of strings containing the field names in the desired
order.
2024-12-15 03:31:31 +00:00
748 changed files with 41277 additions and 12540 deletions

View File

@@ -180,6 +180,7 @@ jobs:
- postgres-uuid
- supabase
- sqlite
- sqlite-uuid
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -202,6 +203,13 @@ jobs:
# needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
redis:
image: redis:latest
ports:
- 6379:6379 # Expose Redis on port 6379
options: --health-cmd "redis-cli ping" --health-timeout 30s --health-retries 3
steps:
- uses: actions/checkout@v4
@@ -252,6 +260,10 @@ jobs:
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
if: matrix.database == 'supabase'
- name: Configure Redis
run: |
echo "REDIS_URL=redis://127.0.0.1:6379" >> $GITHUB_ENV
- name: Integration Tests
uses: nick-fields/retry@v3
env:

View File

@@ -41,7 +41,9 @@ jobs:
db-vercel-postgres
db-sqlite
drizzle
email-\*
email-nodemailer
email-resend
eslint
graphql
live-preview

View File

@@ -25,19 +25,16 @@ jobs:
with:
debug-only: ${{ inputs.dry-run || false }}
days-before-stale: 23
days-before-close: 7
days-before-pr-close: -1 # Never close PRs
days-before-stale: 30
days-before-close: -1 # Disable closing
ascending: true
operations-per-run: 300
exempt-all-assignees: true
# Issues
stale-issue-label: stale
exempt-issue-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor,status: verified'
stale-issue-message: |
This issue has been marked as stale due to lack of activity.
To keep this issue open, please indicate that it is still relevant in a comment below.
stale-issue-message: ''
close-issue-message: |
This issue was automatically closed due to lack of activity.
@@ -45,10 +42,7 @@ jobs:
# Pull Requests
stale-pr-label: stale
exempt-pr-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor'
stale-pr-message: |
This PR is stale due to lack of activity.
To keep the PR open, please indicate that it is still relevant in a comment below.
stale-pr-message: ''
close-pr-message: |
This pull request was automatically closed due to lack of activity.

View File

@@ -76,7 +76,7 @@ If a Collection supports [Versions](../versions/overview), the following additio
Returns a boolean which allows/denies access to the `create` request.
To add create Access Control to a Collection, use the `create` property in the [Collection Config](../collections/overview):
To add create Access Control to a Collection, use the `create` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
@@ -104,7 +104,7 @@ The following arguments are provided to the `create` function:
Returns a boolean which allows/denies access to the `read` request.
To add read Access Control to a Collection, use the `read` property in the [Collection Config](../collections/overview):
To add read Access Control to a Collection, use the `read` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
@@ -158,7 +158,7 @@ The following arguments are provided to the `read` function:
Returns a boolean which allows/denies access to the `update` request.
To add update Access Control to a Collection, use the `update` property in the [Collection Config](../collections/overview):
To add update Access Control to a Collection, use the `update` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
@@ -208,7 +208,7 @@ The following arguments are provided to the `update` function:
Similarly to the Update function, returns a boolean or a [query constraint](/docs/queries/overview) to limit which documents can be deleted by which users.
To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](../collections/overview):
To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
@@ -288,7 +288,7 @@ The following arguments are provided to the `admin` function:
Determines which users can [unlock](/docs/authentication/operations#unlock) other users who may be blocked from authenticating successfully due to [failing too many login attempts](/docs/authentication/overview#options).
To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../collections/overview):
To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'

View File

@@ -6,7 +6,7 @@ desc: Global-level Access Control is specified within each Global's `access` pro
keywords: globals, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Global Access Control is [Access Control](../overview) used to restrict access to [Global](../globals/overview) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global.
Global Access Control is [Access Control](../overview) used to restrict access to [Global](../configuration/globals) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global.
To add Access Control to a Global, use the `access` property in your [Global Config](../configuration/globals):
@@ -25,7 +25,7 @@ export const GlobalWithAccessControl: GlobalConfig = {
Access Control is specific to the operation of the request.
To add Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../globals/overview):
To add Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
@@ -63,7 +63,7 @@ If a Global supports [Versions](../versions/overview), the following additional
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can read this global based on its current properties.
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../globals/overview):
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
@@ -90,7 +90,7 @@ The following arguments are provided to the `read` function:
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can update this global based on its current properties.
To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../globals/overview):
To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
@@ -118,7 +118,7 @@ The following arguments are provided to the `update` function:
If the Global has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document.
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Global Config](../globals/overview):
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Global Config](../configuration/globals):
```ts
import type { GlobalConfig } from 'payload'

View File

@@ -198,7 +198,7 @@ The following options are available:
Global Views are views that are scoped under the `/globals` route, such as the Document Edit View.
To swap out Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../globals/overview):
To swap out Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../configuration/globals):
```ts
import type { SanitizedGlobalConfig } from 'payload'
@@ -248,7 +248,7 @@ The following options are available:
Document Views are views that are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, such as the Edit View or the API View. All Document Views keep their overall structure across navigation changes, such as their title and tabs, and replace only the content below.
To swap out Document Views with your own, or to [create entirely new ones](#adding-new-document-views), use the `admin.components.views.Edit[key]` property in your [Collection Config](../collections/overview) or [Global Config](../globals/overview):
To swap out Document Views with your own, or to [create entirely new ones](#adding-new-document-views), use the `admin.components.views.Edit[key]` property in your [Collection Config](../collections/overview) or [Global Config](../configuration/globals):
```ts
import type { SanitizedCollectionConfig } from 'payload'

View File

@@ -45,7 +45,7 @@ import type { CollectionConfig } from 'payload'
export const UsersWithoutJWTs: CollectionConfig = {
slug: 'users-without-jwts',
auth: {
removeTokenFromRepsonse: true, // highlight-line
removeTokenFromResponse: true, // highlight-line
},
}
```

View File

@@ -79,7 +79,7 @@ The following options are available:
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/overview#csrf-protection). |
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |

View File

@@ -30,14 +30,15 @@ export default buildConfig({
## Options
| Option | Description |
| -------------------- | ----------- |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| Option | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
## Access to Mongoose models

View File

@@ -50,6 +50,11 @@ export default buildConfig({
})
```
<Banner type="info">
<strong>Note:</strong>
If when using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to adapter's 'args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
</Banner>
## Options
| Option | Description |
@@ -60,21 +65,33 @@ export default buildConfig({
| `schemaName` (experimental) | A string for the postgres schema to use, defaults to 'public'. |
| `idType` | A string of 'serial', or 'uuid' that is used for the data type given to id columns. |
| `transactionOptions` | A PgTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
## Access to Drizzle
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
You can access Drizzle as follows:
To ensure type-safety, you need to generate Drizzle schema first with:
```sh
npx payload generate:db-schema
```
```text
payload.db.drizzle
Then, you can access Drizzle as follows:
```ts
import { posts } from './payload-generated-schema'
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
import { eq, sql, and } from '@payloadcms/db-postgres/drizzle'
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
const posts = await payload.db.drizzle.query.posts.findMany()
// Drizzle's Select API https://orm.drizzle.team/docs/select
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
```
## Tables, relations, and enums
@@ -109,7 +126,7 @@ Runs before the schema is built. You can use this hook to extend your database s
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'
import { integer, pgTable, serial } from '@payloadcms/db-postgres/drizzle/pg-core'
postgresAdapter({
beforeSchemaInit: [
@@ -178,7 +195,7 @@ postgresAdapter({
})
```
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
### afterSchemaInit
@@ -189,7 +206,7 @@ The following example adds the `extra_integer_column` column and a composite ind
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { index, integer } from 'drizzle-orm/pg-core'
import { index, integer } from '@payloadcms/db-postgres/drizzle/pg-core'
import { buildConfig } from 'payload'
export default buildConfig({
@@ -231,3 +248,43 @@ export default buildConfig({
})
```
### Note for generated schema:
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
postgresAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
// Add a new table
schema.rawTables.myTable = {
name: 'my_table',
columns: [{
name: 'my_id',
type: 'serial',
primaryKey: true
}],
}
// Add a new column to generated by Payload table:
schema.rawTables.posts.columns.customColumn = {
name: 'custom_column',
// Note that Payload SQL doesn't support everything that Drizzle does.
type: 'integer',
notNull: true
}
// Add a new index to generated by Payload table:
schema.rawTables.posts.indexes.customColumnIdx = {
name: 'custom_column_idx',
unique: true,
on: ['custom_column']
}
return schema
},
],
})
```

View File

@@ -34,27 +34,42 @@ export default buildConfig({
## Options
| Option | Description |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
| Option | Description |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `idType` | A string of 'number', or 'uuid' that is used for the data type given to id columns. |
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
| `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows |
## Access to Drizzle
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
You can access Drizzle as follows:
To ensure type-safety, you need to generate Drizzle schema first with:
```sh
npx payload generate:db-schema
```
```text
payload.db.drizzle
Then, you can access Drizzle as follows:
```ts
// Import table from the generated file
import { posts } from './payload-generated-schema'
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
import { eq, sql, and } from '@payloadcms/db-sqlite/drizzle'
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
const posts = await payload.db.drizzle.query.posts.findMany()
// Drizzle's Select API https://orm.drizzle.team/docs/select
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
```
## Tables and relations
@@ -88,7 +103,7 @@ Runs before the schema is built. You can use this hook to extend your database s
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
import { integer, sqliteTable } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
sqliteAdapter({
beforeSchemaInit: [
@@ -168,7 +183,7 @@ The following example adds the `extra_integer_column` column and a composite ind
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { index, integer } from 'drizzle-orm/sqlite-core'
import { index, integer } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
import { buildConfig } from 'payload'
export default buildConfig({
@@ -210,3 +225,43 @@ export default buildConfig({
})
```
### Note for generated schema:
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
sqliteAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
// Add a new table
schema.rawTables.myTable = {
name: 'my_table',
columns: [{
name: 'my_id',
type: 'integer',
primaryKey: true
}],
}
// Add a new column to generated by Payload table:
schema.rawTables.posts.columns.customColumn = {
name: 'custom_column',
// Note that Payload SQL doesn't support everything that Drizzle does.
type: 'integer',
notNull: true
}
// Add a new index to generated by Payload table:
schema.rawTables.posts.indexes.customColumnIdx = {
name: 'custom_column_idx',
unique: true,
on: ['custom_column']
}
return schema
},
],
})
```

View File

@@ -125,8 +125,8 @@ powerful Admin UI.
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`collection`** \* | The `slug`s having the relationship field. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`where`** \* | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
@@ -145,10 +145,11 @@ _\* An asterisk denotes that a property is required._
You can control the user experience of the join field using the `admin` config properties. The following options are supported:
| Option | Description |
|------------------------|----------------------------------------------------------------------------------------|
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
| **`components.Label`** | Override the default Label of the Field Component. [More details](../admin/fields#label) |
| Option | Description |
|------------------------|---------------------------------------------------------------------------------------------------------------------------|
| **`defaultColumns`** | Array of field names that correspond to which columns to show in the relationship table. Default is the collection config. |
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
| **`components.Label`** | Override the default Label of the Field Component. [More details](../admin/fields#label) |
## Join Field Data

View File

@@ -212,6 +212,7 @@ Functions can be written to make use of the following argument properties:
- `user` - the authenticated user object
- `locale` - the currently selected locale string
- `req` - the `PayloadRequest` object
Here is an example of a `defaultValue` function:
@@ -227,7 +228,7 @@ export const myField: Field = {
name: 'attribution',
type: 'text',
// highlight-start
defaultValue: ({ user, locale }) =>
defaultValue: ({ user, locale, req }) =>
`${translation[locale]} ${user.name}`,
// highlight-end
}
@@ -235,7 +236,7 @@ export const myField: Field = {
<Banner type="success">
<strong>Tip:</strong>
You can use async `defaultValue` functions to fill fields with data from API requests.
You can use async `defaultValue` functions to fill fields with data from API requests or Local API using `req.payload`.
</Banner>
### Validation

View File

@@ -68,11 +68,12 @@ export const MyTextareaField: Field = {
The Textarea Field inherits all of the default options from the base [Field Admin Config](../admin/fields#admin-options), plus the following additional options:
| Option | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
| **`rows`** | Set the number of visible text rows in the textarea. Defaults to `2` if not specified. |
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
## Example

View File

@@ -141,3 +141,65 @@ export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job,
}
}
```
### Configuring task restoration
By default, if a task has passed previously and a workflow is re-run, the task will not be re-run. Instead, the output from the previous task run will be returned. This is to prevent unnecessary re-runs of tasks that have already passed.
You can configure this behavior through the `retries.shouldRestore` property. This property accepts a boolean or a function.
If `shouldRestore` is set to true, the task will only be re-run if it previously failed. This is the default behavior.
If `shouldRestore` this is set to false, the task will be re-run even if it previously succeeded, ignoring the maximum number of retries.
If `shouldRestore` is a function, the return value of the function will determine whether the task should be re-run. This can be used for more complex restore logic, e.g you may want to re-run a task up to X amount of times and then restore it for consecutive runs, or only re-run a task if the input has changed.
Example:
```ts
export default buildConfig({
// ...
jobs: {
tasks: [
{
slug: 'myTask',
retries: {
shouldRestore: false,
}
// ...
} as TaskConfig<'myTask'>,
]
}
})
```
Example - determine whether a task should be restored based on the input data:
```ts
export default buildConfig({
// ...
jobs: {
tasks: [
{
slug: 'myTask',
inputSchema: [
{
name: 'someDate',
type: 'date',
required: true,
},
],
retries: {
shouldRestore: ({ input }) => {
if(new Date(input.someDate) > new Date()) {
return false
}
return true
},
}
// ...
} as TaskConfig<'myTask'>,
]
}
})
```

View File

@@ -196,7 +196,7 @@ const examplePageConfig: CollectionConfig = {
},
// Note: if you override the `filterOptions` of the `parent` field,
// be sure to continue to prevent the document from referencing itself as the parent like this:
// filterOptions: ({ id }) => ({ id: {not_equals: id }})`
// filterOptions: ({ id }) => ({ id: {not_equals: id }})
},
),
createBreadcrumbsField(

View File

@@ -20,7 +20,7 @@ Querying a collection and automatically including related documents via `depth`
## Cross-Site Request Forgery (CSRF)
CSRF prevention will verify the authenticity of each request to your API to prevent a malicious action from another site from authorized users. See how to configure CSRF [here](/docs/authentication/overview#csrf-protection).
CSRF prevention will verify the authenticity of each request to your API to prevent a malicious action from another site from authorized users. See how to configure CSRF [here](/docs/authentication/cookies#csrf-attacks).
## Cross Origin Resource Sharing (CORS)
@@ -38,7 +38,7 @@ If you do not need GraphQL it is advised that you disable it altogether with the
Payload does not execute uploaded files on the server, but depending on your setup it may be used to transmit and store potentially dangerous files. If your configuration allows file uploads there is the potential that a bad actor uploads a malicious file that is then served to other users. Consider the following ways to mitigate the risks.
First, enable email [verification](/docs/authentication/overview#email-verification) when users are allowed to register new accounts and add other bot prevention services.
First, enable email [verification](/docs/authentication/email#email-verification) when users are allowed to register new accounts and add other bot prevention services.
Review that `create` and `update` access on file upload collections are as restrictive as your application needs allow. Consider limiting `read` access of uploaded user's files and how you might limit user uploaded files from being served outside of Payload.

View File

@@ -79,7 +79,7 @@ This allows you to add i18n translations scoped to your feature. This specific e
### Markdown Transformers#server-feature-markdown-transformers
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/lexical/converters#markdown-lexical).
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converters#markdown-lexical).
```ts
import { createServerFeature } from '@payloadcms/richtext-lexical';

View File

@@ -180,7 +180,7 @@ Notice how even the toolbars are features? That's how extensible our lexical edi
## Creating your own, custom Feature
You can find more information about creating your own feature in our [building custom feature docs](/docs/lexical/building-custom-features).
You can find more information about creating your own feature in our [building custom feature docs](/docs/rich-text/building-custom-features).
## TypeScript

View File

@@ -1,7 +1,11 @@
# NOTE: Change port of `PAYLOAD_PUBLIC_SITE_URL` if front-end is running on another server
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
# Database connection string
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
# Used to encrypt JWT tokens
PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
DATABASE_URI=mongodb://127.0.0.1/payload-example-auth
PAYLOAD_SECRET=PAYLOAD_AUTH_EXAMPLE_SECRET_KEY
# Used to share cookies across subdomains
COOKIE_DOMAIN=localhost

View File

@@ -1,15 +1,4 @@
module.exports = {
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
ignorePatterns: ['**/payload-types.ts'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
extends: ['@payloadcms'],
}

View File

@@ -1,4 +1,5 @@
build
dist
node_modules
package - lock.json.env
package-lock.json
.env

24
examples/auth/.swcrc Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {
"type": "es6"
}
}

View File

@@ -2,28 +2,27 @@
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, the principals are generally the same. To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
## Quick Start
To spin up this example locally, follow these steps:
To spin up this example locally, follow the steps below:
1. Clone this repo
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. Navigate into the project directory and install dependencies using your preferred package manager:
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
- `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. `cp .env.example .env` to copy the example environment variables
> \*NOTE: The --ignore-workspace flag is needed if you are running this example within the Payload monorepo to avoid workspace conflicts.
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
1. Start the server:
- Depending on your package manager, run `pnpm dev`, `yarn dev` or `npm run dev`
- When prompted, type `y` then `enter` to seed the database with sample data
1. Access the application:
- Open your browser and navigate to `http://localhost:3000` to access the homepage.
- Open `http://localhost:3000/admin` to access the admin panel.
1. Login:
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
1. `open http://localhost:3000` to access the home page
1. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
- Use the following credentials to log into the admin panel:
> `Email: demo@payloadcms.com` > `Password: demo`
## How it works
@@ -45,7 +44,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
import config from '../../payload.config'
export default async function AccountPage({ searchParams }) {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config: configPromise })
const { permissions, user } = await payload.auth({ headers })

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -3,41 +3,39 @@
"version": "1.0.0",
"description": "Payload authentication example.",
"license": "MIT",
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:schema": "payload-graphql generate:schema",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"seed": "npm run payload migrate:fresh",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/db-mongodb": "3.0.0-beta.24",
"@payloadcms/next": "3.0.0-beta.24",
"@payloadcms/richtext-slate": "3.0.0-beta.24",
"@payloadcms/ui": "3.0.0-beta.24",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/next": "latest",
"@payloadcms/richtext-slate": "latest",
"@payloadcms/ui": "latest",
"cross-env": "^7.0.3",
"next": "14.3.0-canary.68",
"payload": "3.0.0-beta.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^15.0.0",
"payload": "latest",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.51.3"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.1.6",
"@payloadcms/eslint-config": "^1.1.1",
"@swc/core": "^1.4.14",
"@swc/types": "^0.1.6",
"@types/node": "^20.11.25",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"dotenv": "^16.4.5",
"@payloadcms/graphql": "latest",
"@swc/core": "^1.6.13",
"@types/ejs": "^3.1.5",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"eslint": "^8.57.0",
"tsx": "^4.7.1",
"typescript": "5.4.4"
"eslint-config-next": "^15.0.0",
"tsx": "^4.16.2",
"typescript": "5.5.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import { Gutter } from '../Gutter'
import { HeaderNav } from './Nav'
import classes from './index.module.scss'
export const Header = () => {
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link className={classes.logo} href="/">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
/>
<Image
alt="Payload Logo"
height={30}
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
width={150}
/>
</picture>
</Link>
<HeaderNav />
</Gutter>
</header>
)
}

View File

@@ -13,7 +13,7 @@ import { AccountForm } from './AccountForm'
import classes from './index.module.scss'
export default async function Account() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { permissions, user } = await payload.auth({ headers })

View File

@@ -10,7 +10,7 @@ import { CreateAccountForm } from './CreateAccountForm'
import classes from './index.module.scss'
export default async function CreateAccount() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -10,7 +10,7 @@ import classes from './index.module.scss'
import { LoginForm } from './LoginForm'
export default async function Login() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -9,7 +9,7 @@ import classes from './index.module.scss'
import { LogoutPage } from './LogoutPage'
export default async function Logout() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -8,7 +8,7 @@ import { Gutter } from './_components/Gutter'
import { HydrateClientUser } from './_components/HydrateClientUser'
export default async function HomePage() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { permissions, user } = await payload.auth({ headers })

View File

@@ -9,7 +9,7 @@ import classes from './index.module.scss'
import { RecoverPasswordForm } from './RecoverPasswordForm'
export default async function RecoverPassword() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -9,7 +9,7 @@ import classes from './index.module.scss'
import { ResetPasswordForm } from './ResetPasswordForm'
export default async function ResetPassword() {
const headers = getHeaders()
const headers = await getHeaders()
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers })

View File

@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
params: Promise<{
segments: string[]
}
searchParams: {
}>
searchParams: Promise<{
[key: string]: string | string[]
}
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound

View File

@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
params: Promise<{
segments: string[]
}
searchParams: {
}>
searchParams: Promise<{
[key: string]: string | string[]
}
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page

View File

@@ -0,0 +1,5 @@
import { BeforeLogin as BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
export const importMap = {
'@/components/BeforeLogin#BeforeLogin': BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e,
}

View File

@@ -1,16 +1,32 @@
/* 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 type { ServerFunctionClient } from 'payload'
import '@payloadcms/next/css'
import { RootLayout } from '@payloadcms/next/layouts'
import config from '@payload-config'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -1,13 +1,62 @@
import type { CollectionConfig } from 'payload/types'
import { admins } from './access/admins'
import adminsAndUser from './access/adminsAndUser'
import { anyone } from './access/anyone'
import { checkRole } from './access/checkRole'
import { loginAfterCreate } from './hooks/loginAfterCreate'
import { protectRoles } from './hooks/protectRoles'
export const Users: CollectionConfig = {
slug: 'users',
auth: {
tokenExpiration: 28800, // 8 hours
cookies: {
sameSite: 'none',
secure: true,
domain: process.env.COOKIE_DOMAIN,
},
},
admin: {
useAsTitle: 'email',
},
auth: true,
access: {
read: adminsAndUser,
create: anyone,
update: adminsAndUser,
delete: admins,
admin: ({ req: { user } }) => checkRole(['admin'], user),
},
hooks: {
afterChange: [loginAfterCreate],
},
fields: [
// Email added by default
// Add more fields as needed
{
name: 'firstName',
type: 'text',
},
{
name: 'lastName',
type: 'text',
},
{
name: 'roles',
type: 'select',
hasMany: true,
saveToJWT: true,
hooks: {
beforeChange: [protectRoles],
},
options: [
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'user',
},
],
},
],
}

View File

@@ -1,6 +1,6 @@
import React from 'react'
const BeforeLogin: React.FC = () => {
export const BeforeLogin: React.FC = () => {
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
return (
<p>
@@ -13,5 +13,3 @@ const BeforeLogin: React.FC = () => {
}
return null
}
export default BeforeLogin

View File

@@ -7,16 +7,53 @@
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {};
globalsSelect: {};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -38,6 +75,24 @@ export interface User {
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?: {
relationTo: 'users';
value: string | User;
} | null;
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
@@ -72,6 +127,63 @@ export interface PayloadMigration {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
firstName?: T;
lastName?: T;
roles?: T;
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {

View File

@@ -2,28 +2,21 @@ import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { slateEditor } from '@payloadcms/richtext-slate'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { buildConfig } from 'payload/config'
import { buildConfig } from 'payload'
import { Users } from './collections/Users'
import BeforeLogin from './components/BeforeLogin'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
components: {
beforeLogin: [BeforeLogin],
beforeLogin: ['@/components/BeforeLogin#BeforeLogin'],
},
},
collections: [Users],
cors: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean),
csrf: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean),
cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
db: mongooseAdapter({
url: process.env.DATABASE_URI || '',
}),

View File

@@ -23,10 +23,25 @@
}
],
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["src/payload.config.ts"]
}
"@/*": [
"./src/*"
],
"@payload-config": [
"src/payload.config.ts"
],
"@payload-types": [
"src/payload-types.ts"
]
},
"target": "ES2022",
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

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 type { ServerFunctionClient } from 'payload'
import '@payloadcms/next/css'
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'

View File

@@ -22,7 +22,6 @@
"dotenv": "^8.2.0",
"escape-html": "^1.0.3",
"graphql": "^16.9.0",
"jsonwebtoken": "9.0.2",
"next": "^15.0.0",
"payload": "latest",
"payload-admin-bar": "^1.0.6",

View File

@@ -1,6 +1,5 @@
import type { CollectionSlug } from 'payload'
import type { CollectionSlug, PayloadRequest } from 'payload'
import jwt from 'jsonwebtoken'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
@@ -42,23 +41,20 @@ export async function GET(
return new Response('No path provided', { status: 404 })
}
if (!token) {
new Response('You are not allowed to preview this page', { status: 403 })
}
if (!path.startsWith('/')) {
new Response('This endpoint can only be used for internal previews', { status: 500 })
return new Response('This endpoint can only be used for internal previews', { status: 500 })
}
let user
try {
user = jwt.verify(token, payload.secret)
} catch (error) {
payload.logger.error({
err: error,
msg: 'Error verifying token for live preview',
user = await payload.auth({
req: req as unknown as PayloadRequest,
headers: req.headers,
})
} catch (error) {
payload.logger.error({ err: error }, 'Error verifying token for live preview')
return new Response('You are not allowed to preview this page', { status: 403 })
}
const draft = await draftMode()

View File

@@ -0,0 +1,20 @@
# Database connection string
DATABASE_URI=mongodb://127.0.0.1/payload-template-website
# Used to encrypt JWT tokens
PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to format links and URLs
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Allow robots to index the site (optional)
NEXT_PUBLIC_IS_LIVE=
# Used to preview drafts
PAYLOAD_PUBLIC_DRAFT_SECRET=demo-draft-secret
NEXT_PRIVATE_DRAFT_SECRET=demo-draft-secret
# Used to revalidate static pages
REVALIDATION_KEY=demo-revalation-key
NEXT_PRIVATE_REVALIDATION_KEY=demo-revalation-key

View File

@@ -0,0 +1,8 @@
module.exports = {
extends: 'next',
root: true,
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

10
examples/localization/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
build
dist / media
node_modules
.DS_Store
.env
.next
.vercel
# Payload default media upload directory
public/media/

View File

@@ -0,0 +1,14 @@
**/payload-types.ts
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
**/docs/**
tsconfig.json

View File

@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false
}

View File

@@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"command": "yarn dev",
"name": "Debug Website",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -0,0 +1,24 @@
FROM node:18.8-alpine as base
FROM base as builder
WORKDIR /home/node/app
COPY package*.json ./
COPY . .
RUN yarn install
RUN yarn build
FROM base as runtime
ENV NODE_ENV=production
WORKDIR /home/node/app
COPY package*.json ./
COPY yarn.lock ./
RUN yarn install --production
EXPOSE 3000
CMD ["node", "dist/server.js"]

View File

@@ -0,0 +1,32 @@
# Payload Localization Example (i18n)
This example is built based on an old version of the website template.
The objective is to show how to implement localization in a website. There is no guarantee that it will be kept up to date with the website template or the latest Payload enhancements.
To facilitate the localization process, this example uses the next-intl library.
## Setup
1. `cp .env.example .env` (copy the .env.example file to .env)
2. `pnpm install` (`pnpm i --ignore-workspaces` if you are running from the monorepo)
3. `pnpm run dev`
4. Seed your database in the admin panel (see below)
## Seed
To seed the database with a few pages, posts, and projects you can click the 'seed database' link from the admin panel.
The seed script will also create a demo user for demonstration purposes only:
- Demo Author
- Email: `demo-author@payloadcms.com`
- Password: `password`
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
## Important!
The seed script only creates translations in English and Spanish, so you will not see the website translated to other languages even if you see them in the dropdown menu.
You can translate documents to other languages from the admin panel.

View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app/(frontend)/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/utilities"
}
}

View File

@@ -0,0 +1,31 @@
version: '3'
services:
payload:
image: node:18-alpine
ports:
- '3000:3000'
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
working_dir: /home/node/app/
command: sh -c "yarn install && yarn dev"
depends_on:
- mongo
env_file:
- .env
mongo:
image: mongo:latest
ports:
- '27017:27017'
command:
- --storageEngine=wiredTiger
volumes:
- data:/data/db
logging:
driver: none
volumes:
data:
node_modules:

5
examples/localization/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -0,0 +1,28 @@
import { withPayload } from '@payloadcms/next/withPayload'
import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin()
import redirects from './redirects.js'
const NEXT_PUBLIC_SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => {
const url = new URL(item)
return {
hostname: url.hostname,
protocol: url.protocol.replace(':', ''),
}
}),
],
},
reactStrictMode: true,
redirects,
}
export default withNextIntl(withPayload(nextConfig))

View File

@@ -0,0 +1,74 @@
{
"name": "payload-localization-example",
"version": "1.0.0",
"description": "An example of how to use Payload's localization features",
"type": "module",
"scripts": {
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"ii": "cross-env NODE_OPTIONS=--no-deprecation pnpm --ignore-workspace install",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"lint:fix": "cross-env NODE_OPTIONS=--no-deprecation next lint --fix",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/db-mongodb": "latest",
"@payloadcms/live-preview-react": "latest",
"@payloadcms/next": "latest",
"@payloadcms/payload-cloud": "latest",
"@payloadcms/plugin-form-builder": "latest",
"@payloadcms/plugin-nested-docs": "latest",
"@payloadcms/plugin-redirects": "latest",
"@payloadcms/plugin-search": "latest",
"@payloadcms/plugin-seo": "latest",
"@payloadcms/richtext-lexical": "latest",
"@payloadcms/ui": "latest",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"geist": "^1.3.0",
"graphql": "^16.8.2",
"jsonwebtoken": "9.0.2",
"lucide-react": "^0.378.0",
"next": "^15.1.0",
"next-intl": "^3.23.2",
"payload": "latest",
"payload-admin-bar": "^1.0.6",
"prism-react-renderer": "^2.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "7.45.4",
"sharp": "0.32.6",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@tailwindcss/typography": "^0.5.13",
"@types/escape-html": "^1.0.2",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "22.5.4",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"autoprefixer": "^10.4.19",
"copyfiles": "^2.4.1",
"eslint": "^9.16.0",
"eslint-config-next": "15.1.0",
"postcss": "^8.4.38",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.3",
"typescript": "5.7.2"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"
}
}

10271
examples/localization/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
autoprefixer: {},
tailwindcss: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,15 @@
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
path {
fill: #0F0F0F;
}
@media (prefers-color-scheme: dark) {
path {
fill: white;
}
}
</style>
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,19 @@
const redirects = async () => {
return [
{
// internet explorer
destination: '/ie-incompatible.html',
has: [
{
type: 'header',
key: 'user-agent',
value: '(.*Trident.*)', // all ie browsers
},
],
permanent: false,
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
},
]
}
export default redirects

View File

@@ -0,0 +1,3 @@
import type { Access } from 'payload'
export const anyone: Access = () => true

View File

@@ -0,0 +1,9 @@
import type { AccessArgs } from 'payload'
import type { User } from '@/payload-types'
type isAuthenticated = (args: AccessArgs<User>) => boolean
export const authenticated: isAuthenticated = ({ req: { user } }) => {
return Boolean(user)
}

View File

@@ -0,0 +1,13 @@
import type { Access } from 'payload'
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
if (user) {
return true
}
return {
_status: {
equals: 'published',
},
}
}

View File

@@ -0,0 +1,15 @@
'use client'
import { useHeaderTheme } from '@/providers/HeaderTheme'
import React, { useEffect } from 'react'
const PageClient: React.FC = () => {
/* Force the header to be dark mode while we have an image behind it */
const { setHeaderTheme } = useHeaderTheme()
useEffect(() => {
setHeaderTheme('light')
}, [setHeaderTheme])
return <React.Fragment />
}
export default PageClient

View File

@@ -0,0 +1,108 @@
import type { Metadata } from 'next'
import { PayloadRedirects } from '@/components/PayloadRedirects'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import React, { cache } from 'react'
import { homeStatic } from '@/endpoints/seed/home-static'
import type { Page as PageType } from '@/payload-types'
import { RenderBlocks } from '@/blocks/RenderBlocks'
import { RenderHero } from '@/heros/RenderHero'
import { generateMeta } from '@/utilities/generateMeta'
import PageClient from './page.client'
import { TypedLocale } from 'payload'
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({
collection: 'pages',
draft: false,
limit: 1000,
overrideAccess: false,
})
const params = pages.docs
?.filter((doc) => {
return doc.slug !== 'home'
})
.map(({ slug }) => {
return { slug }
})
return params
}
type Args = {
params: Promise<{
slug?: string
locale: TypedLocale
}>
}
export default async function Page({ params: paramsPromise }: Args) {
const { slug = 'home', locale = 'en' } = await paramsPromise
const url = '/' + slug
let page: PageType | null
page = await queryPage({
slug,
locale,
})
// Remove this code once your website is seeded
if (!page && slug === 'home') {
page = homeStatic
}
if (!page) {
return <PayloadRedirects url={url} />
}
const { hero, layout } = page
return (
<article className="pt-16 pb-24">
<PageClient />
{/* Allows redirects for valid pages too */}
<PayloadRedirects disableNotFound url={url} />
<RenderHero {...hero} />
<RenderBlocks blocks={layout} locale={locale} />
</article>
)
}
export async function generateMetadata({ params: paramsPromise }): Promise<Metadata> {
const { slug = 'home', locale = 'en' } = await paramsPromise
const page = await queryPage({
slug,
locale,
})
return generateMeta({ doc: page })
}
const queryPage = cache(async ({ slug, locale }: { slug: string; locale: TypedLocale }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config: configPromise })
const result = await payload.find({
collection: 'pages',
draft,
limit: 1,
locale,
overrideAccess: draft,
where: {
slug: {
equals: slug,
},
},
})
return result.docs?.[0] || null
})

View File

@@ -0,0 +1,103 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: auto;
font-weight: auto;
}
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 240 5% 96%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 240 6% 90%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.2rem;
--success: 196 52% 74%;
--warning: 34 89% 85%;
--error: 10 100% 86%;
}
[data-theme='dark'] {
--background: 0 0% 0%;
--foreground: 210 40% 98%;
--card: 240 6% 10%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 240 4% 16%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--success: 196 100% 14%;
--warning: 34 51% 25%;
--error: 10 39% 43%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
html {
opacity: 0;
}
html[data-theme='dark'],
html[data-theme='light'] {
opacity: initial;
}

View File

@@ -0,0 +1,84 @@
import type { Metadata } from 'next'
import { cn } from 'src/utilities/cn'
import { GeistMono } from 'geist/font/mono'
import { GeistSans } from 'geist/font/sans'
import React from 'react'
import { AdminBar } from '@/components/AdminBar'
import { Footer } from '@/globals/Footer/Component'
import { Header } from '@/globals/Header/Component'
import { LivePreviewListener } from '@/components/LivePreviewListener'
import { Providers } from '@/providers'
import { InitTheme } from '@/providers/Theme/InitTheme'
import { mergeOpenGraph } from '@/utilities/mergeOpenGraph'
import { draftMode } from 'next/headers'
import { TypedLocale } from 'payload'
import './globals.css'
import { getMessages, setRequestLocale } from 'next-intl/server'
import { NextIntlClientProvider } from 'next-intl'
import { routing } from '@/i18n/routing'
import { notFound } from 'next/navigation'
type Args = {
children: React.ReactNode
params: Promise<{
locale: TypedLocale
}>
}
export default async function RootLayout({ children, params }: Args) {
const { locale } = await params
if (!routing.locales.includes(locale as any)) {
notFound()
}
setRequestLocale(locale)
const { isEnabled } = await draftMode()
const messages = await getMessages()
return (
<html
className={cn(GeistSans.variable, GeistMono.variable)}
lang={locale}
suppressHydrationWarning
>
<head>
<InitTheme />
<link href="/favicon.ico" rel="icon" sizes="32x32" />
<link href="/favicon.svg" rel="icon" type="image/svg+xml" />
</head>
<body>
<Providers>
<NextIntlClientProvider messages={messages}>
<AdminBar
adminBarProps={{
preview: isEnabled,
}}
/>
<LivePreviewListener />
<Header locale={locale} />
{children}
<Footer locale={locale} />
</NextIntlClientProvider>
</Providers>
</body>
</html>
)
}
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SERVER_URL || 'https://payloadcms.com'),
openGraph: mergeOpenGraph(),
twitter: {
card: 'summary_large_image',
creator: '@payloadcms',
},
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }))
}

View File

@@ -0,0 +1,21 @@
import Link from 'next/link'
import React from 'react'
import { Button } from '@/components/ui/button'
import { useTranslations } from 'next-intl'
export default function NotFound() {
const t = useTranslations()
return (
<div className="container py-28">
<div className="prose max-w-none">
<h1 style={{ marginBottom: 0 }}>404</h1>
<p className="mb-4">{t('page-not-found')}</p>
</div>
<Button asChild variant="default">
<Link href="/">{t('go-home')}</Link>
</Button>
</div>
)
}

View File

@@ -0,0 +1,86 @@
import { Metadata } from 'next'
// import PageTemplate from './[slug]/page'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import React, { cache } from 'react'
import { generateMeta } from '@/utilities/generateMeta'
import { TypedLocale } from 'payload'
import { PayloadRedirects } from '@/components/PayloadRedirects'
import { homeStatic } from '@/endpoints/seed/home-static'
import type { Page as PageType } from '@/payload-types'
import { RenderBlocks } from '@/blocks/RenderBlocks'
import { RenderHero } from '@/heros/RenderHero'
import PageClient from './[slug]/page.client'
type Args = {
params: Promise<{
slug?: string
locale: TypedLocale
}>
}
export default async function Page({ params: paramsPromise }: Args) {
const { slug = 'home', locale = 'en' } = await paramsPromise
const url = '/' + slug
let page: PageType | null
page = await queryPage({
slug,
locale,
})
// Remove this code once your website is seeded
if (!page && slug === 'home') {
page = homeStatic
}
if (!page) {
return <PayloadRedirects url={url} />
}
const { hero, layout } = page
return (
<article className="pt-16 pb-24">
<PageClient />
<PayloadRedirects disableNotFound url={url} />
<RenderHero {...hero} />
<RenderBlocks blocks={layout} locale={locale} />
</article>
)
}
export async function generateMetadata({ params }: Args): Promise<Metadata> {
const { locale = 'en', slug = 'home' } = await params
const page = await queryPage({
locale,
slug,
})
return generateMeta({ doc: page })
}
const queryPage = cache(async ({ locale, slug }: { locale: TypedLocale; slug: string }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config: configPromise })
const result = await payload.find({
collection: 'pages',
draft,
limit: 1,
overrideAccess: draft,
locale: locale,
where: {
slug: {
equals: slug,
},
},
})
return result.docs?.[0] || null
})

View File

@@ -0,0 +1,15 @@
'use client'
import { useHeaderTheme } from '@/providers/HeaderTheme'
import React, { useEffect } from 'react'
const PageClient: React.FC = () => {
/* Force the header to be dark mode while we have an image behind it */
const { setHeaderTheme } = useHeaderTheme()
useEffect(() => {
setHeaderTheme('dark')
}, [setHeaderTheme])
return <React.Fragment />
}
export default PageClient

View File

@@ -0,0 +1,103 @@
import type { Metadata } from 'next'
import { RelatedPosts } from '@/blocks/RelatedPosts/Component'
import { PayloadRedirects } from '@/components/PayloadRedirects'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import React, { cache } from 'react'
import RichText from '@/components/RichText'
import type { Post } from '@/payload-types'
import { PostHero } from '@/heros/PostHero'
import { generateMeta } from '@/utilities/generateMeta'
import PageClient from './page.client'
import { TypedLocale } from 'payload'
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
const posts = await payload.find({
collection: 'posts',
draft: false,
limit: 1000,
overrideAccess: false,
})
const params = posts.docs.map(({ slug }) => {
return { slug }
})
return params
}
type Args = {
params: Promise<{
slug?: string
locale?: TypedLocale
}>
}
export default async function Post({ params: paramsPromise }: Args) {
const { slug = '', locale = 'en' } = await paramsPromise
const url = '/posts/' + slug
const post = await queryPost({ slug, locale })
if (!post) return <PayloadRedirects url={url} />
return (
<article className="pt-16 pb-16">
<PageClient />
{/* Allows redirects for valid pages too */}
<PayloadRedirects disableNotFound url={url} />
<PostHero post={post} />
<div className="flex flex-col items-center gap-4 pt-8">
<div className="container lg:mx-0 lg:grid lg:grid-cols-[1fr_48rem_1fr] grid-rows-[1fr]">
<RichText
className="lg:grid lg:grid-cols-subgrid col-start-1 col-span-3 grid-rows-[1fr]"
content={post.content}
enableGutter={false}
/>
</div>
{post.relatedPosts && post.relatedPosts.length > 0 && (
<RelatedPosts
className="mt-12"
docs={post.relatedPosts.filter((post) => typeof post === 'object')}
/>
)}
</div>
</article>
)
}
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
const { slug = '', locale = 'en' } = await paramsPromise
const post = await queryPost({ slug, locale })
return generateMeta({ doc: post })
}
const queryPost = cache(async ({ slug, locale }: { slug: string; locale: TypedLocale }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config: configPromise })
const result = await payload.find({
collection: 'posts',
draft,
limit: 1,
overrideAccess: draft,
locale,
where: {
slug: {
equals: slug,
},
},
})
return result.docs?.[0] || null
})

View File

@@ -0,0 +1,15 @@
'use client'
import { useHeaderTheme } from '@/providers/HeaderTheme'
import React, { useEffect } from 'react'
const PageClient: React.FC = () => {
/* Force the header to be dark mode while we have an image behind it */
const { setHeaderTheme } = useHeaderTheme()
useEffect(() => {
setHeaderTheme('light')
}, [setHeaderTheme])
return <React.Fragment />
}
export default PageClient

View File

@@ -0,0 +1,67 @@
import type { Metadata } from 'next/types'
import { CollectionArchive } from '@/components/CollectionArchive'
import { PageRange } from '@/components/PageRange'
import { Pagination } from '@/components/Pagination'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import PageClient from './page.client'
import { TypedLocale } from 'payload'
import { getTranslations, setRequestLocale } from 'next-intl/server'
export const revalidate = 600
type Args = {
params: Promise<{
locale: TypedLocale
}>
}
export default async function Page({ params }: Args) {
const { locale = 'en' } = await params
const t = await getTranslations()
const payload = await getPayload({ config: configPromise })
const posts = await payload.find({
collection: 'posts',
locale,
depth: 1,
limit: 12,
overrideAccess: false,
})
return (
<div className="pt-24 pb-24">
<PageClient />
<div className="container mb-16">
<div className="prose dark:prose-invert max-w-none">
<h1>{t('posts')}</h1>
</div>
</div>
<div className="container mb-8">
<PageRange
collection="posts"
currentPage={posts.page}
limit={12}
totalDocs={posts.totalDocs}
/>
</div>
<CollectionArchive posts={posts.docs} />
<div className="container">
{posts.totalPages > 1 && posts.page && (
<Pagination page={posts.page} totalPages={posts.totalPages} />
)}
</div>
</div>
)
}
export function generateMetadata(): Metadata {
return {
title: `Payload Website Template Posts`,
}
}

View File

@@ -0,0 +1,15 @@
'use client'
import { useHeaderTheme } from '@/providers/HeaderTheme'
import React, { useEffect } from 'react'
const PageClient: React.FC = () => {
/* Force the header to be dark mode while we have an image behind it */
const { setHeaderTheme } = useHeaderTheme()
useEffect(() => {
setHeaderTheme('light')
}, [setHeaderTheme])
return <React.Fragment />
}
export default PageClient

View File

@@ -0,0 +1,94 @@
import type { Metadata } from 'next/types'
import { CollectionArchive } from '@/components/CollectionArchive'
import { PageRange } from '@/components/PageRange'
import { Pagination } from '@/components/Pagination'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import PageClient from './page.client'
import { notFound } from 'next/navigation'
import { getTranslations } from 'next-intl/server'
import { TypedLocale } from 'payload'
export const revalidate = 600
type Args = {
params: Promise<{
pageNumber: string
locale: TypedLocale
}>
}
export default async function Page({ params: paramsPromise }: Args) {
const { pageNumber, locale } = await paramsPromise
const payload = await getPayload({ config: configPromise })
const t = await getTranslations()
const sanitizedPageNumber = Number(pageNumber)
if (!Number.isInteger(sanitizedPageNumber)) notFound()
const posts = await payload.find({
collection: 'posts',
depth: 1,
limit: 12,
locale,
page: sanitizedPageNumber,
overrideAccess: false,
})
return (
<div className="pt-24 pb-24">
<PageClient />
<div className="container mb-16">
<div className="prose dark:prose-invert max-w-none">
<h1>{t('posts')}</h1>
</div>
</div>
<div className="container mb-8">
<PageRange
collection="posts"
currentPage={posts.page}
limit={12}
totalDocs={posts.totalDocs}
/>
</div>
<CollectionArchive posts={posts.docs} />
<div className="container">
{posts.totalPages > 1 && posts.page && (
<Pagination page={posts.page} totalPages={posts.totalPages} />
)}
</div>
</div>
)
}
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
const { pageNumber } = await paramsPromise
return {
title: `Payload Website Template Posts Page ${pageNumber || ''}`,
}
}
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
const posts = await payload.find({
collection: 'posts',
depth: 0,
limit: 10,
draft: false,
overrideAccess: false,
})
const pages: { pageNumber: string }[] = []
for (let i = 1; i <= posts.totalPages; i++) {
pages.push({ pageNumber: String(i) })
}
return pages
}

View File

@@ -0,0 +1,15 @@
'use client'
import { useHeaderTheme } from '@/providers/HeaderTheme'
import React, { useEffect } from 'react'
const PageClient: React.FC = () => {
/* Force the header to be dark mode while we have an image behind it */
const { setHeaderTheme } = useHeaderTheme()
useEffect(() => {
setHeaderTheme('light')
}, [setHeaderTheme])
return <React.Fragment />
}
export default PageClient

View File

@@ -0,0 +1,88 @@
import type { Metadata } from 'next/types'
import { CollectionArchive } from '@/components/CollectionArchive'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import { Post } from '@/payload-types'
import { Search } from '@/search/Component'
import PageClient from './page.client'
import { getTranslations } from 'next-intl/server'
import { TypedLocale } from 'payload'
type Args = {
searchParams: Promise<{
q: string
}>
params: Promise<{
locale: TypedLocale
}>
}
export default async function Page({
searchParams: searchParamsPromise,
params: paramsPromise,
}: Args) {
const { q: query } = await searchParamsPromise
const { locale } = await paramsPromise
const payload = await getPayload({ config: configPromise })
const t = await getTranslations()
const posts = await payload.find({
collection: 'search',
depth: 1,
limit: 12,
locale,
...(query
? {
where: {
or: [
{
title: {
like: query,
},
},
{
'meta.description': {
like: query,
},
},
{
'meta.title': {
like: query,
},
},
{
slug: {
like: query,
},
},
],
},
}
: {}),
})
return (
<div className="pt-24 pb-24">
<PageClient />
<div className="container mb-16">
<div className="prose dark:prose-invert max-w-none">
<h1 className="sr-only">{t('search')}</h1>
<Search />
</div>
</div>
{posts.totalDocs > 0 ? (
<CollectionArchive posts={posts.docs as unknown as Post[]} />
) : (
<div className="container">No results found.</div>
)}
</div>
)
}
export function generateMetadata(): Metadata {
return {
title: `Payload Website Template Search`,
}
}

View File

@@ -0,0 +1,7 @@
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}

View File

@@ -0,0 +1,7 @@
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}

View File

@@ -0,0 +1,91 @@
import jwt from 'jsonwebtoken'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { CollectionSlug, TypedLocale } from 'payload'
const payloadToken = 'payload-token'
export async function GET(
req: Request & {
cookies: {
get: (name: string) => {
value: string
}
}
},
): Promise<Response> {
const payload = await getPayload({ config: configPromise })
const token = req.cookies.get(payloadToken)?.value
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
const collection = searchParams.get('collection') as CollectionSlug
const slug = searchParams.get('slug')
const previewSecret = searchParams.get('previewSecret')
if (previewSecret) {
return new Response('You are not allowed to preview this page', { status: 403 })
} else {
if (!path) {
return new Response('No path provided', { status: 404 })
}
if (!collection) {
return new Response('No path provided', { status: 404 })
}
if (!slug) {
return new Response('No path provided', { status: 404 })
}
if (!token) {
new Response('You are not allowed to preview this page', { status: 403 })
}
if (!path.startsWith('/')) {
new Response('This endpoint can only be used for internal previews', { status: 500 })
}
let user
try {
user = jwt.verify(token, payload.secret)
} catch (error) {
payload.logger.error('Error verifying token for live preview:', error)
}
const draft = await draftMode()
// You can add additional checks here to see if the user is allowed to preview this page
if (!user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
// Verify the given slug exists
try {
const docs = await payload.find({
collection: collection,
draft: true,
locale: path.split('/')[0] as TypedLocale,
where: {
slug: {
equals: slug,
},
},
})
if (!docs.docs.length) {
return new Response('Document not found', { status: 404 })
}
} catch (error) {
payload.logger.error('Error verifying token for live preview:', error)
}
draft.enable()
redirect(path)
}
}

View File

@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params, searchParams, importMap })
export default NotFound

View File

@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap })
export default Page

View File

@@ -0,0 +1,61 @@
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { default as default_035a063f0e4325a280e3cc815d2ec5d7 } from '@/components/AfterDashboard'
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
export const importMap = {
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell':
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalField':
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient':
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient':
FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#HeadingFeatureClient':
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient':
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#BoldFeatureClient':
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#ItalicFeatureClient':
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#LinkFeatureClient':
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/plugin-seo/client#OverviewComponent':
OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#MetaTitleComponent':
MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#MetaImageComponent':
MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#MetaDescriptionComponent':
MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#PreviewComponent':
PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@/fields/slug/SlugComponent#SlugComponent': SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient':
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#BlocksFeatureClient':
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/plugin-search/client#LinkToDoc': LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
'@payloadcms/plugin-search/client#ReindexButton': ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
'@/components/AfterDashboard#default': default_035a063f0e4325a280e3cc815d2ec5d7,
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
}

View File

@@ -0,0 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)

View File

@@ -0,0 +1,32 @@
/* 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 React from 'react'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import type { ServerFunctionClient } from 'payload'
import config from '@payload-config'
import './custom.scss'
import { importMap } from './admin/importMap'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout importMap={importMap} config={configPromise} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -0,0 +1,76 @@
import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import RichText from '@/components/RichText'
import { CollectionArchive } from '@/components/CollectionArchive'
import { TypedLocale } from 'payload'
export const ArchiveBlock: React.FC<
ArchiveBlockProps & {
id?: string
locale: TypedLocale
}
> = async (props) => {
const {
id,
categories,
introContent,
limit: limitFromProps,
populateBy,
selectedDocs,
locale,
} = props
const limit = limitFromProps || 3
let posts: Post[] = []
if (populateBy === 'collection') {
const payload = await getPayload({ config: configPromise })
const flattenedCategories = categories?.map((category) => {
if (typeof category === 'object') return category.id
else return category
})
const fetchedPosts = await payload.find({
collection: 'posts',
depth: 1,
locale,
limit,
...(flattenedCategories && flattenedCategories.length > 0
? {
where: {
categories: {
in: flattenedCategories,
},
},
}
: {}),
})
posts = fetchedPosts.docs
} else {
if (selectedDocs?.length) {
const filteredSelectedPosts = selectedDocs.map((post) => {
if (typeof post.value === 'object') return post.value
}) as Post[]
posts = filteredSelectedPosts
}
}
return (
<div className="my-16" id={`block-${id}`}>
{introContent && (
<div className="container mb-16">
<RichText className="ml-0 max-w-[48rem]" content={introContent} enableGutter={false} />
</div>
)}
<CollectionArchive posts={posts} />
</div>
)
}

View File

@@ -0,0 +1,95 @@
import type { Block } from 'payload'
import {
FixedToolbarFeature,
HeadingFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
export const Archive: Block = {
slug: 'archive',
interfaceName: 'ArchiveBlock',
fields: [
{
name: 'introContent',
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
]
},
}),
label: 'Intro Content',
},
{
name: 'populateBy',
type: 'select',
defaultValue: 'collection',
options: [
{
label: 'Collection',
value: 'collection',
},
{
label: 'Individual Selection',
value: 'selection',
},
],
},
{
name: 'relationTo',
type: 'select',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'collection',
},
defaultValue: 'posts',
label: 'Collections To Show',
options: [
{
label: 'Posts',
value: 'posts',
},
],
},
{
name: 'categories',
type: 'relationship',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'collection',
},
hasMany: true,
label: 'Categories To Show',
relationTo: 'categories',
},
{
name: 'limit',
type: 'number',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'collection',
step: 1,
},
defaultValue: 10,
label: 'Limit',
},
{
name: 'selectedDocs',
type: 'relationship',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'selection',
},
hasMany: true,
label: 'Selection',
relationTo: ['posts'],
},
],
labels: {
plural: 'Archives',
singular: 'Archive',
},
}

View File

@@ -0,0 +1,26 @@
import type { BannerBlock as BannerBlockProps } from 'src/payload-types'
import { cn } from 'src/utilities/cn'
import React from 'react'
import RichText from '@/components/RichText'
type Props = {
className?: string
} & BannerBlockProps
export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
return (
<div className={cn('mx-auto my-8 w-full', className)}>
<div
className={cn('border py-3 px-6 flex items-center rounded', {
'border-border bg-card': style === 'info',
'border-error bg-error/30': style === 'error',
'border-success bg-success/30': style === 'success',
'border-warning bg-warning/30': style === 'warning',
})}
>
<RichText content={content} enableGutter={false} enableProse={false} />
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
import type { Block } from 'payload'
import {
FixedToolbarFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
export const Banner: Block = {
slug: 'banner',
fields: [
{
name: 'style',
type: 'select',
defaultValue: 'info',
options: [
{ label: 'Info', value: 'info' },
{ label: 'Warning', value: 'warning' },
{ label: 'Error', value: 'error' },
{ label: 'Success', value: 'success' },
],
required: true,
},
{
name: 'content',
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
},
}),
label: false,
required: true,
},
],
interfaceName: 'BannerBlock',
}

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