Compare commits

...

83 Commits

Author SHA1 Message Date
Paul Popus
dc8c6d3261 commit progress 2025-06-24 19:42:45 +01:00
Paul Popus
4a088eb9f4 update lock file 2025-06-10 22:52:44 +01:00
Paul Popus
884563c246 update test suite 2025-06-10 22:52:24 +01:00
Paul Popus
ebf23dd8a7 update packages 2025-06-10 22:51:52 +01:00
Paul Popus
dfd8bd1998 Merge branch 'main' into feat/ecommerce-package 2025-06-04 20:52:38 +01:00
Paul Popus
88f51b09c4 update test 2025-06-04 20:34:59 +01:00
Paul Popus
a3b28715f8 update 2025-06-04 20:33:05 +01:00
Jarrod Flesch
337f6188da feat: optionally exclude collection documents from appearing in browse-by-folder (#12654)
Adds configurations for browse-by-folder document results. This PR
**does NOT** allow for filtering out folders on a per collection basis.
That will be addressed in a future PR 👍

### Disable browse-by-folder all together
```ts
type RootFoldersConfiguration = {
  /**
   * If true, the browse by folder view will be enabled
   *
   * @default true
   */
  browseByFolder?: boolean
  // ...rest of type
}
```

### Remove document types from appearing in the browse by folder view
```ts
type CollectionFoldersConfiguration =
  | boolean
  | {
      /**
       * If true, the collection documents will be included in the browse by folder view
       *
       * @default true
       */
      browseByFolder?: boolean
    }
```

### Misc
Fixes https://github.com/payloadcms/payload/issues/12631 where adding
folders.collectionOverrides was being set on the client config - it
should be omitted.

Fixes an issue where `baseListFilters` were not being respected.
2025-06-04 13:22:26 -04:00
Elliot DeNolf
48218bccb5 chore: fix lint warnings for default exports, unused imports, unused err in catch (#12666)
Fix various lint warnings in payload package. 

387 warnings -> 215 warnings

- Migrate (most) default exports to named
- Remove unused imports
- Rename unused errors in catch statements to `ignore`
2025-06-04 10:15:59 -04:00
Said Akhrarov
bd512f1eda fix(db-postgres): ensure deletion of numbers and texts in upsertRow (#11787)
### What?
This PR fixes an issue while using `text` & `number` fields with
`hasMany: true` where the last entry would be unreachable, and thus
undeletable, because the `transformForWrite` function did not track
these rows for deletion. This causes values that should've been deleted
to remain in the edit view form, as well as the db, after a submission.

This PR also properly threads the placeholder value from
`admin.placeholder` to `text` & `number` `hasMany: true` fields.

### Why?
To remove rows from the db when a submission is made where these fields
are empty arrays, and to properly show an appropriate placeholder when
one is set in config.

### How?
Adjusting `transformForWrite` and the `traverseFields` to keep track of
rows for deletion.

Fixes #11781

Before:


[Editing---Post-dbpg-before--Payload.webm](https://github.com/user-attachments/assets/5ba1708a-2672-4b36-ac68-05212f3aa6cb)

After:


[Editing---Post--dbpg-hasmany-after-Payload.webm](https://github.com/user-attachments/assets/1292e998-83ff-49d0-aa86-6199be319937)
2025-06-04 10:13:46 -04:00
Sasha
c08cdff498 fix(db-postgres): in query with null (#12661)
Previously, this was possible in MongoDB but not in Postgres/SQLite
(having `null` in an `in` query)
```
const { docs } = await payload.find({
  collection: 'posts',
  where: { text: { in: ['text-1', 'text-3', null] } },
})
```
This PR fixes that behavior
2025-06-03 20:56:10 -04:00
Paul
cbc37d84bd fix(translations): add missing import for lv locale from date-fns (#12577)
We added support for Latvian recently but this wasn't added to the
date-fns locale imports
2025-06-03 22:35:51 +00:00
Paul
76bf459ff2 fix(ui): formatDate and formatTimeToNow utility type error on i18n arg to support I18nClient too (#12576)
In one of the versions we've changed the type of the argument from
`I18n<any, any>` to `I18n<unknown, unknown>` and this has caused some
issues with TS resolving the type compatibility in the `formatDate`
utility so it no longer supports `I18nClient`.

This type change happened in
https://github.com/payloadcms/payload/pull/10030
2025-06-03 22:30:38 +00:00
Alessio Gravili
30bb749e25 ci: skip flaky test on supabase (#12667)
This disables running the "`can reliably run workflows with parallel
tasks`" int test on supabase. For unknown reasons, it fails most of the
time.
2025-06-03 16:24:15 -04:00
Alessio Gravili
2bd098c9ea perf(ui): prevent unnecessary client config sanitization (#12665)
- The `ConfigProvider` was unnecessarily sanitizing the client config
twice on initial render, leading to an unnecessary re-render. Now it
only happens once
- Memoizes the context value to prevent accidental, unnecessary
re-renders of consumers
2025-06-03 18:56:48 +00:00
Paul
08ec837339 fix(ui): correctly thread through the autoComplete attribute from admin config to the text input (#12473)
We've already supported `autoComplete` on admin config for text fields
but it wasn't being threaded through to the text input element so it
couldn't be applied.
2025-06-03 10:54:51 -07:00
Paul
505eaa2bba docs: update seo plugin tabbedUI docs to mention potential pitfalls with the config option (#12549)
Closes https://github.com/payloadcms/payload/issues/12355

TabbedUI doesn't always work as intended and it can be affected by
existing fields or the order of other plugins, this mentions that and
links to the recommended direct use of fields example.
2025-06-03 17:44:46 +00:00
Germán Jabloñski
6ec21a53ff chore: migrate to TypeScript strict in Payload package (enable strictNullChecks) - #3 (#12586)
Important: An intentional effort is being made during migration to not
modify runtime behavior. This implies that there will be several
assertions, non-null assertions, and @ts-expect-error. This philosophy
applies only to migrating old code to TypeScript strict, not to writing
new code. For a more detailed justification for this reasoning,
https://github.com/payloadcms/payload/pull/11840#discussion_r2021975897.

In this PR, instead of following the approach of migrating a subset of
files, I'm migrating all files by disabling a specific rule. In this
case, `strictNullChecks`.

`strictNullChecks` is a good rule to start the migration with because
it's easy to silence with non-null assertions or optional chainings.
Additionally, almost all ts strict errors are due to this rule.

This PR improves 200+ files, leaving only 68 remaining to migrate to
strict mode in the payload package.
2025-06-03 14:43:37 +00:00
Jessica Rynkar
625d8d9319 feat(ui): adds new editMenuItems custom component (#12649)
## What
Adds a new custom component called `editMenuItems` that can be used in
the document view.
This options allows users to inject their own custom components into the
dropdown menu found in the document controls (the 3 dot menu), the
provided component(s) will be added below the default existing actions
(Create New, Duplicate, Delete and so on).

## Why
To increase flexibility and customization for users who wish to add
functionality to this menu. This provides a clean and consistent way to
add additional actions without needing to override or duplicate existing
UI logic.

## How
- Introduced the `editMenuItems` slot in the document controls dropdown
(three-dot menu) - in edit and preview tabs.
- Added documentation and tests to cover this new custom component

#### Testing
Use the `admin` test suite and go to the `edit menu items` collection
2025-06-03 14:57:48 +01:00
Paul
a9ff375cc0 fix(ui): clear miliseconds in date fields unless theyre explicitly provided in the display format (#12650)
Fixes https://github.com/payloadcms/payload/issues/12532

Normally we clear any values when picking a date such that your hour,
minutes and seconds are normalised to 0 unless specified. Equally when
you specify a time we will normalise seconds so that only minutes are
relevant as configured.

Miliseconds were never removed from the actual date value and whatever
milisecond the editor was in was that value that was being added.
There's this [abandoned
issue](https://github.com/Hacker0x01/react-datepicker/issues/1991) from
the UI library `react-datepicker` as it's not something configurable.

This fixes that problem by making sure that miliseconds are always 0
unless the `displayFormat` includes `SSS` as an intention to show and
customise them.

This also caused [issues with scheduled
jobs](https://github.com/payloadcms/payload/issues/12566) if things were
slightly out of order or not being scheduled in the expected time
interval.
2025-06-03 02:44:52 -07:00
Alessio Gravili
0ceb96b12d ci: ability to test against turbopack (#12652)
This adds a new `tests-e2e-turbo` CI step that runs our e2e test suite
against turbo. This will ensure that we can guarantee full support for
turbopack.

Our CI runners are already at capacity, so the turbo steps will only run
if the `tests-e2e-turbo` label is set on the PR.
2025-06-03 00:05:05 +00:00
Alessio Gravili
319d3355de feat: improve turbopack compatibility (#11376)
This PR introduces a few changes to improve turbopack compatibility and
ensure e2e tests pass with turbopack enabled

## Changes to improve turbopack compatibility
- Use correct sideEffects configuration to fix scss issues
- Import scss directly instead of duplicating our scss rules
- Fix some scss rules that are not supported by turbopack
- Bump Next.js and all other dependencies used to build payload

## Changes to get tests to pass

For an unknown reason, flaky tests flake a lot more often in turbopack.
This PR does the following to get them to pass:
- add more `wait`s
- fix actual flakes by ensuring previous operations are properly awaited

## Blocking turbopack bugs
- [X] https://github.com/vercel/next.js/issues/76464
  - Fix PR: https://github.com/vercel/next.js/pull/76545
  - Once fixed: change `"sideEffectsDisabled":` back to `"sideEffects":`
  
## Non-blocking turbopack bugs
- [ ] https://github.com/vercel/next.js/issues/76956

## Related PRs

https://github.com/payloadcms/payload/pull/12653
https://github.com/payloadcms/payload/pull/12652
2025-06-02 22:01:07 +00:00
Sasha
2b40e0f21f feat: polymorphic join querying by fields that don't exist in every collection (#12648)
This PR makes it possible to do polymorphic join querying by fields that
don't exist in all collections specified in `field.collection`, for
example:
```
const result = await payload.find({
  collection: 'payload-folders',
  joins: {
    documentsAndFolders: {
      where: {
        and: [
          {
            relationTo: {
              in: ['folderPoly1', 'folderPoly2'],
            },
          },
          {
            folderPoly2Title: { // this field exists only in the folderPoly2 collection, before it'd throw a query error.
              equals: 'Poly 2 Title',
            },
          },
        ],
      },
    },
  },
})
```

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-06-03 00:48:07 +03:00
Alessio Gravili
30dd9a23a3 refactor(ui): improve relationship field option loading reliability using queues (#12653)
This PR uses the new `useQueue` hook for relationship react-select field
for loading options. This will reduce flakiness in our CI and ensure the
following:
- most recently triggered options loading request will not have its
result overwritten by a previous, delayed request
- reduce unnecessary, parallel requests - outdated requests are
discarded from the queue if a newer request exist
2025-06-02 21:33:41 +00:00
Jacob
c639c5f278 fix(next): cannot override tab of default views (#11789)
### What?

TypeScript says that it is possible to modify the tab of the default
view however, when you specify the path to the custom component, nothing
happens. I fixed it.

### How?

If a Component for the tab of the default view is defined in the config,
I return that Component instead of DocumentTab

### Example Configuration

config.ts
```ts
export const MenuGlobal: GlobalConfig = {
  slug: menuSlug,
  fields: [
    {
      name: 'globalText',
      type: 'text',
    },
  ],
  admin: {
    components: {
      views: {
        edit: {
          api: {
            tab: {
              Component: './TestComponent.tsx',
            },
          },
        },
      },
    },
  },
}
```
./TestComponent.tsx
```tsx
const TestComponent = () => 'example'

export default TestComponent
```


### Before
![Screenshot 2025-03-20 at 08 42
06](https://github.com/user-attachments/assets/2acc0950-847f-44c5-bedf-660c5c3747a0)

### After
![Screenshot 2025-03-20 at 08 43
06](https://github.com/user-attachments/assets/c3917d02-abfb-4f80-9235-cc1ba784586d)

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-06-02 14:51:33 -04:00
Patrik
05eeddba7c fix: correctly detect glb & gltf mimetypes during upload (#12623)
### What?

The browser was incorrectly setting the mimetype for `.glb` and `.gltf`
files to `application/octet-stream` when uploading when they should be
receiving proper types consistent with `glb` and `gltf`.

This patch adds logic to infer the correct `MIME` type for `.glb` files
(`model/gltf-binary`) & `gltf` files (`model/gltf+json`) based on file
extension during multipart processing, ensuring consistent MIME type
detection regardless of browser behavior.

Fixes #12620
2025-06-02 11:26:26 -07:00
Tobias Odendahl
08a6f88a4b fix(ui): reset columns state throwing errors (#11903)
### What?
Fixes `resetColumnsState` in `useTableColumns` react hook.

### Why?
`resetColumnsState` threw errors when being executed, e.g. `Uncaught (in
promise) TypeError: Cannot read properties of undefined (reading
'findIndex')`

### How?
Removes unnecessary parsing of URL query parameters in
`setActiveColumns` when resetting columns.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-06-02 14:24:00 -04:00
Said Akhrarov
ede5c671b8 fix(plugin-seo): thread allowCreate to meta image component (#12624)
<!--

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 with `plugin-seo` where the `MetaImageComponent`
would not allow creating a new upload document from the field.

### Why?
To allow users to upload new media documents for use as a meta image.

### How?
Threads `allowCreate` through to the underlying upload input.

Fixes #12616

Before:

![image](https://github.com/user-attachments/assets/44ec32c7-1912-4fc3-9b8a-f5deb167320b)

After:

![image](https://github.com/user-attachments/assets/0dba1f75-78b6-4472-af38-6178f2ab26ea)
2025-06-02 14:11:24 +00:00
Sasha
684c43604a docs: missing dash (#12644) 2025-06-02 14:39:44 +03:00
Germán Jabloñski
8199a7d32a fix(richtext-lexical): export defaultColors for use in client components (#12627)
Fixes #12621

Should be imported from: 
`@payloadcms/richtext-lexical/client`
2025-05-31 00:47:41 +00:00
Anyu Jiang
6f8cff7764 refactor(translations): correct i18n translation for Mandarin (#12561)
Correct translations for Mandarin. Mainly for terms like: locale,
document, item etc.
2025-05-30 14:37:03 -07:00
Germán Jabloñski
89ced5ec6b fix(richtext-lexical): enable select inputs with ctrl+a or cmd+a (#12453)
Fixes #6871

To review this PR, use `pnpm dev lexical` and the auto-created document
in the `lexical fields` collection. Select any input within the blocks
and press `cmd+a`. The selection should contain the entire input.

I made sure that `cmd+a` still works fine inside the editor but outside
of inputs.
2025-05-30 18:28:51 -03:00
Jacob Fletcher
836fd86090 fix(cpa): generate .env when using the --example flag (#12572)
When cloning a new project from the examples dir via create-payload-app,
the corresponding `.env` file is not being generated. This is because
the `--example` flag does not prompt for database credentials, which
ultimately skips this step.

For example:

```bash
npx create-payload-app --example live-preview
```

The result will include the provided `.env.example`, but lacks a `.env`.

We were previously writing to the `.env.example` file, which is
unexpected. We should only be writing to the `.env` file itself. To do
this, we only write the `.env.example` to memory as needed, instead of
the file system.

This PR also simplifies the logic needed to set default vars, and
improves the testing coverage overall.
2025-05-30 14:26:57 -04:00
Sasha
7c094dc572 docs: building without a db connection (#12607)
Closes https://github.com/payloadcms/payload/issues/12605

Adds documentation for one of the most common problems - building a site
without a database connection (and why Payload may even need that).
2025-05-30 11:57:02 -04:00
Jacob Fletcher
c83e791014 fix(live-preview): correct type inference (#12619)
Type inferences broke as a result of migrating to ts strict mode in
#12298. This leads to compile-time errors that may prevent build.

Here is an example:

```ts
export interface Page {
  id: string;
  slug: string;
  title: string;
  // ...
}

/** 
* Type 'Page' does not satisfy the constraint 'Record<string, unknown>'.
* Index signature for type 'string' is missing in type 'Page'.
*/
const { data } = useLivePreview<Page>({
  depth: 2,
  initialData: initialPage,
  serverURL: PAYLOAD_SERVER_URL,
})
```

The problem is that Payload generated type _interfaces_ do not satisfy
the `Record<string, unknown>` type. This is because interfaces are a
possible target for declaration merging, so their properties are not
fully known. More details on this
[here](https://github.com/microsoft/TypeScript/issues/42825).

This PR also cleans up the JSDocs.
2025-05-30 15:40:15 +00:00
Patrik
6119d89fa5 fix(ui): upload action button styles (#12592)
### What

The upload action buttons had extra top & bottom `margin` extended on
them from the `.btn` class which caused the upload-actions container to
be larger than the thumbnail image.

Can be seen below:
![Screenshot 2025-05-28 at 1 04
57 PM](https://github.com/user-attachments/assets/d1a9ff8a-ff69-4c62-bbde-9deda6721ad3)

### Fix

To fix this issue, we've removed the bottom margin to allow the
thumbnail image control the height of the component.

#### Before
![Screenshot 2025-05-28 at 1 04
46 PM](https://github.com/user-attachments/assets/61f6dc9a-bf9d-411e-8d66-d50d27a328e9)

#### After
![Screenshot 2025-05-28 at 1 05
29 PM](https://github.com/user-attachments/assets/7687f3e8-e699-4a16-964d-20072e63d10f)
2025-05-30 15:08:55 +00:00
Paul
d5611953a7 fix: allow unnamed group fields to not set a label at all (#12580)
Technically you could already set `label: undefined` and it would be
supported by group fields but the types didn't reflect this.

So now you can create an unnamed group field like this:

```ts
{
      type: 'group',
      fields: [
        {
          type: 'text',
          name: 'insideGroupWithNoLabel',
        },
      ],
    },
```

This will remove the label while still visually grouping the fields.

![image](https://github.com/user-attachments/assets/ecb0b364-9cff-4d71-bf9f-86961915aecd)

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-05-29 22:03:39 +00:00
Elliot DeNolf
71df378fb0 templates: bump for v3.40.0 (#12613)
🤖 Automated bump of templates for v3.40.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-29 16:40:47 -04:00
Germán Jabloñski
5e3a94bbc9 chore: update CONTRIBUTING.md (#12511)
- I corrected the use of `yarn` instead of `pnpm`
- I corrected the URL for previewing the documentation (it was missing
`/local`).
- I removed the incorrect section about what type of commit falls into
the release notes.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-05-29 20:11:27 +00:00
Elliot DeNolf
3670886bee chore(release): v3.40.0 [skip ci] 2025-05-29 15:43:10 -04:00
Sasha
6888f13f27 fix(db-postgres): properly escape the ' character (#12590)
Fixes the issue when `defaultValue` contains `'` it'd double the amount
of `'` for the `DEFAULT` statement in the generated migration
2025-05-29 15:27:07 -04:00
Sasha
12395e497b fix(db-*): disable DB connection for payload migrate:create (#12596)
Fixes https://github.com/payloadcms/payload/issues/12582
2025-05-29 15:25:54 -04:00
Jarrod Flesch
a17d84e570 fix(ui): reduces pill sizing in autosave cells (#12606) 2025-05-29 10:08:41 -04:00
James Mikrut
ca6f849b53 feat: adds new canSetHeaders prop to auth strategies (#12591)
Exposes a new argument to authentication strategies which allows the
author to determine if this auth strategy has the capability of setting
response headers or not.

This is useful because some auth strategies may want to set headers, but
in Next.js server components (AKA the admin panel), it's not possible to
set headers. It is, however, possible to set headers within API
responses and similar contexts.

So, an author might decide to only run operations that require setting
headers (i.e. refreshing an access token) if the auth strategy is being
executed in contexts where setting headers is possible.
2025-05-29 09:58:58 -04:00
Jarrod Flesch
7e873a9d63 feat: moves getSafeRedirect into payload package (#12593) 2025-05-29 09:36:09 -04:00
Paul Popus
c471a1a106 test suite updates 2025-05-29 12:26:19 +01:00
Paul Popus
e67e48b054 plugin updates 2025-05-29 12:26:04 +01:00
Paul Popus
b723f9e817 commit tsconfig base update 2025-05-29 12:25:54 +01:00
Alessio Gravili
d85909e5ae refactor: use parseCookies method from Next.js (#12599)
Following up on https://github.com/payloadcms/payload/pull/12515, we
could instead use the same `parseCookies` method that Next.js uses. This
handles a few edge-cases differently:
- correctly strips whitespace
- parses attributes without explicit values

I think it's a good idea to match the behavior of Next.js as close as
possible here. [This](https://github.com/vercel/edge-runtime/pull/374)
is a good example of how the Next.js behavior behaves differently.

## Example

Input: `'my_value=true; Secure; HttpOnly'`

Previous Output:
```
Map(3) {
  'my_value' => 'true',
  'Secure' => '',
  'HttpOnly' => '',
}
```

New Output:
```
Map(3) {
  'my_value' => 'true',
  'Secure' => 'true',
  'HttpOnly' => 'true'
}
```
2025-05-29 04:56:24 +00:00
Jacob Fletcher
699af8dc5b feat(ui): export FieldAction type (#12589)
Closes #12356.
2025-05-28 23:00:26 +00:00
Jordy
0c0b0fe0f8 docs: autoLogin codeblock was not nested under 'admin' (#12573)
Additionally changed `process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN` to `NODE_ENV` since this is a more standard practice.
2025-05-28 22:50:56 +00:00
Alessandro Stoppato
bfdcb51793 fix: parseCookies ignore invalid encoded values (#12515)
this has been already reported here:
https://github.com/payloadcms/payload/issues/10591

`parseCookies.ts` tries to decode cookie's values using `decodeURI()`
and throws an Error when it fails

Since it does that on all cookies set on the current domain, there's no
control on which cookie get evaluated; for instance ads networks,
analytics providers, external fonts, etc... all set cookies with
different encodings.

### Taking in consideration:

- HTTP specs doesn't define a standard way for cookie value encoding but
simply provide recommendations:
[RFC6265](https://httpwg.org/specs/rfc6265.html#sane-set-cookie)
> To maximize compatibility with user agents, servers that wish to store
arbitrary data in a cookie-value SHOULD encode that data, for example,
using Base64

- NextJS does a pretty similar parsing and ignore invalid encoded values

https://github.com/vercel/edge-runtime/blob/main/packages/cookies/src/serialize.ts
`function parseCookie(cookie: string)`
```typescript
try {
      map.set(key, decodeURIComponent(value ?? 'true'))
    } catch {
      // ignore invalid encoded values
    }
```

### With the current implementation:
- it's impossible to login because `parseCookies.ts` get called and if
fails to parse throws and error
- requests to `/api/users/me` fail for the same reason

### Fix
the pull request address these issues by simply ignoring decoding
errors:
CURRENT:
```typescript
 try {
        const decodedValue = decodeURI(encodedValue)
        list.set(key, decodedValue)
      } catch (e) {
        throw new APIError(`Error decoding cookie value for key ${key}: ${e.message}`)
      }
```
AFTER THIS PULL REQUEST
```typescript
      try {
        const decodedValue = decodeURI(encodedValue)
        list.set(key, decodedValue)
      } catch {
        // ignore invalid encoded values
      }
```
2025-05-28 15:42:50 -07:00
Alessio Gravili
ca26402377 fix(richtext-lexical): respect disableBlockName (#12597)
Fixes #12588 

Previously, the new `disableBlockName` was not respected for lexical
blocks. This PR adds a new e2e test and does some clean-up of previous
e2e tests
2025-05-28 22:41:05 +00:00
Alessio Gravili
3022cab8ac fix(ui): oversized column selector pills (#12583)
#10030 adjusted the default `Pill` component size but forgot to set the
column selector pill sizes to small

## Before

![Screenshot 2025-05-27 at 14 34
31@2x](https://github.com/user-attachments/assets/0f7d44e7-343a-4542-9bc5-830f4bd2bd96)

## After

![Screenshot 2025-05-27 at 14 34
25@2x](https://github.com/user-attachments/assets/33f65fb7-130a-405b-820f-e31259b4f950)
2025-05-28 21:13:22 +00:00
Germán Jabloñski
8a7ac784c4 fix(translations): improve Spanish translations (#12555)
There are still things to improve.

- We're inconsistent with our use of capital letters. There are still
sentences where every word starts with a capital letter, and it looks
ugly (this also happens in English, but to a lesser extent).
- We're inconsistent with the use of punctuation at the end.
- Sentences with variables like {{count}} can result in inconsistencies
if it's 1 and the noun is plural.
- The same thing happens in Spanish, but also with gender. It's
impossible to know without the context in which it's used.

---------

Co-authored-by: Paul Popus <paul@payloadcms.com>
2025-05-28 16:50:47 -03:00
Jarrod Flesch
54a04840c7 feat: adds calling of before and after operation hooks to resetPassword (#12581) 2025-05-28 15:18:49 -04:00
Germán Jabloñski
f2b54b5b43 fix(richtext-lexical, ui): opening relationship field with appearance: "drawer" inside rich text inline block (#12529)
To reproduce this bug, insert the following feature into the richtext
editor:


```ts
BlocksFeature({
  inlineBlocks: [
    {
      slug: 'inline-media',
      fields: [
        {
          name: 'media',
          type: 'relationship',
          relationTo: ['media'],
          admin: {
            appearance: 'drawer',
          },
        },
      ],
    },
  ],
}),
```

Then try opening the relationship field drawer. The inline block drawer
will close.

Note: Interestingly, at least in Chrome, this only happens with DevTools
closed. It worked fine with DevTools open. It probably has to do with
capturing events like focus.
The current solution is a 50ms delay. I couldn't test it with CPU
throttle because it disappears when I close the devtools. If you
encounter this bug, please open an issue so we can increase the delay
or, better yet, find a more elegant solution.
2025-05-28 11:31:28 -03:00
Jarrod Flesch
4a41369a00 chore: updates bug template (#12587) 2025-05-28 10:18:25 -04:00
Sean Zubrickas
7fa879c3a0 docs: typos and links (#12484)
- update SantizeCollection to SanitizedCollection in TypeScript section

- fix new issue link in Multi-Tenant plugin section

- correct You can disabled to disable in Core Features

- fix duplicate section links for MongoDB and Postgres in Migrations
section
2025-05-28 06:47:51 -07:00
Jarrod Flesch
166dafe05e fix(ui): filtering on hasMany fields (#12579) 2025-05-28 09:45:22 -04:00
Paul Popus
1415ff63ea remove console log 2025-05-28 13:38:30 +01:00
Paul Popus
f2c881a414 add frontend test suite 2025-05-28 13:27:37 +01:00
Jessica Rynkar
68ba24d91f fix(templates): update template/plugin and fix import map issue (#12305)
### What?
1. Adds logic to automatically update the `importMap.js` file with the
project name provided by the user.
2. Adds an updated version of the `README.md` file that we had when this
template existed outside of the monorepo
([here](https://github.com/payloadcms/plugin-template/blob/main/README.md))
to provide clear instructions of required steps.

### Why?
1. The plugin template when installed via `npx create-payload-app` asks
the user for a project name, however the exports from `importMap.js` do
not get updated to the provided name. This throws errors when running
the project and prevents it from building.

2. The `/dev` folder requires the `.env.example` to be copied and
renamed to `.env` - the project will not run until this is done. The
template lacks instructions that this is a required step.

### How?
1. Updates
`packages/create-payload-app/src/lib/configure-plugin-project.ts` to
read the `importMap.js` file and replace the placeholder plugin name
with the name provided by the users. Adds a test to
`packages/create-payload-app/src/lib/create-project.spec.ts` to verify
that this file gets updated correctly.
2. Adds instructions on using this template to the `README.md` file,
ensuring key steps (like adding the `.env` file) are clearly stated.

Additional housekeeping updates:
- Removed Jest and replaced it with Vitest for testing
- Updated the base test approach to use Vitest instead of Jest
- Removed `NextRESTClient` in favor of directly creating Request objects
- Abstracted `getCustomEndpointHandler` function
- Added ensureIndexes: true to the mongooseAdapter configuration
- Removed the custom server from the dev folder
- Updated the pnpm dev script to "dev": "next dev dev --turbo"
- Removed `admin.autoLogin`

Fixes #12198
2025-05-27 21:33:23 +00:00
Patrik
20f7017758 feat: show nested fields in named tabs as separate columns in the list view (#12530)
### What

Continuation of #7355 by extending the functionality to named tabs.

Updates `flattenFields` to hoist nested fields inside named tabs to the
top-level field array when `moveSubFieldsToTop` is enabled.

Also fixes an issue where group fields with custom cells were being
flattened out.

Now, group fields with a custom cell components remain available as
top-level columns.

Fixes #12563
2025-05-27 14:15:47 -07:00
Jacob Fletcher
0204f0dcbc feat: filter query preset constraints (#12485)
You can now specify exactly who can change the constraints within a
query preset.

For example, you want to ensure that only "admins" are allowed to set a
preset to "everyone".

To do this, you can use the new `queryPresets.filterConstraints`
property. When a user lacks the permission to change a constraint, the
option will either be hidden from them or disabled if it is already set.

```ts
import { buildConfig } from 'payload'

const config = buildConfig({
  // ...
  queryPresets: {
    // ...
    filterConstraints: ({ req, options }) =>
      !req.user?.roles?.includes('admin')
        ? options.filter(
            (option) =>
              (typeof option === 'string' ? option : option.value) !==
              'everyone',
          )
        : options,
  },
})
```

The `filterConstraints` functions takes the same arguments as
`reduceOptions` property on select fields introduced in #12487.
2025-05-27 16:55:37 -04:00
Said Akhrarov
032375b016 fix(ui): prevent textarea description overlapping fields and not honoring rows attribute (#12406) 2025-05-27 16:27:00 -04:00
Jarrod Flesch
8448e5b6b6 fix(ui): cloudfront removing X-HTTP-Method-Override header (#12571) 2025-05-27 15:48:51 -04:00
Paul Popus
1e79392f90 update test env 2025-05-27 20:35:37 +01:00
Paul Popus
8e056bbd36 update payment structure 2025-05-27 20:35:24 +01:00
Jacob Fletcher
d6f6b05d77 fix(examples): update live-preview example to ESM (#12570)
Partial fix for #12551.

The Live Preview example was unable to boot because it was running
CommonJS instead of ESM.
2025-05-27 14:30:39 -04:00
Jessica Rynkar
feb7e082af chore(ui): finish adding folders e2e tests (#12524) 2025-05-27 13:00:56 -04:00
Jarrod Flesch
dfa0974894 fix(ui): live-preview-tab should show beforeDocumentControls (#12568) 2025-05-27 11:30:02 -04:00
Jarrod Flesch
f2b6c4a707 fix(db-mongodb): exists query on checkbox fields (#12567) 2025-05-27 11:19:09 -04:00
Paul Popus
60163337ac fix seeding 2025-05-21 15:29:02 +01:00
Paul Popus
69757780bb fix base tsconfig 2025-05-21 13:17:48 +01:00
Paul Popus
ba14c00786 Merge branch 'main' into feat/ecommerce-package 2025-05-21 13:09:45 +01:00
Paul Popus
609daaf8b4 update ts config 2025-05-21 13:09:21 +01:00
Paul Popus
5d5a5d0c57 update test suite 2025-05-21 13:08:02 +01:00
Paul Popus
ae0ac8c669 commit progress 2025-05-21 13:07:45 +01:00
Paul Popus
d077e00138 move package name 2025-05-19 19:15:48 +01:00
Paul Popus
4289d68908 commit progress 2025-05-14 11:25:27 +01:00
Paul Popus
1c42bff054 Merge branch 'main' into feat/ecommerce-package 2025-04-11 11:32:58 +01:00
Paul Popus
4a0ed68751 initial commit 2025-04-04 14:58:38 +01:00
746 changed files with 16718 additions and 9552 deletions

View File

@@ -43,6 +43,7 @@ body:
- 'plugin: cloud'
- 'plugin: cloud-storage'
- 'plugin: form-builder'
- 'plugin: multi-tenant'
- 'plugin: nested-docs'
- 'plugin: richtext-lexical'
- 'plugin: richtext-slate'
@@ -59,10 +60,7 @@ body:
label: Environment Info
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
render: text
placeholder: |
Payload:
Node.js:
Next.js:
placeholder: Run `pnpm payload info` in your terminal and paste the output here.
validations:
required: true

View File

@@ -6,6 +6,7 @@ on:
- opened
- reopened
- synchronize
- labeled
push:
branches:
- main
@@ -383,6 +384,142 @@ jobs:
# report-tag: ${{ matrix.suite }}
# job-summary: true
tests-e2e-turbo:
runs-on: ubuntu-24.04
needs: [changes, build]
if: >-
needs.changes.outputs.needs_tests == 'true' &&
(
contains(github.event.pull_request.labels.*.name, 'run-e2e-turbo') ||
github.event.label.name == 'run-e2e-turbo'
)
name: e2e-turbo-${{ matrix.suite }}
strategy:
fail-fast: false
matrix:
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
suite:
- _community
- access-control
- admin__e2e__general
- admin__e2e__list-view
- admin__e2e__document-view
- admin-bar
- admin-root
- auth
- auth-basic
- bulk-edit
- joins
- field-error-states
- fields-relationship
- fields__collections__Array
- fields__collections__Blocks
- fields__collections__Blocks#config.blockreferences.ts
- fields__collections__Checkbox
- fields__collections__Collapsible
- fields__collections__ConditionalLogic
- fields__collections__CustomID
- fields__collections__Date
- fields__collections__Email
- fields__collections__Indexed
- fields__collections__JSON
- fields__collections__Number
- fields__collections__Point
- fields__collections__Radio
- fields__collections__Relationship
- fields__collections__Row
- fields__collections__Select
- fields__collections__Tabs
- fields__collections__Tabs2
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- hooks
- lexical__collections__Lexical__e2e__main
- lexical__collections__Lexical__e2e__blocks
- lexical__collections__Lexical__e2e__blocks#config.blockreferences.ts
- lexical__collections__RichText
- query-presets
- form-state
- live-preview
- localization
- locked-documents
- i18n
- plugin-cloud-storage
- plugin-form-builder
- plugin-import-export
- plugin-nested-docs
- plugin-seo
- sort
- versions
- uploads
env:
SUITE_NAME: ${{ matrix.suite }}
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
- name: Store Playwright's Version
run: |
# Extract the version number using a more targeted regex pattern with awk
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers for Playwright's Version
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Setup Playwright - Browsers and Dependencies
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Setup Playwright - Dependencies-only
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- name: E2E Tests
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e:prod:ci:turbo ${{ matrix.suite }}
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
NEXT_TELEMETRY_DISABLED: 1
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-turbo${{ matrix.suite }}
path: test/test-results/
if-no-files-found: ignore
retention-days: 1
# Disabled until this is fixed: https://github.com/daun/playwright-report-summary/issues/156
# - uses: daun/playwright-report-summary@v3
# with:
# report-file: results_${{ matrix.suite }}.json
# report-tag: ${{ matrix.suite }}
# job-summary: true
# Build listed templates with packed local packages
build-templates:
runs-on: ubuntu-24.04

2
.gitignore vendored
View File

@@ -311,6 +311,8 @@ test/admin-bar/app/(payload)/admin/importMap.js
/test/admin-bar/app/(payload)/admin/importMap.js
test/live-preview/app/(payload)/admin/importMap.js
/test/live-preview/app/(payload)/admin/importMap.js
test/plugin-ecommerce/app/(payload)/admin/importMap.js
/test/plugin-ecommerce/app/(payload)/admin/importMap.js
test/admin-root/app/(payload)/admin/importMap.js
/test/admin-root/app/(payload)/admin/importMap.js
test/app/(payload)/admin/importMap.js

View File

@@ -87,41 +87,43 @@ You can run the entire test suite using `pnpm test`. If you wish to only run e2e
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
### Commits
### Pull Requests
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for our commit messages. Please follow this format when creating commits. Here are some examples:
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR description.
- `feat: adds new feature`
- `fix: fixes bug`
- `docs: adds documentation`
- `chore: does chore`
All commits within a PR are squashed when merged, using the PR title as the commit message. For that reason, please use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for your PR titles.
Here's a breakdown of the format. At the top-level, we use the following types to categorize our commits:
Here are some examples:
- `feat`: new feature that adds functionality. These are automatically added to the changelog when creating new releases.
- `fix`: a fix to an existing feature. These are automatically added to the changelog when creating new releases.
- `docs`: changes to [docs](./docs) only. These do not appear in the changelog.
- `chore`: changes to code that is neither a fix nor a feature (e.g. refactoring, adding tests, etc.). These do not appear in the changelog.
- `feat: add new feature`
- `fix: fix bug`
- `docs: add documentation`
- `test: add/fix tests`
- `refactor: refactor code`
- `chore: anything that does not fit into the above categories`
If applicable, you must indicate the affected packages in parentheses to "scope" the changes. Changes to the payload chore package do not require scoping.
Here are some examples:
- `feat(ui): add new feature`
- `fix(richtext-lexical): fix bug`
If you are committing to [templates](./templates) or [examples](./examples), use the `chore` type with the proper scope, like this:
- `chore(templates): adds feature to template`
- `chore(examples): fixes bug in example`
## Pull Requests
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
## Previewing docs
This is how you can preview changes you made locally to the docs:
1. Clone our [website repository](https://github.com/payloadcms/website)
2. Run `yarn install`
2. Run `pnpm install`
3. Duplicate the `.env.example` file and rename it to `.env`
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)
5. Run `pnpm fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
6. You're done! Now you can start the website locally using `pnpm dev` and preview the docs under [http://localhost:3000/docs/local](http://localhost:3000/docs/local)
## Internationalization (i18n)

View File

@@ -981,7 +981,15 @@ const MyComponent: React.FC = () => {
## useTableColumns
Returns methods to manipulate table columns
Returns properties and methods to manipulate table columns:
| Property | Description |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| **`columns`** | The current state of columns including their active status and configuration |
| **`LinkedCellOverride`** | A component override for linked cells in the table |
| **`moveColumn`** | A method to reorder columns. Accepts `{ fromIndex: number, toIndex: number }` as arguments |
| **`resetColumnsState`** | A method to reset columns back to their default configuration as defined in the collection config |
| **`setActiveColumns`** | A method to set specific columns to active state while preserving the existing column order. Accepts an array of column names to activate |
| **`toggleColumn`** | A method to toggle a single column's visibility. Accepts a column name as string |
```tsx
'use client'
@@ -989,17 +997,30 @@ import { useTableColumns } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const { setActiveColumns } = useTableColumns()
const { setActiveColumns, resetColumnsState } = useTableColumns()
const resetColumns = () => {
setActiveColumns(['id', 'createdAt', 'updatedAt'])
const activateSpecificColumns = () => {
// Only activates the id and createdAt columns
// Other columns retain their current active/inactive state
// The original column order is preserved
setActiveColumns(['id', 'createdAt'])
}
const resetToDefaults = () => {
// Resets to the default columns defined in the collection config
resetColumnsState()
}
// highlight-end
return (
<button type="button" onClick={resetColumns}>
Reset columns
</button>
<div>
<button type="button" onClick={activateSpecificColumns}>
Activate Specific Columns
</button>
<button type="button" onClick={resetToDefaults}>
Reset To Defaults
</button>
</div>
)
}
```

View File

@@ -25,11 +25,12 @@ A strategy is made up of the following:
The `authenticate` function is passed the following arguments:
| Argument | Description |
| ---------------- | ------------------------------------------------------------------------------------------------- |
| **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
| **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. |
| **`isGraphQL`** | Whether or not the request was made from a GraphQL endpoint. Default is `false`. |
| Argument | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **`canSetHeaders`** \* | Whether or not the strategy is being executed from a context where response headers can be set. Default is `false`. |
| **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
| **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. |
| **`isGraphQL`** | Whether or not the strategy is being executed within the GraphQL endpoint. Default is `false`. |
### Example Strategy

View File

@@ -142,14 +142,17 @@ import { buildConfig } from 'payload'
export default buildConfig({
// ...
// highlight-start
autoLogin:
process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN === 'true'
? {
email: 'test@example.com',
password: 'test',
prefillOnly: true,
}
: false,
admin: {
autoLogin:
process.env.NODE_ENV === 'development'
? {
email: 'test@example.com',
password: 'test',
prefillOnly: true,
}
: false,
},
// highlight-end
})
```

View File

@@ -194,13 +194,15 @@ export const MyCollection: CollectionConfig = {
The following options are available:
| Option | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). |
| `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#savedraftbutton). |
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). |
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). |
| `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). |
| Option | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](../custom-components/edit-view#beforedocumentcontrols). |
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document controls bar. [More details](../custom-components/edit-view#editmenuitems). |
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). |
| `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#savedraftbutton). |
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). |
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). |
| `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). |
<Banner type="success">
**Note:** For details on how to build Custom Components, see [Building Custom

View File

@@ -101,15 +101,16 @@ export const MyCollection: CollectionConfig = {
The following options are available:
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Collection. [More details](#description). |
| `Upload` | A file upload component. [More details](#upload). |
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Collection. [More details](#description). |
| `Upload` | A file upload component. [More details](#upload). |
#### Globals
@@ -134,14 +135,15 @@ export const MyGlobal: GlobalConfig = {
The following options are available:
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Global. [More details](#description). |
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Global. [More details](#description). |
### SaveButton
@@ -260,6 +262,92 @@ export function MyCustomDocumentControlButton(
}
```
### editMenuItems
The `editMenuItems` property allows you to inject custom components into the 3-dot menu dropdown located in the document controls bar. This dropdown contains default options including `Create New`, `Duplicate`, `Delete`, and other options when additional features are enabled. Any custom components you add will appear below these default items.
To add `editMenuItems`, use the `components.edit.editMenuItems` property in your [Collection Config](../configuration/collections):
#### Config Example
```ts
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
components: {
edit: {
// highlight-start
editMenuItems: ['/path/to/CustomEditMenuItem'],
// highlight-end
},
},
},
}
```
Here's an example of a custom `editMenuItems` component:
#### Server Component
```tsx
import React from 'react'
import { PopupList } from '@payloadcms/ui'
import type { EditMenuItemsServerProps } from 'payload'
export const EditMenuItems = async (props: EditMenuItemsServerProps) => {
const href = `/custom-action?id=${props.id}`
return (
<PopupList.ButtonGroup>
<PopupList.Button href={href}>Custom Edit Menu Item</PopupList.Button>
<PopupList.Button href={href}>
Another Custom Edit Menu Item - add as many as you need!
</PopupList.Button>
</PopupList.ButtonGroup>
)
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import { PopupList } from '@payloadcms/ui'
import type { EditViewMenuItemClientProps } from 'payload'
export const EditMenuItems = (props: EditViewMenuItemClientProps) => {
const handleClick = () => {
console.log('Custom button clicked!')
}
return (
<PopupList.ButtonGroup>
<PopupList.Button onClick={handleClick}>
Custom Edit Menu Item
</PopupList.Button>
<PopupList.Button onClick={handleClick}>
Another Custom Edit Menu Item - add as many as you need!
</PopupList.Button>
</PopupList.ButtonGroup>
)
}
```
<Banner type="info">
**Styling:** Use Payload&apos;s built-in <code>PopupList.Button</code> to
ensure your menu items automatically match the default dropdown styles. If you
want a different look, you can customize the appearance by passing your own{' '}
<code>className</code> to <code>PopupList.Button</code>, or use a completely
custom button built with a standard HTML <code>&lt;button&gt;</code> element
or any other component that fits your design preferences.
</Banner>
### SaveDraftButton
The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View.

View File

@@ -94,6 +94,7 @@ The following options are available:
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforelisttable). |
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterlist). |
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterlisttable). |
| `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) |
| `Description` | A component to render a description of the Collection. [More details](#description). |
### beforeList

View File

@@ -183,13 +183,13 @@ Depending on which Database Adapter you use, your migration workflow might diffe
In relational databases, migrations will be **required** for non-development database environments. But with MongoDB, you might only need to run migrations once in a while (or never even need them).
#### MongoDB
#### MongoDB#mongodb-migrations
In MongoDB, you'll only ever really need to run migrations for times where you change your database shape, and you have lots of existing data that you'd like to transform from Shape A to Shape B.
In this case, you can create a migration by running `pnpm payload migrate:create`, and then write the logic that you need to perform to migrate your documents to their new shape. You can then either run your migrations in CI before you build / deploy, or you can run them locally, against your production database, by using your production database connection string on your local computer and running the `pnpm payload migrate` command.
#### Postgres
#### Postgres#postgres-migrations
In relational databases like Postgres, migrations are a bit more important, because each time you add a new field or a new collection, you'll need to update the shape of your database to match your Payload Config (otherwise you'll see errors upon trying to read / write your data).

View File

@@ -37,7 +37,7 @@ export const MyGroupField: Field = {
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Required when name is undefined, defaults to name converted to words. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Defaults to the field name, if defined. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
@@ -113,8 +113,7 @@ export const ExampleCollection: CollectionConfig = {
## Presentational group fields
You can also use the Group field to create a presentational group of fields. This is useful when you want to group fields together visually without affecting the data structure.
The label will be required when a `name` is not provided.
You can also use the Group field to only visually group fields without affecting the data structure. Not defining a label will render just the grouped fields.
```ts
import type { CollectionConfig } from 'payload'

View File

@@ -70,7 +70,7 @@ _\* An asterisk denotes that a property is required._
### filterOptions
Used to dynamically filter which options are available based on the user, data, etc.
Used to dynamically filter which options are available based on the current user, document data, or other criteria.
Some examples of this might include:

View File

@@ -23,6 +23,12 @@ On the payload config, you can configure the following settings under the `folde
// Type definition
type RootFoldersConfiguration = {
/**
* If true, the browse by folder view will be enabled
*
* @default true
*/
browseByFolder?: boolean
/**
* An array of functions to be ran when the folder collection is initialized
* This allows plugins to modify the collection configuration
@@ -82,7 +88,16 @@ To enable folders on a collection, you need to set the `admin.folders` property
```ts
// Type definition
type CollectionFoldersConfiguration = boolean
type CollectionFoldersConfiguration =
| boolean
| {
/**
* If true, the collection will be included in the browse by folder view
*
* @default true
*/
browseByFolder?: boolean
}
```
```ts

View File

@@ -329,7 +329,7 @@ available:
// responseHeaders: { ... } // returned headers from the response
// }
const result = await payload.auth({ headers })
const result = await payload.auth({ headers, canSetHeaders: false })
```
### Login

View File

@@ -16,8 +16,8 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
If you need help, check out our [Community
Help](https://payloadcms.com/community-help). If you think you've found a bug,
please [open a new
issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%multi-tenant&template=bug_report.md&title=plugin-multi-tenant%3A)
with as much detail as possible.
issue](https://github.com/payloadcms/payload/issues/new/choose) with as much
detail as possible.
</Banner>
## Core features
@@ -35,7 +35,7 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
strong access control on your tenants collection to prevent deletions by unauthorized users.
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
You can disable this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
</Banner>

View File

@@ -115,6 +115,7 @@ Set the `uploadsCollection` to your application's upload-enabled collection slug
##### `tabbedUI`
When the `tabbedUI` property is `true`, it appends an `SEO` tab onto your config using Payload's [Tabs Field](../fields/tabs). If your collection is not already tab-enabled, meaning the first field in your config is not of type `tabs`, then one will be created for you called `Content`. Defaults to `false`.
Note that the order of plugins or fields in your config may affect whether or not the plugin can smartly merge tabs with your existing fields. If you have a complex structure we recommend you [make use of the fields directly](#direct-use-of-fields) instead of relying on this config option.
<Banner type="info">
If you wish to continue to use top-level or sidebar fields with `tabbedUI`,

View File

@@ -0,0 +1,32 @@
---
title: Building without a DB connection
label: Building without a DB connection
order: 10
desc: You don't want to have a DB connection while building your Docker container? Learn how to prevent that!
keywords: deployment, production, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
# Building without a DB connection
One of the most common problems when building a site for production, especially with Docker - is the DB connection requirement.
The important note is that Payload by itself does not have this requirement, But [Next.js' SSG ](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation) does if any of your route segments have SSG enabled (which is default, unless you opted out or used a [Dynamic API](https://nextjs.org/docs/app/deep-dive/caching#dynamic-apis)) and use the Payload Local API.
Solutions:
## Using the experimental-build-mode Next.js build flag
You can run Next.js build using the `pnpx next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpx next build --experimental-build-mode generate` command when you have a DB connection.
[Next.js documentation](https://nextjs.org/docs/pages/api-reference/cli/next#next-build-options)
## Opting-out of SSG
You can opt out of SSG by adding this all the route segment files:
```ts
export const dynamic = 'force-dynamic'
```
**Note that it will disable static optimization and your site will be slower**.
More on [Next.js documentation](https://nextjs.org/docs/app/deep-dive/caching#opting-out-2)

View File

@@ -150,7 +150,7 @@ Follow the docs to configure any one of these storage providers. For local devel
## Docker
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed. If you don't want to have a DB connection and your build requires that, learn [here](./building-without-a-db-connection) how to prevent that.
In your Next.js config, set the `output` property `standalone`.

View File

@@ -46,11 +46,12 @@ const config = buildConfig({
The following options are available for Query Presets:
| Option | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
| `labels` | Custom labels to use for the Query Presets collection. |
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
| `filterConstraints` | Used to define which constraints are available to users when managing presets. [More details](#constraint-access-control). |
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
| `labels` | Custom labels to use for the Query Presets collection. |
## Access Control
@@ -59,7 +60,7 @@ Query Presets are subject to the same [Access Control](../access-control/overvie
Access Control for Query Presets can be customized in two ways:
1. [Collection Access Control](#collection-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are dynamically defined on each record in the database.
### Collection Access Control
@@ -97,7 +98,7 @@ This example restricts all Query Presets to users with the role of `admin`.
### Document Access Control
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each document.
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each record.
When a user manages a preset, document-level access control options will be available to them in the Admin Panel for each operation.
@@ -150,8 +151,8 @@ const config = buildConfig({
}),
},
],
// highlight-end
},
// highlight-end
},
})
```
@@ -171,3 +172,39 @@ The following options are available for each constraint:
| `value` | The value to store in the database when this constraint is selected. |
| `fields` | An array of fields to render when this constraint is selected. |
| `access` | A function that determines the access control rules for this constraint. |
### Constraint Access Control
Used to dynamically filter which constraints are available based on the current user, document data, or other criteria.
Some examples of this might include:
- Ensuring that only "admins" are allowed to make a preset available to "everyone"
- Preventing the "onlyMe" option from being selected based on a hypothetical "disablePrivatePresets" checkbox
When a user lacks the permission to set a constraint, the option will either be hidden from them, or disabled if it is already saved to that preset.
To do this, you can use the `filterConstraints` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
queryPresets: {
// ...
// highlight-start
filterConstraints: ({ req, options }) =>
!req.user?.roles?.includes('admin')
? options.filter(
(option) =>
(typeof option === 'string' ? option : option.value) !==
'everyone',
)
: options,
// highlight-end
},
})
```
The `filterConstraints` function receives the same arguments as [`filterOptions`](../fields/select#filterOptions) in the [Select field](../fields/select).

View File

@@ -738,7 +738,7 @@ Payload supports a method override feature that allows you to send GET requests
### How to Use
To use this feature, include the `X-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`.
To use this feature, include the `X-Payload-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`.
### Example
@@ -753,7 +753,7 @@ const res = await fetch(`${api}/${collectionSlug}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/x-www-form-urlencoded',
'X-HTTP-Method-Override': 'GET',
'X-Payload-HTTP-Method-Override': 'GET',
},
body: qs.stringify({
depth: 1,

View File

@@ -42,6 +42,7 @@ export const rootEslintConfig = [
...defaultESLintIgnores,
'packages/eslint-*/**',
'test/live-preview/next-app',
'test/plugin-ecommerce/next-app',
'packages/**/*.spec.ts',
'templates/**',
'examples/**',

View File

@@ -3,14 +3,8 @@
width: var(--base);
.stroke {
stroke-width: 1px;
stroke-width: 2px;
fill: none;
stroke: currentColor;
}
&:local() {
.stroke {
stroke-width: 2px;
}
}
}

View File

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

View File

@@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "Payload Live Preview example.",
"license": "MIT",
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
import type { Page } from '../payload-types'
import { DefaultDocumentIDType } from 'payload'
export const home: Partial<Page> = {
export const home = (id: DefaultDocumentIDType): Partial<Page> => ({
slug: 'home',
richText: [
{
@@ -41,11 +42,26 @@ export const home: Partial<Page> = {
{
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',
},
...(id
? [
{
text: ' To get started, visit ',
},
{
type: 'link',
children: [{ text: 'this page' }],
linkType: 'custom',
newTab: true,
url: `/admin/collections/pages/${id}/preview`,
},
{ text: '.' },
]
: []),
],
},
],
title: 'Home',
}
})
export const examplePage: Partial<Page> = {
slug: 'example-page',
@@ -83,11 +99,18 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
data: examplePage as any, // eslint-disable-line
})
const homepageJSON = JSON.parse(JSON.stringify(home))
const { id: homePageID } = await payload.create({
const { id: ogHomePageID } = await payload.create({
collection: 'pages',
data: homepageJSON,
data: {
title: 'Home',
richText: [],
},
})
const { id: homePageID } = await payload.update({
id: ogHomePageID,
collection: 'pages',
data: home(ogHomePageID),
})
await payload.updateGlobal({
@@ -121,7 +144,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
type: 'custom',
label: 'Dashboard',
reference: undefined,
url: 'http://localhost:3000/admin',
url: '/admin',
},
},
],

View File

@@ -6,10 +6,66 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
pages: Page;
users: User;

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.39.1",
"version": "3.40.0",
"private": true,
"type": "module",
"scripts": {
@@ -84,7 +84,7 @@
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
"reinstall": "pnpm clean:all && pnpm install",
"release": "pnpm --filter releaser release --tag latest",
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
"runts": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" node --no-deprecation --no-experimental-strip-types --import @swc-node/register/esm-register",
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
"script:gen-templates": "pnpm --filter scripts gen-templates",
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
@@ -93,17 +93,19 @@
"script:pack": "pnpm --filter scripts pack-all-to-dest",
"pretest": "pnpm build",
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation\" jest --config=jest.components.config.js",
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation --no-experimental-strip-types\" jest --config=jest.components.config.js",
"test:e2e": "pnpm runts ./test/runE2E.ts",
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
"test:e2e:debug": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
"test:e2e:prod": "pnpm prepare-run-test-against-prod && pnpm runts ./test/runE2E.ts --prod",
"test:e2e:prod:ci": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod",
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:e2e:prod:ci:turbo": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod --turbo",
"test:e2e:turbo": "pnpm runts ./test/runE2E.ts --turbo",
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:types": "tstyche",
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
"translateNewKeys": "pnpm --filter translations run translateNewKeys"
},
"lint-staged": {
@@ -120,7 +122,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.3.0",
"@next/bundle-analyzer": "15.3.2",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -128,9 +130,9 @@
"@playwright/test": "1.50.0",
"@sentry/nextjs": "^8.33.1",
"@sentry/node": "^8.33.1",
"@swc-node/register": "1.10.9",
"@swc/cli": "0.6.0",
"@swc/jest": "0.2.37",
"@swc-node/register": "1.10.10",
"@swc/cli": "0.7.7",
"@swc/jest": "0.2.38",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
"@types/minimist": "1.2.5",
@@ -145,8 +147,6 @@
"cross-env": "7.0.3",
"dotenv": "16.4.7",
"drizzle-kit": "0.28.0",
"drizzle-orm": "0.36.1",
"escape-html": "^1.0.3",
"execa": "5.1.1",
"form-data": "3.0.1",
"fs-extra": "10.1.0",
@@ -156,7 +156,7 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^10",
"next": "15.3.0",
"next": "15.3.2",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.50.0",
@@ -169,7 +169,7 @@
"shelljs": "0.8.5",
"slash": "3.0.0",
"sort-package-json": "^2.10.0",
"swc-plugin-transform-remove-imports": "3.1.0",
"swc-plugin-transform-remove-imports": "4.0.4",
"tempy": "1.0.1",
"tstyche": "^3.1.1",
"tsx": "4.19.2",
@@ -186,7 +186,6 @@
"copyfiles": "$copyfiles",
"cross-env": "$cross-env",
"dotenv": "$dotenv",
"drizzle-orm": "$drizzle-orm",
"graphql": "^16.8.1",
"mongodb-memory-server": "$mongodb-memory-server",
"react": "$react",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.39.1",
"version": "3.40.0",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.39.1",
"version": "3.40.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -16,6 +16,7 @@
"url": "https://payloadcms.com"
}
],
"sideEffects": false,
"type": "module",
"exports": {
"./types": {
@@ -60,7 +61,7 @@
"dependencies": {
"@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0",
"@swc/core": "1.10.12",
"@swc/core": "1.11.29",
"arg": "^5.0.0",
"chalk": "^4.1.0",
"comment-json": "^4.2.3",

View File

@@ -16,12 +16,15 @@ export const configurePluginProject = ({
const devPayloadConfigPath = path.resolve(projectDirPath, './dev/payload.config.ts')
const devTsConfigPath = path.resolve(projectDirPath, './dev/tsconfig.json')
const indexTsPath = path.resolve(projectDirPath, './src/index.ts')
const devImportMapPath = path.resolve(projectDirPath, './dev/app/(payload)/admin/importMap.js')
const devPayloadConfig = fse.readFileSync(devPayloadConfigPath, 'utf8')
const devTsConfig = fse.readFileSync(devTsConfigPath, 'utf8')
const indexTs = fse.readFileSync(indexTsPath, 'utf-8')
const devImportMap = fse.readFileSync(devImportMapPath, 'utf-8')
const updatedTsConfig = devTsConfig.replaceAll('plugin-package-name-placeholder', projectName)
const updatedImportMap = devImportMap.replaceAll('plugin-package-name-placeholder', projectName)
let updatedIndexTs = indexTs.replaceAll('plugin-package-name-placeholder', projectName)
const pluginExportVariableName = toCamelCase(projectName)
@@ -43,4 +46,5 @@ export const configurePluginProject = ({
fse.writeFileSync(devPayloadConfigPath, updatedPayloadConfig)
fse.writeFileSync(devTsConfigPath, updatedTsConfig)
fse.writeFileSync(indexTsPath, updatedIndexTs)
fse.writeFileSync(devImportMapPath, updatedImportMap)
}

View File

@@ -10,10 +10,10 @@ import type { CliArgs, DbType, ProjectExample, ProjectTemplate } from '../types.
import { createProject } from './create-project.js'
import { dbReplacements } from './replacements.js'
import { getValidTemplates } from './templates.js'
import { manageEnvFiles } from './manage-env-files.js'
describe('createProject', () => {
let projectDir: string
beforeAll(() => {
// eslint-disable-next-line no-console
console.log = jest.fn()
@@ -63,6 +63,30 @@ describe('createProject', () => {
expect(packageJson.name).toStrictEqual(projectName)
})
it('updates project name in plugin template importMap file', async () => {
const projectName = 'my-custom-plugin'
const template: ProjectTemplate = {
name: 'plugin',
type: 'plugin',
description: 'Template for creating a Payload plugin',
url: 'https://github.com/payloadcms/payload/templates/plugin',
}
await createProject({
cliArgs: { ...args, '--local-template': 'plugin' } as CliArgs,
packageManager,
projectDir,
projectName,
template,
})
const importMapPath = path.resolve(projectDir, './dev/app/(payload)/admin/importMap.js')
const importMapFile = fse.readFileSync(importMapPath, 'utf-8')
expect(importMapFile).not.toContain('plugin-package-name-placeholder')
expect(importMapFile).toContain('my-custom-plugin')
})
it('creates example', async () => {
const projectName = 'custom-server-example'
const example: ProjectExample = {
@@ -155,75 +179,5 @@ describe('createProject', () => {
expect(content).toContain(dbReplacement.configReplacement().join('\n'))
})
})
describe('managing env files', () => {
it('updates .env files without overwriting existing data', async () => {
const envFilePath = path.join(projectDir, '.env')
const envExampleFilePath = path.join(projectDir, '.env.example')
fse.ensureDirSync(projectDir)
fse.ensureFileSync(envFilePath)
fse.ensureFileSync(envExampleFilePath)
const initialEnvContent = `CUSTOM_VAR=custom-value\nDATABASE_URI=old-connection\n`
const initialEnvExampleContent = `CUSTOM_VAR=custom-value\nDATABASE_URI=old-connection\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
fse.writeFileSync(envFilePath, initialEnvContent)
fse.writeFileSync(envExampleFilePath, initialEnvExampleContent)
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseType: 'mongodb',
databaseUri: 'mongodb://localhost:27017/test',
payloadSecret: 'test-secret',
projectDir,
template: undefined,
})
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toContain('CUSTOM_VAR=custom-value')
expect(updatedEnvContent).toContain('DATABASE_URI=mongodb://localhost:27017/test')
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=test-secret')
const updatedEnvExampleContent = fse.readFileSync(envExampleFilePath, 'utf-8')
expect(updatedEnvExampleContent).toContain('CUSTOM_VAR=custom-value')
expect(updatedEnvContent).toContain('DATABASE_URI=mongodb://localhost:27017/test')
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=test-secret')
})
it('creates .env and .env.example if they do not exist', async () => {
const envFilePath = path.join(projectDir, '.env')
const envExampleFilePath = path.join(projectDir, '.env.example')
fse.ensureDirSync(projectDir)
if (fse.existsSync(envFilePath)) fse.removeSync(envFilePath)
if (fse.existsSync(envExampleFilePath)) fse.removeSync(envExampleFilePath)
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseUri: '',
payloadSecret: '',
projectDir,
template: undefined,
})
expect(fse.existsSync(envFilePath)).toBe(true)
expect(fse.existsSync(envExampleFilePath)).toBe(true)
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toContain('DATABASE_URI=your-connection-string-here')
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=YOUR_SECRET_HERE')
const updatedEnvExampleContent = fse.readFileSync(envExampleFilePath, 'utf-8')
expect(updatedEnvExampleContent).toContain('DATABASE_URI=your-connection-string-here')
expect(updatedEnvExampleContent).toContain('PAYLOAD_SECRET=YOUR_SECRET_HERE')
})
})
})
})

View File

@@ -144,17 +144,14 @@ export async function createProject(
}
}
// Call manageEnvFiles before initializing Git
if (dbDetails) {
await manageEnvFiles({
cliArgs,
databaseType: dbDetails.type,
databaseUri: dbDetails.dbUri,
payloadSecret: generateSecret(),
projectDir,
template: 'template' in args ? args.template : undefined,
})
}
await manageEnvFiles({
cliArgs,
databaseType: dbDetails?.type,
databaseUri: dbDetails?.dbUri,
payloadSecret: generateSecret(),
projectDir,
template: 'template' in args ? args.template : undefined,
})
// Remove yarn.lock file. This is only desired in Payload Cloud.
const lockPath = path.resolve(projectDir, 'pnpm-lock.yaml')

View File

@@ -0,0 +1,165 @@
import { jest } from '@jest/globals'
import fs from 'fs'
import fse from 'fs-extra'
import * as os from 'node:os'
import path from 'path'
import type { CliArgs } from '../types.js'
import { manageEnvFiles } from './manage-env-files.js'
describe('createProject', () => {
let projectDir: string
let envFilePath = ''
let envExampleFilePath = ''
beforeAll(() => {
// eslint-disable-next-line no-console
console.log = jest.fn()
})
beforeEach(() => {
const tempDirectory = fs.realpathSync(os.tmpdir())
projectDir = `${tempDirectory}/${Math.random().toString(36).substring(7)}`
envFilePath = path.join(projectDir, '.env')
envExampleFilePath = path.join(projectDir, '.env.example')
if (fse.existsSync(envFilePath)) {
fse.removeSync(envFilePath)
}
fse.ensureDirSync(projectDir)
})
afterEach(() => {
if (fse.existsSync(projectDir)) {
fse.rmSync(projectDir, { recursive: true })
}
})
it('generates .env using defaults (not from .env.example)', async () => {
// ensure no .env.example exists so that the default values are used
// the `manageEnvFiles` function will look for .env.example in the file system
if (fse.existsSync(envExampleFilePath)) {
fse.removeSync(envExampleFilePath)
}
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseUri: '', // omitting this will ensure the default vars are used
payloadSecret: '', // omitting this will ensure the default vars are used
projectDir,
template: undefined,
})
expect(fse.existsSync(envFilePath)).toBe(true)
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toBe(
`# Added by Payload\nPAYLOAD_SECRET=YOUR_SECRET_HERE\nDATABASE_URI=your-connection-string-here`,
)
})
it('generates .env from .env.example', async () => {
// create or override the .env.example file with a connection string that will NOT be overridden
fse.ensureFileSync(envExampleFilePath)
fse.writeFileSync(
envExampleFilePath,
`DATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value\n`,
)
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseUri: '', // omitting this will ensure the `.env.example` vars are used
payloadSecret: '', // omitting this will ensure the `.env.example` vars are used
projectDir,
template: undefined,
})
expect(fse.existsSync(envFilePath)).toBe(true)
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toBe(
`DATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n# Added by Payload`,
)
})
it('updates existing .env without overriding vars', async () => {
// create an existing .env file with some custom variables that should NOT be overridden
fse.ensureFileSync(envFilePath)
fse.writeFileSync(
envFilePath,
`CUSTOM_VAR=custom-value\nDATABASE_URI=example-connection-string\n`,
)
// create an .env.example file to ensure that its contents DO NOT override existing .env vars
fse.ensureFileSync(envExampleFilePath)
fse.writeFileSync(
envExampleFilePath,
`CUSTOM_VAR=custom-value-2\nDATABASE_URI=example-connection-string-2\n`,
)
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseUri: '', // omitting this will ensure the `.env` vars are kept
payloadSecret: '', // omitting this will ensure the `.env` vars are kept
projectDir,
template: undefined,
})
expect(fse.existsSync(envFilePath)).toBe(true)
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toBe(
`# Added by Payload\nPAYLOAD_SECRET=YOUR_SECRET_HERE\nDATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value`,
)
})
it('sanitizes .env based on selected database type', async () => {
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseType: 'mongodb', // this mimics the CLI selection and will be used as the DATABASE_URI
databaseUri: 'mongodb://localhost:27017/test', // this mimics the CLI selection and will be used as the DATABASE_URI
payloadSecret: 'test-secret', // this mimics the CLI selection and will be used as the PAYLOAD_SECRET
projectDir,
template: undefined,
})
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toBe(
`# Added by Payload\nPAYLOAD_SECRET=test-secret\nDATABASE_URI=mongodb://localhost:27017/test`,
)
// delete the generated .env file and do it again, but this time, omit the databaseUri to ensure the default is generated
fse.removeSync(envFilePath)
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseType: 'mongodb', // this mimics the CLI selection and will be used as the DATABASE_URI
databaseUri: '', // omit this to ensure the default is generated based on the selected database type
payloadSecret: 'test-secret',
projectDir,
template: undefined,
})
const updatedEnvContentWithDefault = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContentWithDefault).toBe(
`# Added by Payload\nPAYLOAD_SECRET=test-secret\nDATABASE_URI=mongodb://127.0.0.1/your-database-name`,
)
})
})

View File

@@ -6,21 +6,42 @@ import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
import { debug, error } from '../utils/log.js'
import { dbChoiceRecord } from './select-db.js'
const updateEnvExampleVariables = (
contents: string,
databaseType: DbType | undefined,
payloadSecret?: string,
databaseUri?: string,
): string => {
const sanitizeEnv = ({
contents,
databaseType,
databaseUri,
payloadSecret,
}: {
contents: string
databaseType: DbType | undefined
databaseUri?: string
payloadSecret?: string
}): string => {
const seenKeys = new Set<string>()
const updatedEnv = contents
// add defaults
let withDefaults = contents
if (
!contents.includes('DATABASE_URI') &&
!contents.includes('POSTGRES_URL') &&
!contents.includes('MONGODB_URI')
) {
withDefaults += '\nDATABASE_URI=your-connection-string-here'
}
if (!contents.includes('PAYLOAD_SECRET')) {
withDefaults += '\nPAYLOAD_SECRET=YOUR_SECRET_HERE'
}
let updatedEnv = withDefaults
.split('\n')
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) {
return line
}
const [key] = line.split('=')
const [key, value] = line.split('=')
if (!key) {
return
@@ -28,6 +49,7 @@ const updateEnvExampleVariables = (
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
if (dbChoice) {
const placeholderUri = databaseUri
? databaseUri
@@ -36,6 +58,8 @@ const updateEnvExampleVariables = (
databaseType === 'vercel-postgres'
? `POSTGRES_URL=${placeholderUri}`
: `DATABASE_URI=${placeholderUri}`
} else {
line = `${key}=${value}`
}
}
@@ -56,6 +80,10 @@ const updateEnvExampleVariables = (
.reverse()
.join('\n')
if (!updatedEnv.includes('# Added by Payload')) {
updatedEnv = `# Added by Payload\n${updatedEnv}`
}
return updatedEnv
}
@@ -63,7 +91,7 @@ const updateEnvExampleVariables = (
export async function manageEnvFiles(args: {
cliArgs: CliArgs
databaseType?: DbType
databaseUri: string
databaseUri?: string
payloadSecret: string
projectDir: string
template?: ProjectTemplate
@@ -77,70 +105,63 @@ export async function manageEnvFiles(args: {
return
}
const envExamplePath = path.join(projectDir, '.env.example')
const pathToEnvExample = path.join(projectDir, '.env.example')
const envPath = path.join(projectDir, '.env')
const emptyEnvContent = `# Added by Payload\nDATABASE_URI=your-connection-string-here\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
try {
let updatedExampleContents: string
let exampleEnv: null | string = ''
try {
if (template?.type === 'plugin') {
if (debugFlag) {
debug(`plugin template detected - no .env added .env.example added`)
}
return
}
if (!fs.existsSync(envExamplePath)) {
updatedExampleContents = updateEnvExampleVariables(
emptyEnvContent,
databaseType,
payloadSecret,
databaseUri,
)
// If there's a .env.example file, use it to create or update the .env file
if (fs.existsSync(pathToEnvExample)) {
const envExampleContents = await fs.readFile(pathToEnvExample, 'utf8')
await fs.writeFile(envExamplePath, updatedExampleContents)
if (debugFlag) {
debug(`.env.example file successfully created`)
}
} else {
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
const mergedEnvs = envExampleContents + '\n' + emptyEnvContent
updatedExampleContents = updateEnvExampleVariables(
mergedEnvs,
exampleEnv = sanitizeEnv({
contents: envExampleContents,
databaseType,
payloadSecret,
databaseUri,
)
payloadSecret,
})
await fs.writeFile(envExamplePath, updatedExampleContents)
if (debugFlag) {
debug(`.env.example file successfully updated`)
debug(`.env.example file successfully read`)
}
}
// If there's no .env file, create it using the .env.example content (if it exists)
if (!fs.existsSync(envPath)) {
const envContent = updateEnvExampleVariables(
emptyEnvContent,
const envContent = sanitizeEnv({
contents: exampleEnv,
databaseType,
payloadSecret,
databaseUri,
)
payloadSecret,
})
await fs.writeFile(envPath, envContent)
if (debugFlag) {
debug(`.env file successfully created`)
}
} else {
// If the .env file already exists, sanitize it as-is
const envContents = await fs.readFile(envPath, 'utf8')
const mergedEnvs = envContents + '\n' + emptyEnvContent
const updatedEnvContents = updateEnvExampleVariables(
mergedEnvs,
const updatedEnvContents = sanitizeEnv({
contents: envContents,
databaseType,
payloadSecret,
databaseUri,
)
payloadSecret,
})
await fs.writeFile(envPath, updatedEnvContents)
if (debugFlag) {
debug(`.env file successfully updated`)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.39.1",
"version": "3.40.0",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -417,7 +417,7 @@ export const sanitizeQueryValue = ({
return buildExistsQuery(
formattedValue,
path,
!['relationship', 'upload'].includes(field.type),
!['checkbox', 'relationship', 'upload'].includes(field.type),
)
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.39.1",
"version": "3.40.0",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -17,6 +17,7 @@
"url": "https://payloadcms.com"
}
],
"sideEffects": false,
"type": "module",
"exports": {
".": {
@@ -85,10 +86,10 @@
"uuid": "10.0.0"
},
"devDependencies": {
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
"@payloadcms/eslint-config": "workspace:*",
"@types/to-snake-case": "1.0.0",
"esbuild": "0.24.2",
"esbuild": "0.25.5",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -51,13 +51,6 @@ export const connect: Connect = async function connect(
) {
const { hotReload } = options
this.schema = {
pgSchema: this.pgSchema,
...this.tables,
...this.relations,
...this.enums,
}
try {
if (!this.pool) {
this.pool = new this.pg.Pool(this.poolOptions)

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.39.1",
"version": "3.40.0",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -15,11 +15,6 @@ export const connect: Connect = async function connect(
) {
const { hotReload } = options
this.schema = {
...this.tables,
...this.relations,
}
try {
if (!this.client) {
this.client = createClient(this.clientConfig)

View File

@@ -36,4 +36,9 @@ export const init: Init = async function init(this: SQLiteAdapter) {
})
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
this.schema = {
...this.tables,
...this.relations,
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.39.1",
"version": "3.40.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -17,6 +17,7 @@
"url": "https://payloadcms.com"
}
],
"sideEffects": false,
"type": "module",
"exports": {
".": {
@@ -85,11 +86,11 @@
"uuid": "10.0.0"
},
"devDependencies": {
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
"@payloadcms/eslint-config": "workspace:*",
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"esbuild": "0.24.2",
"esbuild": "0.25.5",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -16,13 +16,6 @@ export const connect: Connect = async function connect(
) {
const { hotReload } = options
this.schema = {
pgSchema: this.pgSchema,
...this.tables,
...this.relations,
...this.enums,
}
try {
const logger = this.logger || false

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.39.1",
"version": "3.40.0",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -35,4 +35,11 @@ export const init: Init = async function init(this: BasePostgresAdapter) {
})
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
this.schema = {
pgSchema: this.pgSchema,
...this.tables,
...this.relations,
...this.enums,
}
}

View File

@@ -367,7 +367,25 @@ export function parseParams({
break
}
constraints.push(adapter.operators[queryOperator](resolvedColumn, queryValue))
const orConditions: SQL<unknown>[] = []
let resolvedQueryValue = queryValue
if (
operator === 'in' &&
Array.isArray(queryValue) &&
queryValue.some((v) => v === null)
) {
orConditions.push(isNull(resolvedColumn))
resolvedQueryValue = queryValue.filter((v) => v !== null)
}
let constraint = adapter.operators[queryOperator](
resolvedColumn,
resolvedQueryValue,
)
if (orConditions.length) {
orConditions.push(constraint)
constraint = or(...orConditions)
}
constraints.push(constraint)
}
}
}

View File

@@ -7,14 +7,6 @@ export const withDefault = (column: RawColumn, field: FieldAffectingData): RawCo
return column
}
if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) {
const escapedString = field.defaultValue.replaceAll("'", "''")
return {
...column,
default: escapedString,
}
}
return {
...column,
default: field.defaultValue,

View File

@@ -98,6 +98,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
withinArrayOrBlockLocale,
}: TraverseFieldsArgs): T => {
const sanitizedPath = path ? `${path}.` : path
const localeCodes =
adapter.payload.config.localization && adapter.payload.config.localization.localeCodes
const formatted = fields.reduce((result, field) => {
if (fieldIsVirtual(field)) {
@@ -506,6 +508,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (field.type === 'text' && field?.hasMany) {
const textPathMatch = texts[`${sanitizedPath}${field.name}`]
if (!textPathMatch) {
result[field.name] =
isLocalized && localeCodes
? Object.fromEntries(localeCodes.map((locale) => [locale, []]))
: []
return result
}
@@ -545,6 +551,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (field.type === 'number' && field.hasMany) {
const numberPathMatch = numbers[`${sanitizedPath}${field.name}`]
if (!numberPathMatch) {
result[field.name] =
isLocalized && localeCodes
? Object.fromEntries(localeCodes.map((locale) => [locale, []]))
: []
return result
}
@@ -606,10 +616,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
if (isLocalized && Array.isArray(table._locales)) {
if (!table._locales.length && adapter.payload.config.localization) {
adapter.payload.config.localization.localeCodes.forEach((_locale) =>
(table._locales as unknown[]).push({ _locale }),
)
if (!table._locales.length && localeCodes) {
localeCodes.forEach((_locale) => (table._locales as unknown[]).push({ _locale }))
}
table._locales.forEach((localeRow) => {
@@ -725,8 +733,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
return result
return result
}, dataRef)
if (Array.isArray(table._locales)) {

View File

@@ -3,7 +3,13 @@ import type { FlattenedArrayField } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import type { DrizzleAdapter } from '../../types.js'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
import type {
ArrayRowToInsert,
BlockRowToInsert,
NumberToDelete,
RelationshipToDelete,
TextToDelete,
} from './types.js'
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
import { traverseFields } from './traverseFields.js'
@@ -20,6 +26,7 @@ type Args = {
field: FlattenedArrayField
locale?: string
numbers: Record<string, unknown>[]
numbersToDelete: NumberToDelete[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
@@ -28,6 +35,7 @@ type Args = {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
textsToDelete: TextToDelete[]
/**
* Set to a locale code if this set of fields is traversed within a
* localized array or block field
@@ -45,12 +53,14 @@ export const transformArray = ({
field,
locale,
numbers,
numbersToDelete,
parentIsLocalized,
path,
relationships,
relationshipsToDelete,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
}: Args) => {
const newRows: ArrayRowToInsert[] = []
@@ -104,6 +114,7 @@ export const transformArray = ({
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName: arrayTableName,
path: `${path || ''}${field.name}.${i}.`,
@@ -112,6 +123,7 @@ export const transformArray = ({
row: newRow.row,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
})

View File

@@ -4,7 +4,12 @@ import { fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
import type { BlockRowToInsert, RelationshipToDelete } from './types.js'
import type {
BlockRowToInsert,
NumberToDelete,
RelationshipToDelete,
TextToDelete,
} from './types.js'
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
import { traverseFields } from './traverseFields.js'
@@ -20,6 +25,7 @@ type Args = {
field: FlattenedBlocksField
locale?: string
numbers: Record<string, unknown>[]
numbersToDelete: NumberToDelete[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
@@ -28,6 +34,7 @@ type Args = {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
textsToDelete: TextToDelete[]
/**
* Set to a locale code if this set of fields is traversed within a
* localized array or block field
@@ -43,12 +50,14 @@ export const transformBlocks = ({
field,
locale,
numbers,
numbersToDelete,
parentIsLocalized,
path,
relationships,
relationshipsToDelete,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
}: Args) => {
data.forEach((blockRow, i) => {
@@ -117,6 +126,7 @@ export const transformBlocks = ({
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName: blockTableName,
path: `${path || ''}${field.name}.${i}.`,
@@ -125,6 +135,7 @@ export const transformBlocks = ({
row: newRow.row,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
})

View File

@@ -29,11 +29,13 @@ export const transformForWrite = ({
blocksToDelete: new Set(),
locales: {},
numbers: [],
numbersToDelete: [],
relationships: [],
relationshipsToDelete: [],
row: {},
selects: {},
texts: [],
textsToDelete: [],
}
// This function is responsible for building up the
@@ -50,6 +52,7 @@ export const transformForWrite = ({
fields,
locales: rowToInsert.locales,
numbers: rowToInsert.numbers,
numbersToDelete: rowToInsert.numbersToDelete,
parentIsLocalized,
parentTableName: tableName,
path,
@@ -58,6 +61,7 @@ export const transformForWrite = ({
row: rowToInsert.row,
selects: rowToInsert.selects,
texts: rowToInsert.texts,
textsToDelete: rowToInsert.textsToDelete,
})
return rowToInsert

View File

@@ -5,7 +5,13 @@ import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
import type {
ArrayRowToInsert,
BlockRowToInsert,
NumberToDelete,
RelationshipToDelete,
TextToDelete,
} from './types.js'
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
@@ -51,6 +57,7 @@ type Args = {
[locale: string]: Record<string, unknown>
}
numbers: Record<string, unknown>[]
numbersToDelete: NumberToDelete[]
parentIsLocalized: boolean
/**
* This is the name of the parent table
@@ -64,6 +71,7 @@ type Args = {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
textsToDelete: TextToDelete[]
/**
* Set to a locale code if this set of fields is traversed within a
* localized array or block field
@@ -86,6 +94,7 @@ export const traverseFields = ({
insideArrayOrBlock = false,
locales,
numbers,
numbersToDelete,
parentIsLocalized,
parentTableName,
path,
@@ -94,6 +103,7 @@ export const traverseFields = ({
row,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
}: Args) => {
if (row._uuid) {
@@ -136,12 +146,14 @@ export const traverseFields = ({
field,
locale: localeKey,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale: localeKey,
})
@@ -159,12 +171,14 @@ export const traverseFields = ({
data: data[field.name],
field,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
})
@@ -202,12 +216,14 @@ export const traverseFields = ({
field,
locale: localeKey,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale: localeKey,
})
}
@@ -222,12 +238,14 @@ export const traverseFields = ({
data: fieldData,
field,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
})
}
@@ -257,6 +275,7 @@ export const traverseFields = ({
insideArrayOrBlock,
locales,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
path: `${path || ''}${field.name}.`,
@@ -265,6 +284,7 @@ export const traverseFields = ({
row,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale: localeKey,
})
})
@@ -287,6 +307,7 @@ export const traverseFields = ({
insideArrayOrBlock,
locales,
numbers,
numbersToDelete,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
path: `${path || ''}${field.name}.`,
@@ -295,6 +316,7 @@ export const traverseFields = ({
row,
selects,
texts,
textsToDelete,
withinArrayOrBlockLocale,
})
}
@@ -380,6 +402,11 @@ export const traverseFields = ({
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
if (!localeData.length) {
textsToDelete.push({ locale: localeKey, path: textPath })
return
}
transformTexts({
baseRow: {
locale: localeKey,
@@ -392,6 +419,11 @@ export const traverseFields = ({
})
}
} else if (Array.isArray(fieldData)) {
if (!fieldData.length) {
textsToDelete.push({ locale: withinArrayOrBlockLocale, path: textPath })
return
}
transformTexts({
baseRow: {
locale: withinArrayOrBlockLocale,
@@ -412,6 +444,11 @@ export const traverseFields = ({
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
if (!localeData.length) {
numbersToDelete.push({ locale: localeKey, path: numberPath })
return
}
transformNumbers({
baseRow: {
locale: localeKey,
@@ -424,6 +461,11 @@ export const traverseFields = ({
})
}
} else if (Array.isArray(fieldData)) {
if (!fieldData.length) {
numbersToDelete.push({ locale: withinArrayOrBlockLocale, path: numberPath })
return
}
transformNumbers({
baseRow: {
locale: withinArrayOrBlockLocale,

View File

@@ -23,6 +23,16 @@ export type RelationshipToDelete = {
path: string
}
export type TextToDelete = {
locale?: string
path: string
}
export type NumberToDelete = {
locale?: string
path: string
}
export type RowToInsert = {
arrays: {
[tableName: string]: ArrayRowToInsert[]
@@ -35,6 +45,7 @@ export type RowToInsert = {
[locale: string]: Record<string, unknown>
}
numbers: Record<string, unknown>[]
numbersToDelete: NumberToDelete[]
relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[]
row: Record<string, unknown>
@@ -42,4 +53,5 @@ export type RowToInsert = {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
textsToDelete: TextToDelete[]
}

View File

@@ -211,7 +211,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
parentColumnName: 'parent',
parentID: insertedRow.id,
pathColumnName: 'path',
rows: textsToInsert,
rows: [...textsToInsert, ...rowToInsert.textsToDelete],
tableName: textsTableName,
})
}
@@ -238,7 +238,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
parentColumnName: 'parent',
parentID: insertedRow.id,
pathColumnName: 'path',
rows: numbersToInsert,
rows: [...numbersToInsert, ...rowToInsert.numbersToDelete],
tableName: numbersTableName,
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.39.1",
"version": "3.40.0",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.39.1",
"version": "3.40.0",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -36,7 +36,7 @@
"eslint-plugin-jest-dom": "5.5.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.9.1",
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-hooks": "0.0.0-experimental-d331ba04-20250307",
"eslint-plugin-regexp": "2.7.0",
"globals": "16.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.39.1",
"version": "3.40.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -17,6 +17,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.39.1",
"version": "3.40.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -2,18 +2,27 @@
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview'
import { useCallback, useEffect, useRef, useState } from 'react'
// To prevent the flicker of missing data on initial load,
// you can pass in the initial page data from the server
// To prevent the flicker of stale data while the post message is being sent,
// you can conditionally render loading UI based on the `isLoading` state
export const useLivePreview = <T extends Record<string, unknown>>(props: {
/**
* This is a React hook to implement {@link https://payloadcms.com/docs/live-preview/overview Payload Live Preview}.
*
* @link https://payloadcms.com/docs/live-preview/frontend
*/
// NOTE: cannot use Record<string, unknown> here bc generated interfaces will not satisfy the type constraint
export const useLivePreview = <T extends Record<string, any>>(props: {
apiRoute?: string
depth?: number
/**
* To prevent the flicker of missing data on initial load,
* you can pass in the initial page data from the server.
*/
initialData: T
serverURL: string
}): {
data: T
/**
* To prevent the flicker of stale data while the post message is being sent,
* you can conditionally render loading UI based on the `isLoading` state.
*/
isLoading: boolean
} => {
const { apiRoute, depth, initialData, serverURL } = props

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.39.1",
"version": "3.40.0",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -4,17 +4,25 @@ import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview'
import { onMounted, onUnmounted, ref } from 'vue'
/**
* Vue composable to implement Payload CMS Live Preview.
* This is a Vue composable to implement {@link https://payloadcms.com/docs/live-preview/overview Payload Live Preview}.
*
* {@link https://payloadcms.com/docs/live-preview/frontend View the documentation}
* @link https://payloadcms.com/docs/live-preview/frontend
*/
export const useLivePreview = <T extends Record<string, unknown>>(props: {
export const useLivePreview = <T extends Record<string, any>>(props: {
apiRoute?: string
depth?: number
/**
* To prevent the flicker of missing data on initial load,
* you can pass in the initial page data from the server.
*/
initialData: T
serverURL: string
}): {
data: Ref<T>
/**
* To prevent the flicker of stale data while the post message is being sent,
* you can conditionally render loading UI based on the `isLoading` state.
*/
isLoading: Ref<boolean>
} => {
const { apiRoute, depth, initialData, serverURL } = props

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.39.1",
"version": "3.40.0",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -18,6 +18,7 @@
}
],
"type": "module",
"sideEffects": false,
"exports": {
".": {
"import": "./src/index.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.39.1",
"version": "3.40.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -16,6 +16,10 @@
"url": "https://payloadcms.com"
}
],
"sideEffects": [
"*.scss",
"*.css"
],
"type": "module",
"exports": {
".": {
@@ -104,22 +108,22 @@
"uuid": "10.0.0"
},
"devDependencies": {
"@babel/cli": "7.26.4",
"@babel/core": "7.26.7",
"@babel/preset-env": "7.26.7",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@next/eslint-plugin-next": "15.3.0",
"@babel/cli": "7.27.2",
"@babel/core": "7.27.3",
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@next/eslint-plugin-next": "15.3.2",
"@payloadcms/eslint-config": "workspace:*",
"@types/busboy": "1.5.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"esbuild": "0.24.2",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"esbuild": "0.25.5",
"esbuild-sass-plugin": "3.3.1",
"payload": "workspace:*",
"swc-plugin-transform-remove-imports": "3.1.0"
"swc-plugin-transform-remove-imports": "4.0.4"
},
"peerDependencies": {
"graphql": "^16.8.1",

View File

@@ -1,3 +1,5 @@
@import '~@payloadcms/ui/scss';
@layer payload-default {
.doc-tab {
display: flex;

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.doc-tabs {

View File

@@ -62,11 +62,31 @@ export const DocumentTabs: React.FC<{
(condition &&
Boolean(condition({ collectionConfig, config, globalConfig, permissions })))
const path = viewConfig && 'path' in viewConfig ? viewConfig.path : ''
if (meetsCondition) {
if (tabFromConfig?.Component) {
return RenderServerComponent({
clientProps: {
path,
} satisfies DocumentTabClientProps,
Component: tabFromConfig.Component,
importMap: payload.importMap,
key: `tab-${index}`,
serverProps: {
collectionConfig,
globalConfig,
i18n,
payload,
permissions,
} satisfies DocumentTabServerPropsOnly,
})
}
return (
<DocumentTab
key={`tab-${index}`}
path={viewConfig && 'path' in viewConfig ? viewConfig.path : ''}
path={path}
{...{
...props,
...(tab || {}),

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.doc-header {

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.nav {

View File

@@ -23,20 +23,11 @@ export const DefaultNavClient: React.FC<{
admin: {
routes: { browseByFolder: foldersRoute },
},
collections,
folders,
routes: { admin: adminRoute },
},
} = useConfig()
const [folderCollectionSlugs] = React.useState<string[]>(() => {
return collections.reduce<string[]>((acc, collection) => {
if (collection.folders) {
acc.push(collection.slug)
}
return acc
}, [])
})
const { i18n } = useTranslation()
const folderURL = formatAdminURL({
@@ -48,7 +39,7 @@ export const DefaultNavClient: React.FC<{
return (
<Fragment>
{folderCollectionSlugs.length > 0 && <BrowseByFolderButton active={viewingRootFolderView} />}
{folders && folders.browseByFolder && <BrowseByFolderButton active={viewingRootFolderView} />}
{groups.map(({ entities, label }, key) => {
return (
<NavGroup isOpen={navPreferences?.groups?.[label]?.open} key={key} label={label}>

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.nav {

View File

@@ -99,6 +99,7 @@ export const POST =
(config: Promise<SanitizedConfig> | SanitizedConfig) => async (request: Request) => {
const originalRequest = request.clone()
const req = await createPayloadRequest({
canSetHeaders: true,
config,
request,
})

View File

@@ -1,207 +0,0 @@
@layer payload-default, payload;
@import 'styles';
@import './toasts.scss';
@import './colors.scss';
@layer payload-default {
:root {
--base-px: 20;
--base-body-size: 13;
--base: calc((var(--base-px) / var(--base-body-size)) * 1rem);
--breakpoint-xs-width: #{$breakpoint-xs-width};
--breakpoint-s-width: #{$breakpoint-s-width};
--breakpoint-m-width: #{$breakpoint-m-width};
--breakpoint-l-width: #{$breakpoint-l-width};
--scrollbar-width: 17px;
--theme-bg: var(--theme-elevation-0);
--theme-input-bg: var(--theme-elevation-0);
--theme-text: var(--theme-elevation-800);
--theme-overlay: rgba(5, 5, 5, 0.5);
--theme-baseline: #{$baseline-px};
--theme-baseline-body-size: #{$baseline-body-size};
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
--font-serif: 'Georgia', 'Bitstream Charter', 'Charis SIL', Utopia, 'URW Bookman L', serif;
--font-mono: 'SF Mono', Menlo, Consolas, Monaco, monospace;
--style-radius-s: #{$style-radius-s};
--style-radius-m: #{$style-radius-m};
--style-radius-l: #{$style-radius-l};
--z-popup: 10;
--z-nav: 20;
--z-modal: 30;
--z-status: 40;
--accessibility-outline: 2px solid var(--theme-text);
--accessibility-outline-offset: 2px;
--gutter-h: #{base(3)};
--spacing-view-bottom: var(--gutter-h);
--doc-controls-height: calc(var(--base) * 2.8);
--app-header-height: calc(var(--base) * 2.8);
--nav-width: 275px;
--nav-trans-time: 150ms;
@include mid-break {
--gutter-h: #{base(2)};
--app-header-height: calc(var(--base) * 2.4);
--doc-controls-height: calc(var(--base) * 2.4);
}
@include small-break {
--gutter-h: #{base(0.8)};
--spacing-view-bottom: calc(var(--base) * 2);
--nav-width: 100vw;
}
}
/////////////////////////////
// GLOBAL STYLES
/////////////////////////////
* {
box-sizing: border-box;
}
html {
@extend %body;
background: var(--theme-bg);
-webkit-font-smoothing: antialiased;
&[data-theme='dark'] {
--theme-bg: var(--theme-elevation-0);
--theme-text: var(--theme-elevation-1000);
--theme-input-bg: var(--theme-elevation-50);
--theme-overlay: rgba(5, 5, 5, 0.75);
color-scheme: dark;
::selection {
color: var(--color-base-1000);
}
::-moz-selection {
color: var(--color-base-1000);
}
}
@include mid-break {
font-size: 12px;
}
}
html,
body,
#app {
height: 100%;
}
body {
font-family: var(--font-body);
font-weight: 400;
color: var(--theme-text);
margin: 0;
// this is for the nav to be able to push the document over
overflow-x: hidden;
}
::selection {
background: var(--color-success-250);
color: var(--theme-base-800);
}
::-moz-selection {
background: var(--color-success-250);
color: var(--theme-base-800);
}
img {
max-width: 100%;
height: auto;
display: block;
}
h1 {
@extend %h1;
}
h2 {
@extend %h2;
}
h3 {
@extend %h3;
}
h4 {
@extend %h4;
}
h5 {
@extend %h5;
}
h6 {
@extend %h6;
}
p {
margin: 0;
}
ul,
ol {
padding-left: $baseline;
margin: 0;
}
:focus-visible {
outline: var(--accessibility-outline);
}
a {
color: currentColor;
&:focus {
&:not(:focus-visible) {
opacity: 0.8;
}
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
}
svg {
vertical-align: middle;
}
dialog {
width: 100%;
border: 0;
padding: 0;
color: currentColor;
}
.payload__modal-item {
min-height: 100%;
background: transparent;
}
.payload__modal-container--enterDone {
overflow: auto;
}
.payload__modal-item--enter,
.payload__modal-item--enterDone {
z-index: var(--z-modal);
}
// @import '~payload-user-css'; TODO: re-enable this
}

View File

@@ -1,271 +0,0 @@
@layer payload-default {
:root {
--color-base-0: rgb(255, 255, 255);
--color-base-50: rgb(245, 245, 245);
--color-base-100: rgb(235, 235, 235);
--color-base-150: rgb(221, 221, 221);
--color-base-200: rgb(208, 208, 208);
--color-base-250: rgb(195, 195, 195);
--color-base-300: rgb(181, 181, 181);
--color-base-350: rgb(168, 168, 168);
--color-base-400: rgb(154, 154, 154);
--color-base-450: rgb(141, 141, 141);
--color-base-500: rgb(128, 128, 128);
--color-base-550: rgb(114, 114, 114);
--color-base-600: rgb(101, 101, 101);
--color-base-650: rgb(87, 87, 87);
--color-base-700: rgb(74, 74, 74);
--color-base-750: rgb(60, 60, 60);
--color-base-800: rgb(47, 47, 47);
--color-base-850: rgb(34, 34, 34);
--color-base-900: rgb(20, 20, 20);
--color-base-950: rgb(7, 7, 7);
--color-base-1000: rgb(0, 0, 0);
--color-success-50: rgb(237, 245, 249);
--color-success-100: rgb(218, 237, 248);
--color-success-150: rgb(188, 225, 248);
--color-success-200: rgb(156, 216, 253);
--color-success-250: rgb(125, 204, 248);
--color-success-300: rgb(97, 190, 241);
--color-success-350: rgb(65, 178, 236);
--color-success-400: rgb(36, 164, 223);
--color-success-450: rgb(18, 148, 204);
--color-success-500: rgb(21, 135, 186);
--color-success-550: rgb(12, 121, 168);
--color-success-600: rgb(11, 110, 153);
--color-success-650: rgb(11, 97, 135);
--color-success-700: rgb(17, 88, 121);
--color-success-750: rgb(17, 76, 105);
--color-success-800: rgb(18, 66, 90);
--color-success-850: rgb(18, 56, 76);
--color-success-900: rgb(19, 44, 58);
--color-success-950: rgb(22, 33, 39);
--color-error-50: rgb(250, 241, 240);
--color-error-100: rgb(252, 229, 227);
--color-error-150: rgb(247, 208, 204);
--color-error-200: rgb(254, 193, 188);
--color-error-250: rgb(253, 177, 170);
--color-error-300: rgb(253, 154, 146);
--color-error-350: rgb(253, 131, 123);
--color-error-400: rgb(246, 109, 103);
--color-error-450: rgb(234, 90, 86);
--color-error-500: rgb(218, 75, 72);
--color-error-550: rgb(200, 62, 61);
--color-error-600: rgb(182, 54, 54);
--color-error-650: rgb(161, 47, 47);
--color-error-700: rgb(144, 44, 43);
--color-error-750: rgb(123, 41, 39);
--color-error-800: rgb(105, 39, 37);
--color-error-850: rgb(86, 36, 33);
--color-error-900: rgb(64, 32, 29);
--color-error-950: rgb(44, 26, 24);
--color-warning-50: rgb(249, 242, 237);
--color-warning-100: rgb(248, 232, 219);
--color-warning-150: rgb(243, 212, 186);
--color-warning-200: rgb(243, 200, 162);
--color-warning-250: rgb(240, 185, 136);
--color-warning-300: rgb(238, 166, 98);
--color-warning-350: rgb(234, 148, 58);
--color-warning-400: rgb(223, 132, 17);
--color-warning-450: rgb(204, 120, 15);
--color-warning-500: rgb(185, 108, 13);
--color-warning-550: rgb(167, 97, 10);
--color-warning-600: rgb(150, 87, 11);
--color-warning-650: rgb(134, 78, 11);
--color-warning-700: rgb(120, 70, 13);
--color-warning-750: rgb(105, 61, 13);
--color-warning-800: rgb(90, 55, 19);
--color-warning-850: rgb(73, 47, 21);
--color-warning-900: rgb(56, 38, 20);
--color-warning-950: rgb(38, 29, 21);
--color-blue-50: rgb(237, 245, 249);
--color-blue-100: rgb(218, 237, 248);
--color-blue-150: rgb(188, 225, 248);
--color-blue-200: rgb(156, 216, 253);
--color-blue-250: rgb(125, 204, 248);
--color-blue-300: rgb(97, 190, 241);
--color-blue-350: rgb(65, 178, 236);
--color-blue-400: rgb(36, 164, 223);
--color-blue-450: rgb(18, 148, 204);
--color-blue-500: rgb(21, 135, 186);
--color-blue-550: rgb(12, 121, 168);
--color-blue-600: rgb(11, 110, 153);
--color-blue-650: rgb(11, 97, 135);
--color-blue-700: rgb(17, 88, 121);
--color-blue-750: rgb(17, 76, 105);
--color-blue-800: rgb(18, 66, 90);
--color-blue-850: rgb(18, 56, 76);
--color-blue-900: rgb(19, 44, 58);
--color-blue-950: rgb(22, 33, 39);
--theme-border-color: var(--theme-elevation-150);
--theme-success-50: var(--color-success-50);
--theme-success-100: var(--color-success-100);
--theme-success-150: var(--color-success-150);
--theme-success-200: var(--color-success-200);
--theme-success-250: var(--color-success-250);
--theme-success-300: var(--color-success-300);
--theme-success-350: var(--color-success-350);
--theme-success-400: var(--color-success-400);
--theme-success-450: var(--color-success-450);
--theme-success-500: var(--color-success-500);
--theme-success-550: var(--color-success-550);
--theme-success-600: var(--color-success-600);
--theme-success-650: var(--color-success-650);
--theme-success-700: var(--color-success-700);
--theme-success-750: var(--color-success-750);
--theme-success-800: var(--color-success-800);
--theme-success-850: var(--color-success-850);
--theme-success-900: var(--color-success-900);
--theme-success-950: var(--color-success-950);
--theme-warning-50: var(--color-warning-50);
--theme-warning-100: var(--color-warning-100);
--theme-warning-150: var(--color-warning-150);
--theme-warning-200: var(--color-warning-200);
--theme-warning-250: var(--color-warning-250);
--theme-warning-300: var(--color-warning-300);
--theme-warning-350: var(--color-warning-350);
--theme-warning-400: var(--color-warning-400);
--theme-warning-450: var(--color-warning-450);
--theme-warning-500: var(--color-warning-500);
--theme-warning-550: var(--color-warning-550);
--theme-warning-600: var(--color-warning-600);
--theme-warning-650: var(--color-warning-650);
--theme-warning-700: var(--color-warning-700);
--theme-warning-750: var(--color-warning-750);
--theme-warning-800: var(--color-warning-800);
--theme-warning-850: var(--color-warning-850);
--theme-warning-900: var(--color-warning-900);
--theme-warning-950: var(--color-warning-950);
--theme-error-50: var(--color-error-50);
--theme-error-100: var(--color-error-100);
--theme-error-150: var(--color-error-150);
--theme-error-200: var(--color-error-200);
--theme-error-250: var(--color-error-250);
--theme-error-300: var(--color-error-300);
--theme-error-350: var(--color-error-350);
--theme-error-400: var(--color-error-400);
--theme-error-450: var(--color-error-450);
--theme-error-500: var(--color-error-500);
--theme-error-550: var(--color-error-550);
--theme-error-600: var(--color-error-600);
--theme-error-650: var(--color-error-650);
--theme-error-700: var(--color-error-700);
--theme-error-750: var(--color-error-750);
--theme-error-800: var(--color-error-800);
--theme-error-850: var(--color-error-850);
--theme-error-900: var(--color-error-900);
--theme-error-950: var(--color-error-950);
--theme-elevation-0: var(--color-base-0);
--theme-elevation-50: var(--color-base-50);
--theme-elevation-100: var(--color-base-100);
--theme-elevation-150: var(--color-base-150);
--theme-elevation-200: var(--color-base-200);
--theme-elevation-250: var(--color-base-250);
--theme-elevation-300: var(--color-base-300);
--theme-elevation-350: var(--color-base-350);
--theme-elevation-400: var(--color-base-400);
--theme-elevation-450: var(--color-base-450);
--theme-elevation-500: var(--color-base-500);
--theme-elevation-550: var(--color-base-550);
--theme-elevation-600: var(--color-base-600);
--theme-elevation-650: var(--color-base-650);
--theme-elevation-700: var(--color-base-700);
--theme-elevation-750: var(--color-base-750);
--theme-elevation-800: var(--color-base-800);
--theme-elevation-850: var(--color-base-850);
--theme-elevation-900: var(--color-base-900);
--theme-elevation-950: var(--color-base-950);
--theme-elevation-1000: var(--color-base-1000);
}
html[data-theme='dark'] {
--theme-border-color: var(--theme-elevation-150);
--theme-elevation-0: var(--color-base-900);
--theme-elevation-50: var(--color-base-850);
--theme-elevation-100: var(--color-base-800);
--theme-elevation-150: var(--color-base-750);
--theme-elevation-200: var(--color-base-700);
--theme-elevation-250: var(--color-base-650);
--theme-elevation-300: var(--color-base-600);
--theme-elevation-350: var(--color-base-550);
--theme-elevation-400: var(--color-base-450);
--theme-elevation-450: var(--color-base-400);
--theme-elevation-550: var(--color-base-350);
--theme-elevation-600: var(--color-base-300);
--theme-elevation-650: var(--color-base-250);
--theme-elevation-700: var(--color-base-200);
--theme-elevation-750: var(--color-base-150);
--theme-elevation-800: var(--color-base-100);
--theme-elevation-850: var(--color-base-50);
--theme-elevation-900: var(--color-base-0);
--theme-elevation-950: var(--color-base-0);
--theme-elevation-1000: var(--color-base-0);
--theme-success-50: var(--color-success-950);
--theme-success-100: var(--color-success-900);
--theme-success-150: var(--color-success-850);
--theme-success-200: var(--color-success-800);
--theme-success-250: var(--color-success-750);
--theme-success-300: var(--color-success-700);
--theme-success-350: var(--color-success-650);
--theme-success-400: var(--color-success-600);
--theme-success-450: var(--color-success-550);
--theme-success-550: var(--color-success-450);
--theme-success-600: var(--color-success-400);
--theme-success-650: var(--color-success-350);
--theme-success-700: var(--color-success-300);
--theme-success-750: var(--color-success-250);
--theme-success-800: var(--color-success-200);
--theme-success-850: var(--color-success-150);
--theme-success-900: var(--color-success-100);
--theme-success-950: var(--color-success-50);
--theme-warning-50: var(--color-warning-950);
--theme-warning-100: var(--color-warning-900);
--theme-warning-150: var(--color-warning-850);
--theme-warning-200: var(--color-warning-800);
--theme-warning-250: var(--color-warning-750);
--theme-warning-300: var(--color-warning-700);
--theme-warning-350: var(--color-warning-650);
--theme-warning-400: var(--color-warning-600);
--theme-warning-450: var(--color-warning-550);
--theme-warning-550: var(--color-warning-450);
--theme-warning-600: var(--color-warning-400);
--theme-warning-650: var(--color-warning-350);
--theme-warning-700: var(--color-warning-300);
--theme-warning-750: var(--color-warning-250);
--theme-warning-800: var(--color-warning-200);
--theme-warning-850: var(--color-warning-150);
--theme-warning-900: var(--color-warning-100);
--theme-warning-950: var(--color-warning-50);
--theme-error-50: var(--color-error-950);
--theme-error-100: var(--color-error-900);
--theme-error-150: var(--color-error-850);
--theme-error-200: var(--color-error-800);
--theme-error-250: var(--color-error-750);
--theme-error-300: var(--color-error-700);
--theme-error-350: var(--color-error-650);
--theme-error-400: var(--color-error-600);
--theme-error-450: var(--color-error-550);
--theme-error-550: var(--color-error-450);
--theme-error-600: var(--color-error-400);
--theme-error-650: var(--color-error-350);
--theme-error-700: var(--color-error-300);
--theme-error-750: var(--color-error-250);
--theme-error-800: var(--color-error-200);
--theme-error-850: var(--color-error-150);
--theme-error-900: var(--color-error-100);
--theme-error-950: var(--color-error-50);
}
}

View File

@@ -1 +0,0 @@
/* Used as a placeholder for when the Payload app does not have custom CSS */

View File

@@ -1,27 +0,0 @@
////////////////////////////
// MEDIA QUERIES
/////////////////////////////
@mixin extra-small-break {
@media (max-width: $breakpoint-xs-width) {
@content;
}
}
@mixin small-break {
@media (max-width: $breakpoint-s-width) {
@content;
}
}
@mixin mid-break {
@media (max-width: $breakpoint-m-width) {
@content;
}
}
@mixin large-break {
@media (max-width: $breakpoint-l-width) {
@content;
}
}

View File

@@ -1,19 +0,0 @@
@layer payload-default {
%btn-reset {
border: 0;
background: none;
box-shadow: none;
border-radius: 0;
padding: 0;
color: currentColor;
}
}
@mixin btn-reset {
border: 0;
background: none;
box-shadow: none;
border-radius: 0;
padding: 0;
color: currentColor;
}

View File

@@ -1,11 +0,0 @@
@import 'vars';
@import 'z-index';
//////////////////////////////
// IMPORT OVERRIDES
//////////////////////////////
@import 'type';
@import 'queries';
@import 'resets';
@import 'svg';

View File

@@ -1,10 +0,0 @@
@mixin color-svg($color) {
.stroke {
stroke: $color;
fill: none;
}
.fill {
fill: $color;
}
}

View File

@@ -1,61 +0,0 @@
@import 'vars';
@import 'queries';
@layer payload-default {
.Toastify {
.Toastify__toast-container {
left: base(5);
transform: none;
right: base(5);
width: auto;
}
.Toastify__toast {
padding: base(0.5);
border-radius: $style-radius-m;
font-weight: 600;
}
.Toastify__close-button {
align-self: center;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.Toastify__toast--success {
color: var(--color-success-900);
background: var(--color-success-500);
.Toastify__progress-bar {
background-color: var(--color-success-900);
}
}
.Toastify__close-button--success {
color: var(--color-success-900);
}
.Toastify__toast--error {
background: var(--theme-error-500);
color: #fff;
.Toastify__progress-bar {
background-color: #fff;
}
}
.Toastify__close-button--light {
color: inherit;
}
@include mid-break {
.Toastify__toast-container {
left: $baseline;
right: $baseline;
}
}
}
}

View File

@@ -1,144 +0,0 @@
@import './styles.scss';
@layer payload-default {
.payload-toast-container {
padding: 0;
margin: 0;
.payload-toast-close-button {
position: absolute;
order: 3;
left: unset;
inset-inline-end: base(0.8);
top: 50%;
transform: translateY(-50%);
color: var(--theme-elevation-600);
background: unset;
border: none;
svg {
width: base(0.8);
height: base(0.8);
}
&:hover {
color: var(--theme-elevation-250);
background: none;
}
[dir='RTL'] & {
right: unset;
left: 0.5rem;
}
}
.toast-title {
line-height: base(1);
margin-right: base(1);
}
.payload-toast-item {
padding: base(0.8);
color: var(--theme-elevation-800);
font-style: normal;
font-weight: 600;
display: flex;
gap: 1rem;
align-items: center;
width: 100%;
border-radius: 4px;
border: 1px solid var(--theme-border-color);
background: var(--theme-input-bg);
box-shadow:
0px 10px 4px -8px rgba(0, 2, 4, 0.02),
0px 2px 3px 0px rgba(0, 2, 4, 0.05);
.toast-content {
transition: opacity 100ms cubic-bezier(0.55, 0.055, 0.675, 0.19);
width: 100%;
}
&[data-front='false'] {
.toast-content {
opacity: 0;
}
}
&[data-expanded='true'] {
.toast-content {
opacity: 1;
}
}
.toast-icon {
width: base(0.8);
height: base(0.8);
margin: 0;
display: flex;
align-items: center;
justify-content: center;
& > * {
width: base(1.2);
height: base(1.2);
}
}
&.toast-warning {
color: var(--theme-warning-800);
border-color: var(--theme-warning-250);
background-color: var(--theme-warning-100);
.payload-toast-close-button {
color: var(--theme-warning-600);
&:hover {
color: var(--theme-warning-250);
}
}
}
&.toast-error {
color: var(--theme-error-800);
border-color: var(--theme-error-250);
background-color: var(--theme-error-100);
.payload-toast-close-button {
color: var(--theme-error-600);
&:hover {
color: var(--theme-error-250);
}
}
}
&.toast-success {
color: var(--theme-success-800);
border-color: var(--theme-success-250);
background-color: var(--theme-success-100);
.payload-toast-close-button {
color: var(--theme-success-600);
&:hover {
color: var(--theme-success-250);
}
}
}
&.toast-info {
color: var(--theme-elevation-800);
border-color: var(--theme-elevation-250);
background-color: var(--theme-elevation-100);
.payload-toast-close-button {
color: var(--theme-elevation-600);
&:hover {
color: var(--theme-elevation-250);
}
}
}
}
}
}

View File

@@ -1,110 +0,0 @@
@import 'vars';
@import 'queries';
/////////////////////////////
// HEADINGS
/////////////////////////////
@layer payload-default {
%h1,
%h2,
%h3,
%h4,
%h5,
%h6 {
font-family: var(--font-body);
font-weight: 500;
}
%h1 {
margin: 0;
font-size: base(1.6);
line-height: base(1.8);
@include small-break {
letter-spacing: -0.5px;
font-size: base(1.25);
}
}
%h2 {
margin: 0;
font-size: base(1.3);
line-height: base(1.6);
@include small-break {
font-size: base(0.85);
}
}
%h3 {
margin: 0;
font-size: base(1);
line-height: base(1.2);
@include small-break {
font-size: base(0.65);
line-height: 1.25;
}
}
%h4 {
margin: 0;
font-size: base(0.8);
line-height: base(1);
letter-spacing: -0.375px;
}
%h5 {
margin: 0;
font-size: base(0.65);
line-height: base(0.8);
}
%h6 {
margin: 0;
font-size: base(0.6);
line-height: base(0.8);
}
%small {
margin: 0;
font-size: 12px;
line-height: 20px;
}
/////////////////////////////
// TYPE STYLES
/////////////////////////////
%large-body {
font-size: base(0.6);
line-height: base(1);
letter-spacing: base(0.02);
@include mid-break {
font-size: base(0.7);
line-height: base(1);
}
@include small-break {
font-size: base(0.55);
line-height: base(0.75);
}
}
%body {
font-size: $baseline-body-size;
line-height: $baseline-px;
font-weight: normal;
font-family: var(--font-body);
}
%code {
font-size: base(0.4);
color: var(--theme-elevation-400);
span {
color: var(--theme-elevation-800);
}
}
}

View File

@@ -1,192 +0,0 @@
@use 'sass:math';
/////////////////////////////
// BREAKPOINTS
/////////////////////////////
$breakpoint-xs-width: 400px !default;
$breakpoint-s-width: 768px !default;
$breakpoint-m-width: 1024px !default;
$breakpoint-l-width: 1440px !default;
//////////////////////////////
// BASELINE GRID
//////////////////////////////
$baseline-px: 20px !default;
$baseline-body-size: 13px !default;
$baseline: math.div($baseline-px, $baseline-body-size) + rem;
@function base($multiplier) {
@return (math.div($baseline-px, $baseline-body-size) * $multiplier) + rem;
}
//////////////////////////////
// COLORS (DEPRECATED. DO NOT USE. PREFER CSS VARIABLES)
//////////////////////////////
$color-dark-gray: #333333 !default;
$color-gray: #9a9a9a !default;
$color-light-gray: #dadada !default;
$color-background-gray: #f3f3f3 !default;
$color-red: #ff6f76 !default;
$color-yellow: #fdffa4 !default;
$color-green: #b2ffd6 !default;
$color-purple: #f3ddf3 !default;
//////////////////////////////
// STYLES
//////////////////////////////
$style-radius-s: 3px !default;
$style-radius-m: 4px !default;
$style-radius-l: 8px !default;
$style-stroke-width: 1px !default;
$style-stroke-width-s: 1px !default;
$style-stroke-width-m: 2px !default;
//////////////////////////////
// MISC
//////////////////////////////
$top-header-offset: calc(base(1) - 1px);
$top-header-offset-m: base(3);
$focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
//////////////////////////////
// SHADOWS
//////////////////////////////
@mixin shadow-sm {
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
@mixin shadow-m {
box-shadow: 0 4px 8px -3px rgba(0, 0, 0, 0.1);
}
@mixin shadow-lg {
box-shadow: 0 -2px 16px -2px rgba(0, 0, 0, 0.2);
}
@mixin shadow-lg-top {
box-shadow: 0 2px 16px -2px rgba(0, 0, 0, 0.2);
}
@mixin inputShadow {
@include shadow-sm;
&:not(:disabled) {
&:hover {
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.2);
}
}
}
@mixin soft-shadow-bottom {
box-shadow: 0 7px 14px 0px rgb(0 0 0 / 5%);
}
//////////////////////////////
// STYLE MIXINS
//////////////////////////////
@mixin blur-bg($color: var(--theme-bg), $opacity: 0.75) {
&:before,
&:after {
content: ' ';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&:before {
background: $color;
opacity: $opacity;
}
&:after {
backdrop-filter: blur(8px);
}
}
@mixin blur-bg-light {
@include blur-bg(var(--theme-bg), 0.3);
}
@mixin readOnly {
background: var(--theme-elevation-100);
color: var(--theme-elevation-400);
box-shadow: none;
&:hover {
border-color: var(--theme-elevation-150);
box-shadow: none;
}
}
@mixin formInput() {
@include inputShadow;
font-family: var(--font-body);
width: 100%;
border: 1px solid var(--theme-elevation-150);
border-radius: var(--style-radius-s);
background: var(--theme-input-bg);
color: var(--theme-elevation-800);
font-size: 1rem;
height: base(2);
line-height: base(1);
padding: base(0.4) base(0.75);
-webkit-appearance: none;
transition-property: border, box-shadow;
transition-duration: 100ms;
transition-timing-function: cubic-bezier(0, 0.2, 0.2, 1);
&[data-rtl='true'] {
direction: rtl;
}
&::-webkit-input-placeholder {
color: var(--theme-elevation-400);
font-weight: normal;
font-size: 1rem;
}
&::-moz-placeholder {
color: var(--theme-elevation-400);
font-weight: normal;
font-size: 1rem;
}
&:hover {
border-color: var(--theme-elevation-250);
}
&:focus,
&:focus-within,
&:active {
border-color: var(--theme-elevation-400);
outline: 0;
}
&:disabled {
@include readOnly;
}
}
@mixin lightInputError {
background-color: var(--theme-error-50);
border: 1px solid var(--theme-error-500);
}
@mixin darkInputError {
background-color: var(--theme-error-100);
border: 1px solid var(--theme-error-400);
&:hover {
border-color: var(--theme-error-500);
}
}

View File

@@ -1,9 +0,0 @@
/////////////////////////////
// Z-INDEX CHART (DEPRECATED. DO NOT USE. PREFER CSS VARIABLES)
/////////////////////////////
$z-page: 20;
$z-page-content: 30;
$z-nav: 40;
$z-modal: 50;
$z-status: 60;

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.template-default {

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.template-default {

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.template-minimal {

View File

@@ -49,11 +49,13 @@ const reqCache = selectiveCache<Result>('req')
* As access control and getting the request locale is dependent on the current URL and
*/
export const initReq = async function ({
canSetHeaders,
configPromise,
importMap,
key,
overrides,
}: {
canSetHeaders?: boolean
configPromise: Promise<SanitizedConfig> | SanitizedConfig
importMap: ImportMap
key: string
@@ -77,6 +79,7 @@ export const initReq = async function ({
})
const { responseHeaders, user } = await executeAuthStrategies({
canSetHeaders,
headers,
payload,
})

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
$tab-width: 16px;

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.query-inspector {

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.payload-settings {

View File

@@ -3,6 +3,7 @@ import type {
BuildCollectionFolderViewResult,
FolderListViewServerPropsOnly,
ListQuery,
Where,
} from 'payload'
import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
@@ -10,6 +11,7 @@ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerCompo
import { formatAdminURL } from '@payloadcms/ui/shared'
import { redirect } from 'next/navigation.js'
import { getFolderData } from 'payload'
import { buildFolderWhereConstraints } from 'payload/shared'
import React from 'react'
import { getPreferences } from '../../utilities/getPreferences.js'
@@ -29,10 +31,10 @@ export const buildBrowseByFolderView = async (
args: BuildFolderViewArgs,
): Promise<BuildCollectionFolderViewResult> => {
const {
browseByFolderSlugs: browseByFolderSlugsFromArgs = [],
disableBulkDelete,
disableBulkEdit,
enableRowSelections,
folderCollectionSlugs,
folderID,
initPageResult,
isInDrawer,
@@ -54,30 +56,83 @@ export const buildBrowseByFolderView = async (
visibleEntities,
} = initPageResult
const collections = folderCollectionSlugs.filter(
const browseByFolderSlugs = browseByFolderSlugsFromArgs.filter(
(collectionSlug) =>
permissions?.collections?.[collectionSlug]?.read &&
visibleEntities.collections.includes(collectionSlug),
)
if (!collections.length) {
if (config.folders === false || config.folders.browseByFolder === false) {
throw new Error('not-found')
}
const query = queryFromArgs || queryFromReq
const selectedCollectionSlugs: string[] =
Array.isArray(query?.relationTo) && query.relationTo.length
? query.relationTo
: [...folderCollectionSlugs, config.folders.slug]
? query.relationTo.filter(
(slug) =>
browseByFolderSlugs.includes(slug) || (config.folders && slug === config.folders.slug),
)
: [...browseByFolderSlugs, config.folders.slug]
const {
routes: { admin: adminRoute },
} = config
const folderCollectionConfig = payload.collections[config.folders.slug].config
const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>(
'browse-by-folder',
payload,
user.id,
user.collection,
)
let documentWhere: undefined | Where = undefined
let folderWhere: undefined | Where = undefined
// if folderID, dont make a documentWhere since it only queries root folders
for (const collectionSlug of selectedCollectionSlugs) {
if (collectionSlug === config.folders.slug) {
const folderCollectionConstraints = await buildFolderWhereConstraints({
collectionConfig: folderCollectionConfig,
folderID,
localeCode: fullLocale?.code,
req: initPageResult.req,
search: typeof query?.search === 'string' ? query.search : undefined,
})
if (folderCollectionConstraints) {
folderWhere = folderCollectionConstraints
}
} else if (folderID) {
if (!documentWhere) {
documentWhere = {
or: [],
}
}
const collectionConfig = payload.collections[collectionSlug].config
if (collectionConfig.folders && collectionConfig.folders.browseByFolder === true) {
const collectionConstraints = await buildFolderWhereConstraints({
collectionConfig,
folderID,
localeCode: fullLocale?.code,
req: initPageResult.req,
search: typeof query?.search === 'string' ? query.search : undefined,
})
if (collectionConstraints) {
documentWhere.or.push(collectionConstraints)
}
}
}
}
const { breadcrumbs, documents, subfolders } = await getFolderData({
documentWhere,
folderID,
folderWhere,
req: initPageResult.req,
search: query?.search as string,
})
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
@@ -96,13 +151,6 @@ export const buildBrowseByFolderView = async (
)
}
const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>(
'browse-by-folder',
payload,
user.id,
user.collection,
)
const serverProps: Omit<FolderListViewServerPropsOnly, 'collectionConfig' | 'listPreferences'> = {
documents,
i18n,
@@ -125,7 +173,7 @@ export const buildBrowseByFolderView = async (
// documents cannot be created without a parent folder in this view
const hasCreatePermissionCollectionSlugs = folderID
? [config.folders.slug, ...folderCollectionSlugs]
? [config.folders.slug, ...browseByFolderSlugs]
: [config.folders.slug]
return {
@@ -134,7 +182,8 @@ export const buildBrowseByFolderView = async (
breadcrumbs={breadcrumbs}
documents={documents}
filteredCollectionSlugs={selectedCollectionSlugs}
folderCollectionSlugs={folderCollectionSlugs}
folderCollectionSlugs={browseByFolderSlugs}
folderFieldName={config.folders.fieldName}
folderID={folderID}
subfolders={subfolders}
>

View File

@@ -8,9 +8,10 @@ import type {
import { DefaultCollectionFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { redirect } from 'next/navigation.js'
import { getFolderData, parseDocumentID } from 'payload'
import { getFolderData } from 'payload'
import { buildFolderWhereConstraints } from 'payload/shared'
import React from 'react'
import { getPreferences } from '../../utilities/getPreferences.js'
@@ -37,7 +38,6 @@ export const buildCollectionFolderView = async (
disableBulkDelete,
disableBulkEdit,
enableRowSelections,
folderCollectionSlugs,
folderID,
initPageResult,
isInDrawer,
@@ -69,12 +69,12 @@ export const buildCollectionFolderView = async (
if (collectionConfig) {
const query = queryFromArgs || queryFromReq
const collectionFolderPreferences = await getPreferences<{ viewPreference: string }>(
`${collectionSlug}-collection-folder`,
payload,
user.id,
user.collection,
)
const collectionFolderPreferences = await getPreferences<{
sort?: string
viewPreference: string
}>(`${collectionSlug}-collection-folder`, payload, user.id, user.collection)
const sortPreference = collectionFolderPreferences?.value.sort
const {
routes: { admin: adminRoute },
@@ -82,38 +82,45 @@ export const buildCollectionFolderView = async (
if (
(!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) ||
!folderCollectionSlugs.includes(collectionSlug)
!config.folders
) {
throw new Error('not-found')
}
const whereConstraints = [
mergeListSearchAndWhere({
collectionConfig,
search: typeof query?.search === 'string' ? query.search : undefined,
where: (query?.where as Where) || undefined,
}),
]
let folderWhere: undefined | Where
const folderCollectionConfig = payload.collections[config.folders.slug].config
const folderCollectionConstraints = await buildFolderWhereConstraints({
collectionConfig: folderCollectionConfig,
folderID,
localeCode: fullLocale?.code,
req: initPageResult.req,
search: typeof query?.search === 'string' ? query.search : undefined,
sort: sortPreference,
})
if (folderID) {
whereConstraints.push({
[config.folders.fieldName]: {
equals: parseDocumentID({ id: folderID, collectionSlug, payload }),
},
})
} else {
whereConstraints.push({
[config.folders.fieldName]: {
exists: false,
},
})
if (folderCollectionConstraints) {
folderWhere = folderCollectionConstraints
}
let documentWhere: undefined | Where
const collectionConstraints = await buildFolderWhereConstraints({
collectionConfig,
folderID,
localeCode: fullLocale?.code,
req: initPageResult.req,
search: typeof query?.search === 'string' ? query.search : undefined,
sort: sortPreference,
})
if (collectionConstraints) {
documentWhere = collectionConstraints
}
const { breadcrumbs, documents, subfolders } = await getFolderData({
collectionSlug,
documentWhere,
folderID,
folderWhere,
req: initPageResult.req,
search: query?.search as string,
})
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
@@ -175,7 +182,8 @@ export const buildCollectionFolderView = async (
breadcrumbs={breadcrumbs}
collectionSlug={collectionSlug}
documents={documents}
folderCollectionSlugs={folderCollectionSlugs}
folderCollectionSlugs={[collectionSlug]}
folderFieldName={config.folders.fieldName}
folderID={folderID}
search={search}
subfolders={subfolders}

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.create-first-user {

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.dashboard {

View File

@@ -2,6 +2,7 @@ import type {
BeforeDocumentControlsServerPropsOnly,
DefaultServerFunctionArgs,
DocumentSlots,
EditMenuItemsServerPropsOnly,
PayloadRequest,
PreviewButtonServerPropsOnly,
PublishButtonServerPropsOnly,
@@ -55,6 +56,16 @@ export const renderDocumentSlots: (args: {
})
}
const EditMenuItems = collectionConfig?.admin?.components?.edit?.editMenuItems
if (EditMenuItems) {
components.EditMenuItems = RenderServerComponent({
Component: EditMenuItems,
importMap: req.payload.importMap,
serverProps: serverProps satisfies EditMenuItemsServerPropsOnly,
})
}
const CustomPreviewButton =
collectionConfig?.admin?.components?.edit?.PreviewButton ||
globalConfig?.admin?.components?.elements?.PreviewButton

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.live-preview-window {

View File

@@ -1,4 +1,4 @@
@import '../../../../scss/styles.scss';
@import '~@payloadcms/ui/scss';
@layer payload-default {
.live-preview-toolbar-controls {

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