Compare commits
38 Commits
feat/live-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27cfe775dd | ||
|
|
24e436bfa8 | ||
|
|
bbcdea5450 | ||
|
|
794bf8299c | ||
|
|
9f0573d714 | ||
|
|
f288cf6a8f | ||
|
|
03f7102433 | ||
|
|
09e3174834 | ||
|
|
a4a0298435 | ||
|
|
7a8bcdf7ba | ||
|
|
6e203db33c | ||
|
|
0c44c3bdd9 | ||
|
|
008a52d8ec | ||
|
|
7e98fbf78e | ||
|
|
d109b44856 | ||
|
|
5146fc865f | ||
|
|
d9e183242c | ||
|
|
7794541af3 | ||
|
|
1a1696d9ae | ||
|
|
b8d7ccb4dc | ||
|
|
be47f65b7c | ||
|
|
0f6d748365 | ||
|
|
a11586811e | ||
|
|
9b109339ee | ||
|
|
4e972c3fe2 | ||
|
|
cfb70f06bb | ||
|
|
1c68ed5251 | ||
|
|
65b3845d92 | ||
|
|
917c66fe44 | ||
|
|
ac691b675b | ||
|
|
fdab2712c0 | ||
|
|
a231a05b7c | ||
|
|
b99c324f1e | ||
|
|
426f99ca99 | ||
|
|
4600c94cac | ||
|
|
c1b4960795 | ||
|
|
97c41ce0c5 | ||
|
|
e0ffada80b |
2
.github/actions/triage/action.yml
vendored
2
.github/actions/triage/action.yml
vendored
@@ -26,7 +26,7 @@ runs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Run action
|
||||
run: node ${{ github.action_path }}/dist/index.js
|
||||
shell: sh
|
||||
|
||||
4
.github/workflows/audit-dependencies.yml
vendored
4
.github/workflows/audit-dependencies.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
- name: Slack notification on failure
|
||||
if: failure()
|
||||
uses: slackapi/slack-github-action@v2.1.0
|
||||
uses: slackapi/slack-github-action@v2.1.1
|
||||
with:
|
||||
webhook: ${{ inputs.debug == 'true' && secrets.SLACK_TEST_WEBHOOK_URL || secrets.SLACK_WEBHOOK_URL }}
|
||||
webhook-type: incoming-webhook
|
||||
|
||||
2
.github/workflows/dispatch-event.yml
vendored
2
.github/workflows/dispatch-event.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Repository dispatch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Dispatch event
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
22
.github/workflows/main.yml
vendored
22
.github/workflows/main.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
env:
|
||||
SUITE_NAME: ${{ matrix.suite }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -447,7 +447,7 @@ jobs:
|
||||
env:
|
||||
SUITE_NAME: ${{ matrix.suite }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -550,7 +550,7 @@ jobs:
|
||||
MONGODB_VERSION: 6.0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -647,7 +647,7 @@ jobs:
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -706,7 +706,7 @@ jobs:
|
||||
actions: read # for fetching base branch bundle stats
|
||||
pull-requests: write # for comments
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
4
.github/workflows/post-release-templates.yml
vendored
4
.github/workflows/post-release-templates.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
sparse-checkout: .github/workflows
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
POSTGRES_DB: payloadtests
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
6
.github/workflows/post-release.yml
vendored
6
.github/workflows/post-release.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: ./.github/actions/release-commenter
|
||||
continue-on-error: true
|
||||
env:
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Github Releases To Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1.16.2
|
||||
uses: SethCohen/github-releases-to-discord@v1.19.0
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_RELEASES_WEBHOOK_URL }}
|
||||
color: '16777215'
|
||||
|
||||
2
.github/workflows/pr-title.yml
vendored
2
.github/workflows/pr-title.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
name: lint-pr-title
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/publish-prerelease.yml
vendored
2
.github/workflows/publish-prerelease.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
- name: Load npm token
|
||||
|
||||
2
.github/workflows/triage.yml
vendored
2
.github/workflows/triage.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
54
AGENTS.md
Normal file
54
AGENTS.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Payload Monorepo Agent Instructions
|
||||
|
||||
## Project Structure
|
||||
|
||||
- Packages are located in the `packages/` directory.
|
||||
- The main Payload package is `packages/payload`. This contains the core functionality.
|
||||
- Database adapters are in `packages/db-*`.
|
||||
- The UI package is `packages/ui`.
|
||||
- The Next.js integration is in `packages/next`.
|
||||
- Rich text editor packages are in `packages/richtext-*`.
|
||||
- Storage adapters are in `packages/storage-*`.
|
||||
- Email adapters are in `packages/email-*`.
|
||||
- Plugins which add additional functionality are in `packages/plugin-*`.
|
||||
- Documentation is in the `docs/` directory.
|
||||
- Monorepo tooling is in the `tools/` directory.
|
||||
- Test suites and configs are in the `test/` directory.
|
||||
- LLMS.txt is at URL: https://payloadcms.com/llms.txt
|
||||
- LLMS-FULL.txt is at URL: https://payloadcms.com/llms-full.txt
|
||||
|
||||
## Dev environment tips
|
||||
|
||||
- Any package can be built using a `pnpm build:*` script defined in the root `package.json`. These typically follow the format `pnpm build:<directory_name>`. The options are all of the top-level directories inside the `packages/` directory. Ex `pnpm build:db-mongodb` which builds the `packages/db-mongodb` package.
|
||||
- ALL packages can be built with `pnpm build:all`.
|
||||
- Use `pnpm dev` to start the monorepo dev server. This loads the default config located at `test/_community/config.ts`.
|
||||
- Specific dev configs for each package can be run with `pnpm dev <directory_name>`. The options are all of the top-level directories inside the `test/` directory. Ex `pnpm dev fields` which loads the `test/fields/config.ts` config. The directory name can either encompass a single area of functionality or be the name of a specific package.
|
||||
|
||||
## Testing instructions
|
||||
|
||||
- There are unit, integration, and e2e tests in the monorepo.
|
||||
- Unit tests can be run with `pnpm test:unit`.
|
||||
- Integration tests can be run with `pnpm test:int`. Individual test suites can be run with `pnpm test:int <directory_name>`, which will point at `test/<directory_name>/int.spec.ts`.
|
||||
- E2E tests can be run with `pnpm test:e2e`.
|
||||
- All tests can be run with `pnpm test`.
|
||||
- Prefer running `pnpm test:int` for verifying local code changes.
|
||||
|
||||
## PR Guidelines
|
||||
|
||||
- This repository follows conventional commits for PR titles
|
||||
- PR Title format: <type>(<scope>): <title>. Title must start with a lowercase letter.
|
||||
- Valid types are build, chore, ci, docs, examples, feat, fix, perf, refactor, revert, style, templates, test
|
||||
- Prefer `feat` for new features and `fix` for bug fixes.
|
||||
- Valid scopes are the following regex patterns: cpa, db-\*, db-mongodb, db-postgres, db-vercel-postgres, db-sqlite, drizzle, email-\*, email-nodemailer, email-resend, eslint, graphql, live-preview, live-preview-react, next, payload-cloud, plugin-cloud, plugin-cloud-storage, plugin-form-builder, plugin-import-export, plugin-multi-tenant, plugin-nested-docs, plugin-redirects, plugin-search, plugin-sentry, plugin-seo, plugin-stripe, richtext-\*, richtext-lexical, richtext-slate, storage-\*, storage-azure, storage-gcs, storage-uploadthing, storage-vercel-blob, storage-s3, translations, ui, templates, examples(\/(\w|-)+)?, deps
|
||||
- Scopes should be chosen based upon the package(s) being modified. If multiple packages are being modified, choose the most relevant one or no scope at all.
|
||||
- Example PR titles:
|
||||
- `feat(db-mongodb): add support for transactions`
|
||||
- `feat(richtext-lexical): add options to hide block handles`
|
||||
- `fix(ui): json field type ignoring editorOptions`
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
- This repository follows conventional commits for commit messages
|
||||
- The first commit of a branch should follow the PR title format: <type>(<scope>): <title>. Follow the same rules as PR titles.
|
||||
- Subsequent commits should prefer `chore` commits without a scope unless a specific package is being modified.
|
||||
- These will eventually be squashed into the first commit when merging the PR.
|
||||
@@ -98,6 +98,7 @@ The following options are available:
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| `autoRefresh` | Used to automatically refresh user tokens for users logged into the dashboard. [More details](../authentication/overview). |
|
||||
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
|
||||
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
|
||||
@@ -49,7 +49,7 @@ export const Users: CollectionConfig = {
|
||||
strategies: [
|
||||
{
|
||||
name: 'custom-strategy',
|
||||
authenticate: ({ payload, headers }) => {
|
||||
authenticate: async ({ payload, headers }) => {
|
||||
const usersQuery = await payload.find({
|
||||
collection: 'users',
|
||||
where: {
|
||||
@@ -66,7 +66,7 @@ export const Users: CollectionConfig = {
|
||||
// Send the user with the collection slug back to authenticate,
|
||||
// or send null if no user should be authenticated
|
||||
user: usersQuery.docs[0] ? {
|
||||
collection: 'users'
|
||||
collection: 'users',
|
||||
...usersQuery.docs[0],
|
||||
} : null,
|
||||
|
||||
|
||||
@@ -173,6 +173,25 @@ The following options are available:
|
||||
| **`password`** | The password of the user to login as. This is only needed if `prefillOnly` is set to true |
|
||||
| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. |
|
||||
|
||||
## Auto-Refresh
|
||||
|
||||
Turning this property on will allow users to stay logged in indefinitely while their browser is open and on the admin panel, by automatically refreshing their authentication token before it expires.
|
||||
|
||||
To enable auto-refresh for user tokens, set `autoRefresh: true` in the [Payload Config](../admin/overview#admin-options) to:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
admin: {
|
||||
autoRefresh: true,
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
## Operations
|
||||
|
||||
All auth-related operations are available via Payload's REST, Local, and GraphQL APIs. These operations are automatically added to your Collection when you enable Authentication. [More details](./operations).
|
||||
|
||||
@@ -131,6 +131,29 @@ localization: {
|
||||
|
||||
Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.
|
||||
|
||||
## Experimental Options
|
||||
|
||||
Experimental options are features that may not be fully stable and may change or be removed in future releases.
|
||||
|
||||
These options can be enabled in your Payload Config under the `experimental` key. You can set them like this:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
experimental: {
|
||||
localizeStatus: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The following experimental options are available related to localization:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
|
||||
|
||||
## Field Localization
|
||||
|
||||
Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.
|
||||
|
||||
@@ -70,6 +70,7 @@ The following options are available:
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`experimental`** | Configure experimental features for Payload. These may be unstable and may change or be removed in future releases. [More details](../experimental). |
|
||||
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
|
||||
66
docs/experimental/overview.mdx
Normal file
66
docs/experimental/overview.mdx
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Experimental Features
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Enable and configure experimental functionality within Payload. These featuresmay be unstable and may change or be removed without notice.
|
||||
keywords: experimental, unstable, beta, preview, features, configuration, Payload, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Experimental features allow you to try out new functionality before it becomes a stable part of Payload. These features may still be in active development, may have incomplete functionality, and can change or be removed in future releases without warning.
|
||||
|
||||
## How It Works
|
||||
|
||||
Experimental features are configured via the root-level `experimental` property in your [Payload Config](../configuration/overview). This property contains individual feature flags, each flag can be configured independently, allowing you to selectively opt into specific functionality.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
experimental: {
|
||||
localizeStatus: true, // highlight-line
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Experimental Options
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
|
||||
|
||||
This list may change without notice.
|
||||
|
||||
## When to Use Experimental Features
|
||||
|
||||
You might enable an experimental feature when:
|
||||
|
||||
- You want early access to new capabilities before their stable release.
|
||||
- You can accept the risks of using potentially unstable functionality.
|
||||
- You are testing new features in a development or staging environment.
|
||||
- You wish to provide feedback to the Payload team on new functionality.
|
||||
|
||||
If you are working on a production application, carefully evaluate whether the benefits outweigh the risks. For most stable applications, it is recommended to wait until the feature is officially released.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong> To stay up to date on experimental features or share
|
||||
your feedback, visit the{' '}
|
||||
<a
|
||||
href="https://github.com/payloadcms/payload/discussions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Payload GitHub Discussions
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a
|
||||
href="https://github.com/payloadcms/payload/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
open an issue
|
||||
</a>
|
||||
.
|
||||
</Banner>
|
||||
@@ -79,6 +79,7 @@ formBuilderPlugin({
|
||||
text: true,
|
||||
textarea: true,
|
||||
select: true,
|
||||
radio: true,
|
||||
email: true,
|
||||
state: true,
|
||||
country: true,
|
||||
@@ -293,14 +294,46 @@ Maps to a `textarea` input on your front-end. Used to collect a multi-line strin
|
||||
|
||||
Maps to a `select` input on your front-end. Used to display a list of options.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | -------- | -------------------------------------------------------- |
|
||||
| `name` | string | The name of the field. |
|
||||
| `label` | string | The label of the field. |
|
||||
| `defaultValue` | string | The default value of the field. |
|
||||
| `width` | string | The width of the field on the front-end. |
|
||||
| `required` | checkbox | Whether or not the field is required when submitted. |
|
||||
| `options` | array | An array of objects with `label` and `value` properties. |
|
||||
| Property | Type | Description |
|
||||
| -------------- | -------- | ------------------------------------------------------------------------------- |
|
||||
| `name` | string | The name of the field. |
|
||||
| `label` | string | The label of the field. |
|
||||
| `defaultValue` | string | The default value of the field. |
|
||||
| `placeholder` | string | The placeholder text for the field. |
|
||||
| `width` | string | The width of the field on the front-end. |
|
||||
| `required` | checkbox | Whether or not the field is required when submitted. |
|
||||
| `options` | array | An array of objects that define the select options. See below for more details. |
|
||||
|
||||
#### Select Options
|
||||
|
||||
Each option in the `options` array defines a selectable choice for the select field.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | ------ | ----------------------------------- |
|
||||
| `label` | string | The display text for the option. |
|
||||
| `value` | string | The value submitted for the option. |
|
||||
|
||||
### Radio
|
||||
|
||||
Maps to radio button inputs on your front-end. Used to allow users to select a single option from a list of choices.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | -------- | ------------------------------------------------------------------------------ |
|
||||
| `name` | string | The name of the field. |
|
||||
| `label` | string | The label of the field. |
|
||||
| `defaultValue` | string | The default value of the field. |
|
||||
| `width` | string | The width of the field on the front-end. |
|
||||
| `required` | checkbox | Whether or not the field is required when submitted. |
|
||||
| `options` | array | An array of objects that define the radio options. See below for more details. |
|
||||
|
||||
#### Radio Options
|
||||
|
||||
Each option in the `options` array defines a selectable choice for the radio field.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | ------ | ----------------------------------- |
|
||||
| `label` | string | The display text for the option. |
|
||||
| `value` | string | The value submitted for the option. |
|
||||
|
||||
### Email (field)
|
||||
|
||||
|
||||
@@ -773,3 +773,28 @@ const res = await fetch(`${api}/${collectionSlug}?depth=1&locale=en`, {
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Passing as JSON
|
||||
|
||||
When using `X-Payload-HTTP-Method-Override`, it expects the body to be a query string. If you want to pass JSON instead, you can set the `Content-Type` to `application/json` and include the JSON body in the request.
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
const res = await fetch(`${api}/${collectionSlug}/${id}`, {
|
||||
// Only the findByID endpoint supports HTTP method overrides with JSON data
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Payload-HTTP-Method-Override': 'GET',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
depth: 1,
|
||||
locale: 'en',
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
This can be more efficient for large JSON payloads, as you avoid converting data to and from query strings. However, only certain endpoints support this. Supported endpoints will read the parsed body under a `data` property, instead of reading from query parameters as with standard GET requests.
|
||||
|
||||
@@ -11,7 +11,7 @@ keywords: lexical, richtext, html
|
||||
There are two main approaches to convert your Lexical-based rich text to HTML:
|
||||
|
||||
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
|
||||
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
|
||||
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API.
|
||||
|
||||
### On-demand
|
||||
|
||||
@@ -101,10 +101,7 @@ export const MyRSCComponent = async ({
|
||||
|
||||
### HTML field
|
||||
|
||||
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended for two reasons:
|
||||
|
||||
1. It creates a column with duplicate content in another format.
|
||||
2. In [client-side live preview](/docs/live-preview/client), it makes it not "live".
|
||||
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended, as it creates a column with duplicate content in another format.
|
||||
|
||||
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.
|
||||
|
||||
|
||||
@@ -269,11 +269,13 @@ Lexical does not generate accurate type definitions for your richText fields for
|
||||
|
||||
The Rich Text Field editor configuration has an `admin` property with the following options:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------- |
|
||||
| **`placeholder`** | Set this property to define a placeholder string for the field. |
|
||||
| **`hideGutter`** | Set this property to `true` to hide this field's gutter within the Admin Panel. |
|
||||
| **`hideInsertParagraphAtEnd`** | Set this property to `true` to hide the "+" button that appears at the end of the editor |
|
||||
| Property | Description |
|
||||
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| **`placeholder`** | Set this property to define a placeholder string for the field. |
|
||||
| **`hideGutter`** | Set this property to `true` to hide this field's gutter within the Admin Panel. |
|
||||
| **`hideInsertParagraphAtEnd`** | Set this property to `true` to hide the "+" button that appears at the end of the editor. |
|
||||
| **`hideDraggableBlockElement`** | Set this property to `true` to hide the draggable element that appears when you hover a node in the editor. |
|
||||
| **`hideAddBlockButton`** | Set this property to `true` to hide the "+" button that appears when you hover a node in the editor. |
|
||||
|
||||
### Disable the gutter
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -12,6 +12,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
autosave,
|
||||
createdAt,
|
||||
globalSlug,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
@@ -33,6 +34,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
autosave,
|
||||
collectionSlug,
|
||||
createdAt,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
@@ -37,6 +38,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
|
||||
@@ -179,6 +179,13 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
|
||||
for (let i = 0; i < result.docs.length; i++) {
|
||||
const id = result.docs[i].parent
|
||||
|
||||
const localeStatus = result.docs[i].localeStatus || {}
|
||||
if (locale && localeStatus[locale]) {
|
||||
result.docs[i].status = localeStatus[locale]
|
||||
result.docs[i].version._status = localeStatus[locale]
|
||||
}
|
||||
|
||||
result.docs[i] = result.docs[i].version ?? {}
|
||||
result.docs[i].id = id
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { MongooseUpdateQueryOptions, UpdateQuery } from 'mongoose'
|
||||
import type { Job, UpdateJobs, Where } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
@@ -14,9 +14,13 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
this: MongooseAdapter,
|
||||
{ id, data, limit, req, returning, sort: sortArg, where: whereArg },
|
||||
) {
|
||||
if (!(data?.log as object[])?.length) {
|
||||
if (
|
||||
!(data?.log as object[])?.length &&
|
||||
!(data.log && typeof data.log === 'object' && '$push' in data.log)
|
||||
) {
|
||||
delete data.log
|
||||
}
|
||||
|
||||
const where = id ? { id: { equals: id } } : (whereArg as Where)
|
||||
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
@@ -47,17 +51,44 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
where,
|
||||
})
|
||||
|
||||
transform({ adapter: this, data, fields: collectionConfig.fields, operation: 'write' })
|
||||
let updateData: UpdateQuery<any> = data
|
||||
|
||||
const $inc: Record<string, number> = {}
|
||||
const $push: Record<string, { $each: any[] } | any> = {}
|
||||
|
||||
transform({
|
||||
$inc,
|
||||
$push,
|
||||
adapter: this,
|
||||
data,
|
||||
fields: collectionConfig.fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
const updateOps: UpdateQuery<any> = {}
|
||||
|
||||
if (Object.keys($inc).length) {
|
||||
updateOps.$inc = $inc
|
||||
}
|
||||
if (Object.keys($push).length) {
|
||||
updateOps.$push = $push
|
||||
}
|
||||
if (Object.keys(updateOps).length) {
|
||||
updateOps.$set = updateData
|
||||
updateData = updateOps
|
||||
}
|
||||
|
||||
let result: Job[] = []
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
if (returning === false) {
|
||||
await Model.updateOne(query, data, options)
|
||||
await Model.updateOne(query, updateData, options)
|
||||
transform({ adapter: this, data, fields: collectionConfig.fields, operation: 'read' })
|
||||
|
||||
return null
|
||||
} else {
|
||||
const doc = await Model.findOneAndUpdate(query, data, options)
|
||||
const doc = await Model.findOneAndUpdate(query, updateData, options)
|
||||
result = doc ? [doc] : []
|
||||
}
|
||||
} else {
|
||||
@@ -74,7 +105,7 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
query = { _id: { $in: documentsToUpdate.map((doc) => doc._id) } }
|
||||
}
|
||||
|
||||
await Model.updateMany(query, data, options)
|
||||
await Model.updateMany(query, updateData, options)
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -15,6 +15,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
createdAt,
|
||||
globalSlug,
|
||||
localeStatus,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
@@ -35,6 +36,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
|
||||
@@ -15,6 +15,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
collectionSlug,
|
||||
createdAt,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
@@ -40,6 +41,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
|
||||
@@ -110,19 +110,32 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && operator !== 'exists') {
|
||||
if (typeof val === 'string') {
|
||||
if (val === 'null' || val === '') {
|
||||
formattedValue = null
|
||||
} else {
|
||||
const date = new Date(val)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return { operator, value: undefined }
|
||||
}
|
||||
formattedValue = date.toISOString()
|
||||
// Helper function to convert a single date value to ISO string
|
||||
const convertDateToISO = (item: unknown): unknown => {
|
||||
if (typeof item === 'string') {
|
||||
if (item === 'null' || item === '') {
|
||||
return null
|
||||
}
|
||||
} else if (typeof val === 'number') {
|
||||
formattedValue = new Date(val).toISOString()
|
||||
const date = new Date(item)
|
||||
return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
|
||||
} else if (typeof item === 'number') {
|
||||
return new Date(item).toISOString()
|
||||
} else if (item instanceof Date) {
|
||||
return item.toISOString()
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
if (field.type === 'date' && operator !== 'exists') {
|
||||
if (Array.isArray(formattedValue)) {
|
||||
// Handle arrays of dates for 'in' and 'not_in' operators
|
||||
formattedValue = formattedValue.map(convertDateToISO).filter((item) => item !== undefined)
|
||||
} else {
|
||||
const converted = convertDateToISO(val)
|
||||
if (converted === undefined) {
|
||||
return { operator, value: undefined }
|
||||
}
|
||||
formattedValue = converted
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,15 +36,17 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
where: combinedWhere,
|
||||
})
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs.map((doc) => {
|
||||
doc = {
|
||||
id: doc.parent,
|
||||
...doc.version,
|
||||
}
|
||||
for (let i = 0; i < result.docs.length; i++) {
|
||||
const id = result.docs[i].parent
|
||||
const localeStatus = result.docs[i].localeStatus || {}
|
||||
if (locale && localeStatus[locale]) {
|
||||
result.docs[i].status = localeStatus[locale]
|
||||
result.docs[i].version._status = localeStatus[locale]
|
||||
}
|
||||
|
||||
return doc
|
||||
}),
|
||||
result.docs[i] = result.docs[i].version ?? {}
|
||||
result.docs[i].id = id
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -13,9 +13,13 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
this: DrizzleAdapter,
|
||||
{ id, data, limit: limitArg, req, returning, sort: sortArg, where: whereArg },
|
||||
) {
|
||||
if (!(data?.log as object[])?.length) {
|
||||
if (
|
||||
!(data?.log as object[])?.length &&
|
||||
!(data.log && typeof data.log === 'object' && '$push' in data.log)
|
||||
) {
|
||||
delete data.log
|
||||
}
|
||||
|
||||
const whereToUse: Where = id ? { id: { equals: id } } : whereArg
|
||||
const limit = id ? 1 : limitArg
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import type { FieldSchemaJSON } from 'payload'
|
||||
|
||||
import type { CollectionPopulationRequestHandler, LivePreviewMessageEvent } from './types.js'
|
||||
|
||||
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
|
||||
import { mergeData } from './mergeData.js'
|
||||
|
||||
const _payloadLivePreview: {
|
||||
fieldSchema: FieldSchemaJSON | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
previousData: any
|
||||
} = {
|
||||
/**
|
||||
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
|
||||
* We need to cache this value so that it can be used across subsequent messages
|
||||
* To do this, save `fieldSchemaJSON` when it arrives as a global variable
|
||||
* Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
|
||||
*/
|
||||
fieldSchema: undefined,
|
||||
/**
|
||||
* Each time the data is merged, cache the result as a `previousData` variable
|
||||
* This will ensure changes compound overtop of each other
|
||||
@@ -35,26 +25,13 @@ export const handleMessage = async <T extends Record<string, any>>(args: {
|
||||
const { apiRoute, depth, event, initialData, requestHandler, serverURL } = args
|
||||
|
||||
if (isLivePreviewEvent(event, serverURL)) {
|
||||
const { data, externallyUpdatedRelationship, fieldSchemaJSON, locale } = event.data
|
||||
|
||||
if (!_payloadLivePreview?.fieldSchema && fieldSchemaJSON) {
|
||||
_payloadLivePreview.fieldSchema = fieldSchemaJSON
|
||||
}
|
||||
|
||||
if (!_payloadLivePreview?.fieldSchema) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Payload Live Preview: No `fieldSchemaJSON` was received from the parent window. Unable to merge data.',
|
||||
)
|
||||
|
||||
return initialData
|
||||
}
|
||||
const { collectionSlug, data, globalSlug, locale } = event.data
|
||||
|
||||
const mergedData = await mergeData<T>({
|
||||
apiRoute,
|
||||
collectionSlug,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: _payloadLivePreview.fieldSchema,
|
||||
globalSlug,
|
||||
incomingData: data,
|
||||
initialData: _payloadLivePreview?.previousData || initialData,
|
||||
locale,
|
||||
|
||||
@@ -4,6 +4,5 @@ export { isLivePreviewEvent } from './isLivePreviewEvent.js'
|
||||
export { mergeData } from './mergeData.js'
|
||||
export { ready } from './ready.js'
|
||||
export { subscribe } from './subscribe.js'
|
||||
export { traverseRichText } from './traverseRichText.js'
|
||||
export type { LivePreviewMessageEvent } from './types.js'
|
||||
export { unsubscribe } from './unsubscribe.js'
|
||||
|
||||
@@ -1,115 +1,60 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON, PaginatedDocs } from 'payload'
|
||||
import type { CollectionPopulationRequestHandler } from './types.js'
|
||||
|
||||
import type { CollectionPopulationRequestHandler, PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const defaultRequestHandler = ({
|
||||
const defaultRequestHandler: CollectionPopulationRequestHandler = ({
|
||||
apiPath,
|
||||
data,
|
||||
endpoint,
|
||||
serverURL,
|
||||
}: {
|
||||
apiPath: string
|
||||
endpoint: string
|
||||
serverURL: string
|
||||
}) => {
|
||||
const url = `${serverURL}${apiPath}/${endpoint}`
|
||||
|
||||
return fetch(url, {
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Payload-HTTP-Method-Override': 'GET',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// Relationships are only updated when their `id` or `relationTo` changes, by comparing the old and new values
|
||||
// This needs to also happen when locale changes, except this is not not part of the API response
|
||||
// Instead, we keep track of the old locale ourselves and trigger a re-population when it changes
|
||||
let prevLocale: string | undefined
|
||||
|
||||
export const mergeData = async <T extends Record<string, any>>(args: {
|
||||
apiRoute?: string
|
||||
/**
|
||||
* @deprecated Use `requestHandler` instead
|
||||
*/
|
||||
collectionPopulationRequestHandler?: CollectionPopulationRequestHandler
|
||||
collectionSlug?: string
|
||||
depth?: number
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: FieldSchemaJSON
|
||||
globalSlug?: string
|
||||
incomingData: Partial<T>
|
||||
initialData: T
|
||||
locale?: string
|
||||
requestHandler?: CollectionPopulationRequestHandler
|
||||
returnNumberOfRequests?: boolean
|
||||
serverURL: string
|
||||
}): Promise<
|
||||
{
|
||||
_numberOfRequests?: number
|
||||
} & T
|
||||
> => {
|
||||
}): Promise<T> => {
|
||||
const {
|
||||
apiRoute,
|
||||
collectionSlug,
|
||||
depth,
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema,
|
||||
globalSlug,
|
||||
incomingData,
|
||||
initialData,
|
||||
locale,
|
||||
returnNumberOfRequests,
|
||||
serverURL,
|
||||
} = args
|
||||
|
||||
const result = { ...initialData }
|
||||
const requestHandler = args.requestHandler || defaultRequestHandler
|
||||
|
||||
const populationsByCollection: PopulationsByCollection = {}
|
||||
const result = await requestHandler({
|
||||
apiPath: apiRoute || '/api',
|
||||
data: {
|
||||
data: incomingData,
|
||||
depth,
|
||||
locale,
|
||||
},
|
||||
endpoint: encodeURI(
|
||||
`${globalSlug ? 'globals/' : ''}${collectionSlug ?? globalSlug}${collectionSlug ? `/${initialData.id}` : ''}`,
|
||||
),
|
||||
serverURL,
|
||||
}).then((res) => res.json())
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
localeChanged: prevLocale !== locale,
|
||||
populationsByCollection,
|
||||
result,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(populationsByCollection).map(async ([collection, populations]) => {
|
||||
let res: PaginatedDocs
|
||||
|
||||
const ids = new Set(populations.map(({ id }) => id))
|
||||
const requestHandler =
|
||||
args.collectionPopulationRequestHandler || args.requestHandler || defaultRequestHandler
|
||||
|
||||
try {
|
||||
res = await requestHandler({
|
||||
apiPath: apiRoute || '/api',
|
||||
endpoint: encodeURI(
|
||||
`${collection}?depth=${depth}&limit=${ids.size}&where[id][in]=${Array.from(ids).join(',')}${locale ? `&locale=${locale}` : ''}`,
|
||||
),
|
||||
serverURL,
|
||||
}).then((res) => res.json())
|
||||
|
||||
if (res?.docs?.length > 0) {
|
||||
res.docs.forEach((doc) => {
|
||||
populationsByCollection[collection]?.forEach((population) => {
|
||||
if (population.id === doc.id) {
|
||||
population.ref[population.accessor] = doc
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err) // eslint-disable-line no-console
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
prevLocale = locale
|
||||
|
||||
return {
|
||||
...result,
|
||||
...(returnNumberOfRequests
|
||||
? { _numberOfRequests: Object.keys(populationsByCollection).length }
|
||||
: {}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
|
||||
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseRichText } from './traverseRichText.js'
|
||||
|
||||
export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: FieldSchemaJSON
|
||||
incomingData: T
|
||||
localeChanged: boolean
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: Record<string, any>
|
||||
}): void => {
|
||||
const {
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchemas,
|
||||
incomingData,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result,
|
||||
} = args
|
||||
|
||||
fieldSchemas.forEach((fieldSchema) => {
|
||||
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
|
||||
const fieldName = fieldSchema.name
|
||||
|
||||
switch (fieldSchema.type) {
|
||||
case 'array':
|
||||
if (
|
||||
!incomingData[fieldName] &&
|
||||
incomingData[fieldName] !== undefined &&
|
||||
result?.[fieldName] !== undefined
|
||||
) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
incomingData: incomingRow,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result: result[fieldName][i],
|
||||
})
|
||||
|
||||
return result[fieldName][i]
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
|
||||
const incomingBlockJSON = fieldSchema.blocks?.[incomingBlock.blockType]
|
||||
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
if (
|
||||
!result[fieldName][i] ||
|
||||
result[fieldName][i].id !== incomingBlock.id ||
|
||||
result[fieldName][i].blockType !== incomingBlock.blockType
|
||||
) {
|
||||
result[fieldName][i] = {
|
||||
blockType: incomingBlock.blockType,
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: incomingBlockJSON!.fields!,
|
||||
incomingData: incomingBlock,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result: result[fieldName][i],
|
||||
})
|
||||
|
||||
return result[fieldName][i]
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'group':
|
||||
// falls through
|
||||
case 'tabs':
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = {}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
result: result[fieldName],
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'relationship':
|
||||
// falls through
|
||||
case 'upload':
|
||||
// Handle `hasMany` relationships
|
||||
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||
if (!result[fieldName] || !incomingData[fieldName].length) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
incomingData[fieldName].forEach((incomingRelation, i) => {
|
||||
// Handle `hasMany` polymorphic
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {
|
||||
relationTo: incomingRelation.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const oldID = result[fieldName][i]?.value?.id
|
||||
const oldRelation = result[fieldName][i]?.relationTo
|
||||
const newID = incomingRelation.value
|
||||
const newRelation = incomingRelation.relationTo
|
||||
|
||||
const hasChanged = newID !== oldID || newRelation !== oldRelation
|
||||
|
||||
const hasUpdated =
|
||||
newRelation === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: incomingRelation.value,
|
||||
accessor: 'value',
|
||||
ref: result[fieldName][i],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Handle `hasMany` monomorphic
|
||||
const hasChanged = incomingRelation !== result[fieldName][i]?.id
|
||||
|
||||
const hasUpdated =
|
||||
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
|
||||
incomingRelation === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
ref: result[fieldName],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Handle `hasOne` polymorphic
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = {
|
||||
relationTo: incomingData[fieldName]?.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const hasNewValue =
|
||||
incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName] !== null
|
||||
|
||||
const hasOldValue =
|
||||
result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName] !== null
|
||||
|
||||
const newID = hasNewValue
|
||||
? typeof incomingData[fieldName].value === 'object'
|
||||
? incomingData[fieldName].value.id
|
||||
: incomingData[fieldName].value
|
||||
: ''
|
||||
|
||||
const oldID = hasOldValue
|
||||
? typeof result[fieldName].value === 'object'
|
||||
? result[fieldName].value.id
|
||||
: result[fieldName].value
|
||||
: ''
|
||||
|
||||
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
|
||||
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
|
||||
|
||||
const hasChanged = newID !== oldID || newRelation !== oldRelation
|
||||
|
||||
const hasUpdated =
|
||||
newRelation === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
// if the new value/relation is different from the old value/relation
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: newID,
|
||||
accessor: 'value',
|
||||
ref: result[fieldName],
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle `hasOne` monomorphic
|
||||
const newID: number | string | undefined =
|
||||
(incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName].id) ||
|
||||
incomingData[fieldName]
|
||||
|
||||
const oldID: number | string | undefined =
|
||||
(result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName].id) ||
|
||||
result[fieldName]
|
||||
|
||||
const hasChanged = newID !== oldID
|
||||
|
||||
const hasUpdated =
|
||||
fieldSchema.relationTo === externallyUpdatedRelationship?.entitySlug &&
|
||||
newID === externallyUpdatedRelationship?.id
|
||||
|
||||
// if the new value is different from the old value
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
ref: result as Record<string, unknown>,
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
case 'richText':
|
||||
result[fieldName] = traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: incomingData[fieldName],
|
||||
populationsByCollection,
|
||||
result: result[fieldName],
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
result[fieldName] = incomingData[fieldName]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import type { DocumentEvent } from 'payload'
|
||||
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
export const traverseRichText = ({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData,
|
||||
populationsByCollection,
|
||||
result,
|
||||
}: {
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
incomingData: any
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: any
|
||||
}): any => {
|
||||
if (Array.isArray(incomingData)) {
|
||||
if (!result) {
|
||||
result = []
|
||||
}
|
||||
|
||||
result = incomingData.map((item, index) => {
|
||||
if (!result[index]) {
|
||||
result[index] = item
|
||||
}
|
||||
|
||||
return traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: item,
|
||||
populationsByCollection,
|
||||
result: result[index],
|
||||
})
|
||||
})
|
||||
} else if (incomingData && typeof incomingData === 'object') {
|
||||
if (!result) {
|
||||
result = {}
|
||||
}
|
||||
|
||||
// Remove keys from `result` that do not appear in `incomingData`
|
||||
// There's likely another way to do this,
|
||||
// But recursion and references make this very difficult
|
||||
Object.keys(result).forEach((key) => {
|
||||
if (!(key in incomingData)) {
|
||||
delete result[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Iterate over the keys of `incomingData` and populate `result`
|
||||
Object.keys(incomingData).forEach((key) => {
|
||||
if (!result[key]) {
|
||||
// Instantiate the key in `result` if it doesn't exist
|
||||
// Ensure its type matches the type of the `incomingData`
|
||||
// We don't have a schema to check against here
|
||||
result[key] =
|
||||
incomingData[key] && typeof incomingData[key] === 'object'
|
||||
? Array.isArray(incomingData[key])
|
||||
? []
|
||||
: {}
|
||||
: undefined
|
||||
}
|
||||
|
||||
const isRelationship = key === 'value' && 'relationTo' in incomingData
|
||||
|
||||
if (isRelationship) {
|
||||
// or if there are no keys besides id
|
||||
const needsPopulation =
|
||||
!result.value ||
|
||||
typeof result.value !== 'object' ||
|
||||
(typeof result.value === 'object' &&
|
||||
Object.keys(result.value).length === 1 &&
|
||||
'id' in result.value)
|
||||
|
||||
const hasChanged =
|
||||
result &&
|
||||
typeof result === 'object' &&
|
||||
result.value.id === externallyUpdatedRelationship?.id
|
||||
|
||||
if (needsPopulation || hasChanged) {
|
||||
if (!populationsByCollection[incomingData.relationTo]) {
|
||||
populationsByCollection[incomingData.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[incomingData.relationTo]?.push({
|
||||
id:
|
||||
incomingData[key] && typeof incomingData[key] === 'object'
|
||||
? incomingData[key].id
|
||||
: incomingData[key],
|
||||
accessor: 'value',
|
||||
ref: result,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
result[key] = traverseRichText({
|
||||
externallyUpdatedRelationship,
|
||||
incomingData: incomingData[key],
|
||||
populationsByCollection,
|
||||
result: result[key],
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
result = incomingData
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
|
||||
import type { DocumentEvent } from 'payload'
|
||||
|
||||
export type CollectionPopulationRequestHandler = ({
|
||||
apiPath,
|
||||
data,
|
||||
endpoint,
|
||||
serverURL,
|
||||
}: {
|
||||
apiPath: string
|
||||
data: Record<string, any>
|
||||
endpoint: string
|
||||
serverURL: string
|
||||
}) => Promise<Response>
|
||||
@@ -14,18 +16,11 @@ export type LivePreviewArgs = {}
|
||||
|
||||
export type LivePreview = void
|
||||
|
||||
export type PopulationsByCollection = {
|
||||
[slug: string]: Array<{
|
||||
accessor: number | string
|
||||
id: number | string
|
||||
ref: Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
|
||||
export type LivePreviewMessageEvent<T> = MessageEvent<{
|
||||
collectionSlug?: string
|
||||
data: T
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchemaJSON: FieldSchemaJSON
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
type: 'payload-live-preview'
|
||||
}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -17,6 +17,11 @@ export async function refresh({ config }: { config: any }) {
|
||||
throw new Error('Cannot refresh token: user not authenticated')
|
||||
}
|
||||
|
||||
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
|
||||
if (!existingCookie) {
|
||||
return { message: 'No valid token found to refresh', success: false }
|
||||
}
|
||||
|
||||
const collection: CollectionSlug | undefined = result.user.collection
|
||||
const collectionConfig = payload.collections[collection]
|
||||
|
||||
@@ -35,15 +40,10 @@ export async function refresh({ config }: { config: any }) {
|
||||
return { message: 'Token refresh failed', success: false }
|
||||
}
|
||||
|
||||
const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
|
||||
if (!existingCookie) {
|
||||
return { message: 'No valid token found to refresh', success: false }
|
||||
}
|
||||
|
||||
await setPayloadAuthCookie({
|
||||
authConfig: collectionConfig.config.auth,
|
||||
cookiePrefix: payload.config.cookiePrefix,
|
||||
token: existingCookie.value,
|
||||
token: refreshResult.refreshedToken,
|
||||
})
|
||||
|
||||
return { message: 'Token refreshed successfully', success: true }
|
||||
|
||||
@@ -178,7 +178,9 @@ export const buildCollectionFolderView = async (
|
||||
permissions?.collections?.[config.folders.slug]?.create
|
||||
? config.folders.slug
|
||||
: null,
|
||||
permissions?.collections?.[collectionSlug]?.create ? collectionSlug : null,
|
||||
resolvedFolderID && permissions?.collections?.[collectionSlug]?.create
|
||||
? collectionSlug
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
baseFolderPath: `/collections/${collectionSlug}/${config.folders.slug}`,
|
||||
breadcrumbs,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { AdminViewServerProps } from 'payload'
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
SanitizedDocumentPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
} from 'payload'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import React from 'react'
|
||||
|
||||
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { CreateFirstUserClient } from './index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -43,18 +46,27 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
// Get permissions
|
||||
const { docPermissions } = await getDocumentPermissions({
|
||||
collectionConfig,
|
||||
data,
|
||||
req,
|
||||
})
|
||||
const baseFields: SanitizedFieldsPermissions = Object.fromEntries(
|
||||
collectionConfig.fields
|
||||
.filter((f): f is { name: string } & typeof f => 'name' in f && typeof f.name === 'string')
|
||||
.map((f) => [f.name, { create: true, read: true, update: true }]),
|
||||
)
|
||||
|
||||
// In create-first-user we should always allow all fields
|
||||
const docPermissionsForForm: SanitizedDocumentPermissions = {
|
||||
create: true,
|
||||
delete: true,
|
||||
fields: baseFields,
|
||||
read: true,
|
||||
readVersions: true,
|
||||
update: true,
|
||||
}
|
||||
|
||||
// Build initial form state from data
|
||||
const { state: formState } = await buildFormState({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPermissions: docPermissionsForForm,
|
||||
docPreferences,
|
||||
locale: locale?.code,
|
||||
operation: 'create',
|
||||
@@ -69,7 +81,7 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
|
||||
<h1>{req.t('general:welcome')}</h1>
|
||||
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
||||
<CreateFirstUserClient
|
||||
docPermissions={docPermissions}
|
||||
docPermissions={docPermissionsForForm}
|
||||
docPreferences={docPreferences}
|
||||
initialState={formState}
|
||||
loginWithUsername={loginWithUsername}
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useConfig, useLocale, useStepNav, useTranslation } from '@payloadcms/ui'
|
||||
import { fieldAffectsData, formatAdminURL } from 'payload/shared'
|
||||
import { useConfig, useDocumentTitle, useLocale, useStepNav, useTranslation } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const SetStepNav: React.FC<{
|
||||
@@ -15,7 +15,6 @@ export const SetStepNav: React.FC<{
|
||||
readonly isTrashed?: boolean
|
||||
versionToCreatedAtFormatted?: string
|
||||
versionToID?: string
|
||||
versionToUseAsTitle?: Record<string, string> | string
|
||||
}> = ({
|
||||
id,
|
||||
collectionConfig,
|
||||
@@ -23,12 +22,12 @@ export const SetStepNav: React.FC<{
|
||||
isTrashed,
|
||||
versionToCreatedAtFormatted,
|
||||
versionToID,
|
||||
versionToUseAsTitle,
|
||||
}) => {
|
||||
const { config } = useConfig()
|
||||
const { setStepNav } = useStepNav()
|
||||
const { i18n, t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const { title } = useDocumentTitle()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
@@ -38,24 +37,7 @@ export const SetStepNav: React.FC<{
|
||||
if (collectionConfig) {
|
||||
const collectionSlug = collectionConfig.slug
|
||||
|
||||
const useAsTitle = collectionConfig.admin?.useAsTitle || 'id'
|
||||
const pluralLabel = collectionConfig.labels?.plural
|
||||
let docLabel = `[${t('general:untitled')}]`
|
||||
|
||||
const fields = collectionConfig.fields
|
||||
|
||||
const titleField = fields.find(
|
||||
(f) => fieldAffectsData(f) && 'name' in f && f.name === useAsTitle,
|
||||
)
|
||||
|
||||
if (titleField && versionToUseAsTitle) {
|
||||
docLabel =
|
||||
'localized' in titleField && titleField.localized
|
||||
? versionToUseAsTitle?.[locale.code] || docLabel
|
||||
: versionToUseAsTitle
|
||||
} else if (useAsTitle === 'id') {
|
||||
docLabel = String(id)
|
||||
}
|
||||
|
||||
const docBasePath: `/${string}` = isTrashed
|
||||
? `/collections/${collectionSlug}/trash/${id}`
|
||||
@@ -83,7 +65,7 @@ export const SetStepNav: React.FC<{
|
||||
|
||||
nav.push(
|
||||
{
|
||||
label: docLabel,
|
||||
label: title,
|
||||
url: formatAdminURL({
|
||||
adminRoute,
|
||||
path: docBasePath,
|
||||
@@ -139,7 +121,7 @@ export const SetStepNav: React.FC<{
|
||||
i18n,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
versionToUseAsTitle,
|
||||
title,
|
||||
versionToCreatedAtFormatted,
|
||||
versionToID,
|
||||
])
|
||||
|
||||
@@ -40,7 +40,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
VersionToCreatedAtLabel,
|
||||
versionToID,
|
||||
versionToStatus,
|
||||
versionToUseAsTitle,
|
||||
}) => {
|
||||
const { config, getEntityConfig } = useConfig()
|
||||
const { code } = useLocale()
|
||||
@@ -275,7 +274,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
isTrashed={isTrashed}
|
||||
versionToCreatedAtFormatted={versionToCreatedAtFormatted}
|
||||
versionToID={versionToID}
|
||||
versionToUseAsTitle={versionToUseAsTitle}
|
||||
/>
|
||||
<Gutter className={`${baseClass}__diff-wrap`}>
|
||||
<SelectedLocalesContext value={{ selectedLocales: locales.map((locale) => locale.name) }}>
|
||||
|
||||
@@ -21,5 +21,4 @@ export type DefaultVersionsViewProps = {
|
||||
VersionToCreatedAtLabel: React.ReactNode
|
||||
versionToID?: string
|
||||
versionToStatus?: string
|
||||
versionToUseAsTitle?: string
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { PayloadRequest, RelationshipField, TypeWithID } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import {
|
||||
fieldAffectsData,
|
||||
fieldIsPresentationalOnly,
|
||||
fieldShouldBeLocalized,
|
||||
flattenTopLevelFields,
|
||||
} from 'payload/shared'
|
||||
|
||||
import type { PopulatedRelationshipValue } from './index.js'
|
||||
import type { RelationshipValue } from './index.js'
|
||||
|
||||
export const generateLabelFromValue = ({
|
||||
field,
|
||||
@@ -15,9 +20,9 @@ export const generateLabelFromValue = ({
|
||||
locale: string
|
||||
parentIsLocalized: boolean
|
||||
req: PayloadRequest
|
||||
value: PopulatedRelationshipValue
|
||||
value: RelationshipValue
|
||||
}): string => {
|
||||
let relatedDoc: TypeWithID
|
||||
let relatedDoc: number | string | TypeWithID
|
||||
let relationTo: string = field.relationTo as string
|
||||
let valueToReturn: string = ''
|
||||
|
||||
@@ -25,14 +30,19 @@ export const generateLabelFromValue = ({
|
||||
relatedDoc = value.value
|
||||
relationTo = value.relationTo
|
||||
} else {
|
||||
// Non-polymorphic relationship
|
||||
// Non-polymorphic relationship or deleted document
|
||||
relatedDoc = value
|
||||
}
|
||||
|
||||
const relatedCollection = req.payload.collections[relationTo].config
|
||||
|
||||
const useAsTitle = relatedCollection?.admin?.useAsTitle
|
||||
const useAsTitleField = relatedCollection.fields.find(
|
||||
|
||||
const flattenedRelatedCollectionFields = flattenTopLevelFields(relatedCollection.fields, {
|
||||
moveSubFieldsToTop: true,
|
||||
})
|
||||
|
||||
const useAsTitleField = flattenedRelatedCollectionFields.find(
|
||||
(f) => fieldAffectsData(f) && !fieldIsPresentationalOnly(f) && f.name === useAsTitle,
|
||||
)
|
||||
let titleFieldIsLocalized = false
|
||||
@@ -44,7 +54,11 @@ export const generateLabelFromValue = ({
|
||||
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
|
||||
valueToReturn = relatedDoc[useAsTitle]
|
||||
} else {
|
||||
valueToReturn = String(relatedDoc.id)
|
||||
valueToReturn = String(
|
||||
typeof relatedDoc === 'object'
|
||||
? relatedDoc.id
|
||||
: `${req.i18n.t('general:untitled')} - ID: ${relatedDoc}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -16,7 +16,9 @@ import { generateLabelFromValue } from './generateLabelFromValue.js'
|
||||
|
||||
const baseClass = 'relationship-diff'
|
||||
|
||||
export type PopulatedRelationshipValue = { relationTo: string; value: TypeWithID } | TypeWithID
|
||||
export type RelationshipValue =
|
||||
| { relationTo: string; value: number | string | TypeWithID }
|
||||
| (number | string | TypeWithID)
|
||||
|
||||
export const Relationship: RelationshipFieldDiffServerComponent = ({
|
||||
comparisonValue: valueFrom,
|
||||
@@ -41,8 +43,8 @@ export const Relationship: RelationshipFieldDiffServerComponent = ({
|
||||
parentIsLocalized={parentIsLocalized}
|
||||
polymorphic={polymorphic}
|
||||
req={req}
|
||||
valueFrom={valueFrom as PopulatedRelationshipValue[] | undefined}
|
||||
valueTo={valueTo as PopulatedRelationshipValue[] | undefined}
|
||||
valueFrom={valueFrom as RelationshipValue[] | undefined}
|
||||
valueTo={valueTo as RelationshipValue[] | undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -56,8 +58,8 @@ export const Relationship: RelationshipFieldDiffServerComponent = ({
|
||||
parentIsLocalized={parentIsLocalized}
|
||||
polymorphic={polymorphic}
|
||||
req={req}
|
||||
valueFrom={valueFrom as PopulatedRelationshipValue}
|
||||
valueTo={valueTo as PopulatedRelationshipValue}
|
||||
valueFrom={valueFrom as RelationshipValue}
|
||||
valueTo={valueTo as RelationshipValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -70,8 +72,8 @@ export const SingleRelationshipDiff: React.FC<{
|
||||
parentIsLocalized: boolean
|
||||
polymorphic: boolean
|
||||
req: PayloadRequest
|
||||
valueFrom: PopulatedRelationshipValue
|
||||
valueTo: PopulatedRelationshipValue
|
||||
valueFrom: RelationshipValue
|
||||
valueTo: RelationshipValue
|
||||
}> = async (args) => {
|
||||
const {
|
||||
field,
|
||||
@@ -151,8 +153,8 @@ const ManyRelationshipDiff: React.FC<{
|
||||
parentIsLocalized: boolean
|
||||
polymorphic: boolean
|
||||
req: PayloadRequest
|
||||
valueFrom: PopulatedRelationshipValue[] | undefined
|
||||
valueTo: PopulatedRelationshipValue[] | undefined
|
||||
valueFrom: RelationshipValue[] | undefined
|
||||
valueTo: RelationshipValue[] | undefined
|
||||
}> = async ({
|
||||
field,
|
||||
i18n,
|
||||
@@ -169,7 +171,7 @@ const ManyRelationshipDiff: React.FC<{
|
||||
const fromArr = Array.isArray(valueFrom) ? valueFrom : []
|
||||
const toArr = Array.isArray(valueTo) ? valueTo : []
|
||||
|
||||
const makeNodes = (list: PopulatedRelationshipValue[]) =>
|
||||
const makeNodes = (list: RelationshipValue[]) =>
|
||||
list.map((val, idx) => (
|
||||
<RelationshipDocumentDiff
|
||||
field={field}
|
||||
@@ -234,7 +236,7 @@ const RelationshipDocumentDiff = ({
|
||||
relationTo: string
|
||||
req: PayloadRequest
|
||||
showPill?: boolean
|
||||
value: PopulatedRelationshipValue
|
||||
value: RelationshipValue
|
||||
}) => {
|
||||
const localeToUse =
|
||||
locale ??
|
||||
|
||||
@@ -15,6 +15,8 @@ import React from 'react'
|
||||
|
||||
const baseClass = 'upload-diff'
|
||||
|
||||
type UploadDoc = (FileData & TypeWithID) | string
|
||||
|
||||
export const Upload: UploadFieldDiffServerComponent = (args) => {
|
||||
const {
|
||||
comparisonValue: valueFrom,
|
||||
@@ -34,8 +36,8 @@ export const Upload: UploadFieldDiffServerComponent = (args) => {
|
||||
locale={locale}
|
||||
nestingLevel={nestingLevel}
|
||||
req={req}
|
||||
valueFrom={valueFrom as any}
|
||||
valueTo={valueTo as any}
|
||||
valueFrom={valueFrom as UploadDoc[]}
|
||||
valueTo={valueTo as UploadDoc[]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -47,8 +49,8 @@ export const Upload: UploadFieldDiffServerComponent = (args) => {
|
||||
locale={locale}
|
||||
nestingLevel={nestingLevel}
|
||||
req={req}
|
||||
valueFrom={valueFrom as any}
|
||||
valueTo={valueTo as any}
|
||||
valueFrom={valueFrom as UploadDoc}
|
||||
valueTo={valueTo as UploadDoc}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -59,8 +61,8 @@ export const HasManyUploadDiff: React.FC<{
|
||||
locale: string
|
||||
nestingLevel?: number
|
||||
req: PayloadRequest
|
||||
valueFrom: Array<FileData & TypeWithID>
|
||||
valueTo: Array<FileData & TypeWithID>
|
||||
valueFrom: Array<UploadDoc>
|
||||
valueTo: Array<UploadDoc>
|
||||
}> = async (args) => {
|
||||
const { field, i18n, locale, nestingLevel, req, valueFrom, valueTo } = args
|
||||
const ReactDOMServer = (await import('react-dom/server')).default
|
||||
@@ -74,7 +76,7 @@ export const HasManyUploadDiff: React.FC<{
|
||||
? valueFrom.map((uploadDoc) => (
|
||||
<UploadDocumentDiff
|
||||
i18n={i18n}
|
||||
key={uploadDoc.id}
|
||||
key={typeof uploadDoc === 'object' ? uploadDoc.id : uploadDoc}
|
||||
relationTo={field.relationTo}
|
||||
req={req}
|
||||
showCollectionSlug={showCollectionSlug}
|
||||
@@ -86,7 +88,7 @@ export const HasManyUploadDiff: React.FC<{
|
||||
? valueTo.map((uploadDoc) => (
|
||||
<UploadDocumentDiff
|
||||
i18n={i18n}
|
||||
key={uploadDoc.id}
|
||||
key={typeof uploadDoc === 'object' ? uploadDoc.id : uploadDoc}
|
||||
relationTo={field.relationTo}
|
||||
req={req}
|
||||
showCollectionSlug={showCollectionSlug}
|
||||
@@ -138,8 +140,8 @@ export const SingleUploadDiff: React.FC<{
|
||||
locale: string
|
||||
nestingLevel?: number
|
||||
req: PayloadRequest
|
||||
valueFrom: FileData & TypeWithID
|
||||
valueTo: FileData & TypeWithID
|
||||
valueFrom: UploadDoc
|
||||
valueTo: UploadDoc
|
||||
}> = async (args) => {
|
||||
const { field, i18n, locale, nestingLevel, req, valueFrom, valueTo } = args
|
||||
|
||||
@@ -204,12 +206,24 @@ const UploadDocumentDiff = (args: {
|
||||
relationTo: string
|
||||
req: PayloadRequest
|
||||
showCollectionSlug?: boolean
|
||||
uploadDoc: FileData & TypeWithID
|
||||
uploadDoc: UploadDoc
|
||||
}) => {
|
||||
const { i18n, relationTo, req, showCollectionSlug, uploadDoc } = args
|
||||
|
||||
const thumbnailSRC: string =
|
||||
('thumbnailURL' in uploadDoc && (uploadDoc?.thumbnailURL as string)) || uploadDoc?.url || ''
|
||||
let thumbnailSRC: string = ''
|
||||
if (uploadDoc && typeof uploadDoc === 'object' && 'thumbnailURL' in uploadDoc) {
|
||||
thumbnailSRC =
|
||||
(typeof uploadDoc.thumbnailURL === 'string' && uploadDoc.thumbnailURL) ||
|
||||
(typeof uploadDoc.url === 'string' && uploadDoc.url) ||
|
||||
''
|
||||
}
|
||||
|
||||
let filename: string
|
||||
if (uploadDoc && typeof uploadDoc === 'object') {
|
||||
filename = uploadDoc.filename
|
||||
} else {
|
||||
filename = `${i18n.t('general:untitled')} - ID: ${uploadDoc as number | string}`
|
||||
}
|
||||
|
||||
let pillLabel: null | string = null
|
||||
|
||||
@@ -224,12 +238,12 @@ const UploadDocumentDiff = (args: {
|
||||
<div
|
||||
className={`${baseClass}`}
|
||||
data-enable-match="true"
|
||||
data-id={uploadDoc?.id}
|
||||
data-id={typeof uploadDoc === 'object' ? uploadDoc?.id : uploadDoc}
|
||||
data-relation-to={relationTo}
|
||||
>
|
||||
<div className={`${baseClass}__card`}>
|
||||
<div className={`${baseClass}__thumbnail`}>
|
||||
{thumbnailSRC?.length ? <img alt={uploadDoc?.filename} src={thumbnailSRC} /> : <File />}
|
||||
{thumbnailSRC?.length ? <img alt={filename} src={thumbnailSRC} /> : <File />}
|
||||
</div>
|
||||
{pillLabel && (
|
||||
<div className={`${baseClass}__pill`} data-enable-match="false">
|
||||
@@ -237,7 +251,7 @@ const UploadDocumentDiff = (args: {
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__info`} data-enable-match="false">
|
||||
<strong>{uploadDoc?.filename}</strong>
|
||||
<strong>{filename}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -411,11 +411,6 @@ export async function VersionView(props: DocumentViewServerProps) {
|
||||
})
|
||||
}
|
||||
|
||||
const useAsTitleFieldName = collectionConfig?.admin?.useAsTitle || 'id'
|
||||
const versionToUseAsTitle =
|
||||
useAsTitleFieldName === 'id'
|
||||
? String(versionTo.parent)
|
||||
: versionTo.version?.[useAsTitleFieldName]
|
||||
return (
|
||||
<DefaultVersionView
|
||||
canUpdate={docPermissions?.update}
|
||||
@@ -430,7 +425,6 @@ export async function VersionView(props: DocumentViewServerProps) {
|
||||
VersionToCreatedAtLabel={formatPill({ doc: versionTo, labelStyle: 'pill' })}
|
||||
versionToID={versionTo.id}
|
||||
versionToStatus={versionTo.version?._status}
|
||||
versionToUseAsTitle={versionToUseAsTitle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type AutosaveCellProps = {
|
||||
rowData: {
|
||||
autosave?: boolean
|
||||
id: number | string
|
||||
localeStatus?: Record<string, 'draft' | 'published'>
|
||||
publishedLocale?: string
|
||||
version: {
|
||||
_status: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { initTransaction } from '../../utilities/initTransaction.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { getFieldsToSign } from '../getFieldsToSign.js'
|
||||
import { jwtSign } from '../jwt.js'
|
||||
import { addSessionToUser } from '../sessions.js'
|
||||
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
|
||||
import { generatePasswordSaltHash } from '../strategies/local/generatePasswordSaltHash.js'
|
||||
|
||||
@@ -143,12 +144,25 @@ export const resetPasswordOperation = async <TSlug extends CollectionSlug>(
|
||||
|
||||
await authenticateLocalStrategy({ doc, password: data.password })
|
||||
|
||||
const fieldsToSign = getFieldsToSign({
|
||||
const fieldsToSignArgs: Parameters<typeof getFieldsToSign>[0] = {
|
||||
collectionConfig,
|
||||
email: user.email,
|
||||
user,
|
||||
}
|
||||
|
||||
const { sid } = await addSessionToUser({
|
||||
collectionConfig,
|
||||
payload,
|
||||
req,
|
||||
user,
|
||||
})
|
||||
|
||||
if (sid) {
|
||||
fieldsToSignArgs.sid = sid
|
||||
}
|
||||
|
||||
const fieldsToSign = getFieldsToSign(fieldsToSignArgs)
|
||||
|
||||
const { token } = await jwtSign({
|
||||
fieldsToSign,
|
||||
secret,
|
||||
|
||||
@@ -11,16 +11,21 @@ import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
|
||||
import { findByIDOperation } from '../operations/findByID.js'
|
||||
|
||||
export const findByIDHandler: PayloadHandler = async (req) => {
|
||||
const { searchParams } = req
|
||||
const { data, searchParams } = req
|
||||
const { id, collection } = getRequestCollectionWithID(req)
|
||||
const depth = searchParams.get('depth')
|
||||
const trash = searchParams.get('trash') === 'true'
|
||||
const depth = data ? data.depth : searchParams.get('depth')
|
||||
const trash = data ? data.trash : searchParams.get('trash') === 'true'
|
||||
|
||||
const result = await findByIDOperation({
|
||||
id,
|
||||
collection,
|
||||
data: data
|
||||
? data?.data
|
||||
: searchParams.get('data')
|
||||
? JSON.parse(searchParams.get('data') as string)
|
||||
: undefined,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: searchParams.get('draft') === 'true',
|
||||
draft: data ? data.draft : searchParams.get('draft') === 'true',
|
||||
joins: sanitizeJoinParams(req.query.joins as JoinParams),
|
||||
populate: sanitizePopulateParam(req.query.populate),
|
||||
req,
|
||||
|
||||
@@ -291,6 +291,7 @@ export const createOperation = async <
|
||||
autosave,
|
||||
collection: collectionConfig,
|
||||
docWithLocales: result,
|
||||
locale,
|
||||
operation: 'create',
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
|
||||
@@ -32,6 +32,11 @@ import { buildAfterOperation } from './utils.js'
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
currentDepth?: number
|
||||
/**
|
||||
* You may pass the document data directly which will skip the `db.findOne` database query.
|
||||
* This is useful if you want to use this endpoint solely for running hooks and populating data.
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
depth?: number
|
||||
disableErrors?: boolean
|
||||
draft?: boolean
|
||||
@@ -163,7 +168,8 @@ export const findByIDOperation = async <
|
||||
throw new NotFound(t)
|
||||
}
|
||||
|
||||
let result: DataFromCollectionSlug<TSlug> = (await req.payload.db.findOne(findOneArgs))!
|
||||
let result: DataFromCollectionSlug<TSlug> =
|
||||
(args.data as DataFromCollectionSlug<TSlug>) ?? (await req.payload.db.findOne(findOneArgs))!
|
||||
|
||||
if (!result) {
|
||||
if (!disableErrors) {
|
||||
|
||||
@@ -41,6 +41,11 @@ export type Options<
|
||||
* @internal
|
||||
*/
|
||||
currentDepth?: number
|
||||
/**
|
||||
* You may pass the document data directly which will skip the `db.findOne` database query.
|
||||
* This is useful if you want to use this endpoint solely for running hooks and populating data.
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
/**
|
||||
* [Control auto-population](https://payloadcms.com/docs/queries/depth) of nested relationship and upload fields.
|
||||
*/
|
||||
@@ -126,6 +131,7 @@ export async function findByIDLocal<
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
currentDepth,
|
||||
data,
|
||||
depth,
|
||||
disableErrors = false,
|
||||
draft = false,
|
||||
@@ -150,6 +156,7 @@ export async function findByIDLocal<
|
||||
id,
|
||||
collection,
|
||||
currentDepth,
|
||||
data,
|
||||
depth,
|
||||
disableErrors,
|
||||
draft,
|
||||
|
||||
@@ -316,6 +316,7 @@ export const updateDocument = async <
|
||||
collection: collectionConfig,
|
||||
docWithLocales: result,
|
||||
draft: shouldSaveDraft,
|
||||
locale,
|
||||
operation: 'update',
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
|
||||
@@ -159,6 +159,16 @@ export const createClientConfig = ({
|
||||
|
||||
break
|
||||
|
||||
case 'experimental':
|
||||
if (config.experimental) {
|
||||
clientConfig.experimental = {}
|
||||
if (config.experimental?.localizeStatus) {
|
||||
clientConfig.experimental.localizeStatus = config.experimental.localizeStatus
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'folders':
|
||||
if (config.folders) {
|
||||
clientConfig.folders = {
|
||||
|
||||
@@ -47,6 +47,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
||||
defaultDepth: 2,
|
||||
defaultMaxTextLength: 40000,
|
||||
endpoints: [],
|
||||
experimental: {},
|
||||
globals: [],
|
||||
graphQL: {
|
||||
disablePlaygroundInProduction: true,
|
||||
@@ -121,6 +122,7 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
||||
config.defaultDepth = config.defaultDepth ?? 2
|
||||
config.defaultMaxTextLength = config.defaultMaxTextLength ?? 40000
|
||||
config.endpoints = config.endpoints ?? []
|
||||
config.experimental = config.experimental ?? {}
|
||||
config.globals = config.globals ?? []
|
||||
config.graphQL = {
|
||||
disableIntrospectionInProduction: true,
|
||||
|
||||
@@ -721,6 +721,14 @@ export type ImportMapGenerators = Array<
|
||||
}) => void
|
||||
>
|
||||
|
||||
/**
|
||||
* Experimental features.
|
||||
* These may be unstable and may change or be removed in future releases.
|
||||
*/
|
||||
export type ExperimentalConfig = {
|
||||
localizeStatus?: boolean
|
||||
}
|
||||
|
||||
export type AfterErrorHook = (
|
||||
args: AfterErrorHookArgs,
|
||||
) => AfterErrorResult | Promise<AfterErrorResult>
|
||||
@@ -752,6 +760,12 @@ export type Config = {
|
||||
username?: string
|
||||
}
|
||||
| false
|
||||
/**
|
||||
* Automatically refresh user tokens for users logged into the dashboard
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
autoRefresh?: boolean
|
||||
/** Set account profile picture. Options: gravatar, default or a custom React component. */
|
||||
avatar?:
|
||||
| 'default'
|
||||
@@ -1041,6 +1055,12 @@ export type Config = {
|
||||
email?: EmailAdapter | Promise<EmailAdapter>
|
||||
/** Custom REST endpoints */
|
||||
endpoints?: Endpoint[]
|
||||
/**
|
||||
* Configure experimental features for Payload.
|
||||
*
|
||||
* These features may be unstable and may change or be removed in future releases.
|
||||
*/
|
||||
experimental?: ExperimentalConfig
|
||||
/**
|
||||
* Options for folder view within the admin panel
|
||||
* @experimental this feature may change in minor versions until it is fully stable
|
||||
@@ -1309,6 +1329,7 @@ export type SanitizedConfig = {
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor?: RichTextAdapter<any, any, any>
|
||||
endpoints: Endpoint[]
|
||||
experimental?: ExperimentalConfig
|
||||
globals: SanitizedGlobalConfig[]
|
||||
i18n: Required<I18nOptions>
|
||||
jobs: SanitizedJobsConfig
|
||||
|
||||
@@ -390,6 +390,7 @@ export type CreateVersionArgs<T = TypeWithID> = {
|
||||
autosave: boolean
|
||||
collectionSlug: CollectionSlug
|
||||
createdAt: string
|
||||
localeStatus?: Record<string, 'draft' | 'published'>
|
||||
/** ID of the parent document for which the version should be created for */
|
||||
parent: number | string
|
||||
publishedLocale?: string
|
||||
@@ -414,6 +415,7 @@ export type CreateGlobalVersionArgs<T = TypeWithID> = {
|
||||
autosave: boolean
|
||||
createdAt: string
|
||||
globalSlug: GlobalSlug
|
||||
localeStatus?: Record<string, 'draft' | 'published'>
|
||||
/** ID of the parent document for which the version should be created for */
|
||||
parent: number | string
|
||||
publishedLocale?: string
|
||||
|
||||
@@ -75,8 +75,6 @@ export {
|
||||
|
||||
export { extractID } from '../utilities/extractID.js'
|
||||
|
||||
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
|
||||
|
||||
export { flattenAllFields } from '../utilities/flattenAllFields.js'
|
||||
export { flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
|
||||
export { formatAdminURL } from '../utilities/formatAdminURL.js'
|
||||
|
||||
@@ -87,8 +87,8 @@ export type RootFoldersConfiguration = {
|
||||
collectionOverrides?: (({
|
||||
collection,
|
||||
}: {
|
||||
collection: CollectionConfig
|
||||
}) => CollectionConfig | Promise<CollectionConfig>)[]
|
||||
collection: Omit<CollectionConfig, 'trash'>
|
||||
}) => Omit<CollectionConfig, 'trash'> | Promise<Omit<CollectionConfig, 'trash'>>)[]
|
||||
/**
|
||||
* If true, you can scope folders to specific collections.
|
||||
*
|
||||
|
||||
@@ -79,30 +79,17 @@ export const getFolderData = async ({
|
||||
subfolders: sortDocs({ docs: result.subfolders, sort }),
|
||||
}
|
||||
} else {
|
||||
// subfolders and documents are queried separately
|
||||
const subfoldersPromise = getOrphanedDocs({
|
||||
collectionSlug: payload.config.folders.slug,
|
||||
folderFieldName: payload.config.folders.fieldName,
|
||||
req,
|
||||
where: folderWhere,
|
||||
})
|
||||
const documentsPromise = collectionSlug
|
||||
? getOrphanedDocs({
|
||||
collectionSlug,
|
||||
folderFieldName: payload.config.folders.fieldName,
|
||||
req,
|
||||
where: documentWhere,
|
||||
})
|
||||
: Promise.resolve([])
|
||||
const [breadcrumbs, subfolders, documents] = await Promise.all([
|
||||
breadcrumbsPromise,
|
||||
subfoldersPromise,
|
||||
documentsPromise,
|
||||
])
|
||||
const [breadcrumbs, subfolders] = await Promise.all([breadcrumbsPromise, subfoldersPromise])
|
||||
|
||||
return {
|
||||
breadcrumbs,
|
||||
documents: sortDocs({ docs: documents, sort }),
|
||||
documents: [],
|
||||
folderAssignedCollections: collectionSlug ? [collectionSlug] : undefined,
|
||||
subfolders: sortDocs({ docs: subfolders, sort }),
|
||||
}
|
||||
|
||||
@@ -11,13 +11,18 @@ import { findOneOperation } from '../operations/findOne.js'
|
||||
|
||||
export const findOneHandler: PayloadHandler = async (req) => {
|
||||
const globalConfig = getRequestGlobal(req)
|
||||
const { searchParams } = req
|
||||
const depth = searchParams.get('depth')
|
||||
const { data, searchParams } = req
|
||||
const depth = data ? data.depth : searchParams.get('depth')
|
||||
|
||||
const result = await findOneOperation({
|
||||
slug: globalConfig.slug,
|
||||
data: data
|
||||
? data?.data
|
||||
: searchParams.get('data')
|
||||
? JSON.parse(searchParams.get('data') as string)
|
||||
: undefined,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: searchParams.get('draft') === 'true',
|
||||
draft: data ? data.draft : searchParams.get('draft') === 'true',
|
||||
globalConfig,
|
||||
populate: sanitizePopulateParam(req.query.populate),
|
||||
req,
|
||||
|
||||
@@ -17,6 +17,11 @@ import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
|
||||
import { replaceWithDraftIfAvailable } from '../../versions/drafts/replaceWithDraftIfAvailable.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* You may pass the document data directly which will skip the `db.findOne` database query.
|
||||
* This is useful if you want to use this endpoint solely for running hooks and populating data.
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
depth?: number
|
||||
draft?: boolean
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
@@ -67,13 +72,15 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
|
||||
// Perform database operation
|
||||
// /////////////////////////////////////
|
||||
|
||||
let doc = await req.payload.db.findGlobal({
|
||||
slug,
|
||||
locale: locale!,
|
||||
req,
|
||||
select,
|
||||
where: overrideAccess ? undefined : (accessResult as Where),
|
||||
})
|
||||
let doc =
|
||||
(args.data as any) ??
|
||||
(await req.payload.db.findGlobal({
|
||||
slug,
|
||||
locale: locale!,
|
||||
req,
|
||||
select,
|
||||
where: overrideAccess ? undefined : (accessResult as Where),
|
||||
}))
|
||||
if (!doc) {
|
||||
doc = {}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
|
||||
* to determine if it should run or not.
|
||||
*/
|
||||
context?: RequestContext
|
||||
/**
|
||||
* You may pass the document data directly which will skip the `db.findOne` database query.
|
||||
* This is useful if you want to use this endpoint solely for running hooks and populating data.
|
||||
*/
|
||||
data?: Record<string, unknown>
|
||||
/**
|
||||
* [Control auto-population](https://payloadcms.com/docs/queries/depth) of nested relationship and upload fields.
|
||||
*/
|
||||
@@ -84,6 +89,7 @@ export async function findOneGlobalLocal<
|
||||
): Promise<TransformGlobalWithSelect<TSlug, TSelect>> {
|
||||
const {
|
||||
slug: globalSlug,
|
||||
data,
|
||||
depth,
|
||||
draft = false,
|
||||
includeLockStatus,
|
||||
@@ -101,6 +107,7 @@ export async function findOneGlobalLocal<
|
||||
|
||||
return findOneOperation({
|
||||
slug: globalSlug as string,
|
||||
data,
|
||||
depth,
|
||||
draft,
|
||||
globalConfig,
|
||||
|
||||
@@ -285,6 +285,7 @@ export const updateOperation = async <
|
||||
docWithLocales: result,
|
||||
draft: shouldSaveDraft,
|
||||
global: globalConfig,
|
||||
locale,
|
||||
operation: 'update',
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
|
||||
@@ -1671,7 +1671,6 @@ export {
|
||||
type CustomVersionParser,
|
||||
} from './utilities/dependencies/dependencyChecker.js'
|
||||
export { getDependencies } from './utilities/dependencies/getDependencies.js'
|
||||
export type { FieldSchemaJSON } from './utilities/fieldSchemaToJSON.js'
|
||||
export {
|
||||
findUp,
|
||||
findUpSync,
|
||||
|
||||
@@ -157,6 +157,7 @@ export type JobsConfig = {
|
||||
* drastically affect performance.
|
||||
*
|
||||
* @default false
|
||||
* @deprecated - this will be removed in 4.0
|
||||
*/
|
||||
runHooks?: boolean
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
|
||||
import type { PayloadRequest } from '../../index.js'
|
||||
import type { JobLog, PayloadRequest } from '../../index.js'
|
||||
import type { RunJobsSilent } from '../localAPI.js'
|
||||
import type { UpdateJobFunction } from '../operations/runJobs/runJob/getUpdateJobFunction.js'
|
||||
import type { TaskError } from './index.js'
|
||||
@@ -59,19 +59,6 @@ export async function handleTaskError({
|
||||
|
||||
const currentDate = getCurrentDate()
|
||||
|
||||
;(job.log ??= []).push({
|
||||
id: new ObjectId().toHexString(),
|
||||
completedAt: currentDate.toISOString(),
|
||||
error: errorJSON,
|
||||
executedAt: executedAt.toISOString(),
|
||||
input,
|
||||
output: output ?? {},
|
||||
parent: req.payload.config.jobs.addParentToTaskLog ? parent : undefined,
|
||||
state: 'failed',
|
||||
taskID,
|
||||
taskSlug,
|
||||
})
|
||||
|
||||
if (job.waitUntil) {
|
||||
// Check if waitUntil is in the past
|
||||
const waitUntil = new Date(job.waitUntil)
|
||||
@@ -99,6 +86,19 @@ export async function handleTaskError({
|
||||
maxRetries = retriesConfig.attempts
|
||||
}
|
||||
|
||||
const taskLogToPush: JobLog = {
|
||||
id: new ObjectId().toHexString(),
|
||||
completedAt: currentDate.toISOString(),
|
||||
error: errorJSON,
|
||||
executedAt: executedAt.toISOString(),
|
||||
input,
|
||||
output: output ?? {},
|
||||
parent: req.payload.config.jobs.addParentToTaskLog ? parent : undefined,
|
||||
state: 'failed',
|
||||
taskID,
|
||||
taskSlug,
|
||||
}
|
||||
|
||||
if (!taskStatus?.complete && (taskStatus?.totalTried ?? 0) >= maxRetries) {
|
||||
/**
|
||||
* Task reached max retries => workflow will not retry
|
||||
@@ -107,7 +107,9 @@ export async function handleTaskError({
|
||||
await updateJob({
|
||||
error: errorJSON,
|
||||
hasError: true,
|
||||
log: job.log,
|
||||
log: {
|
||||
$push: taskLogToPush,
|
||||
} as any,
|
||||
processing: false,
|
||||
totalTried: (job.totalTried ?? 0) + 1,
|
||||
waitUntil: job.waitUntil,
|
||||
@@ -167,7 +169,9 @@ export async function handleTaskError({
|
||||
await updateJob({
|
||||
error: hasFinalError ? errorJSON : undefined,
|
||||
hasError: hasFinalError, // If reached max retries => final error. If hasError is true this job will not be retried
|
||||
log: job.log,
|
||||
log: {
|
||||
$push: taskLogToPush,
|
||||
} as any,
|
||||
processing: false,
|
||||
totalTried: (job.totalTried ?? 0) + 1,
|
||||
waitUntil: job.waitUntil,
|
||||
|
||||
@@ -79,7 +79,6 @@ export async function handleWorkflowError({
|
||||
await updateJob({
|
||||
error: errorJSON,
|
||||
hasError: hasFinalError, // If reached max retries => final error. If hasError is true this job will not be retried
|
||||
log: job.log,
|
||||
processing: false,
|
||||
totalTried: (job.totalTried ?? 0) + 1,
|
||||
waitUntil: job.waitUntil,
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
TaskType,
|
||||
} from '../../../config/types/taskTypes.js'
|
||||
import type {
|
||||
JobLog,
|
||||
SingleTaskStatus,
|
||||
WorkflowConfig,
|
||||
WorkflowTypes,
|
||||
@@ -184,7 +185,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
||||
await taskConfig.onSuccess()
|
||||
}
|
||||
|
||||
;(job.log ??= []).push({
|
||||
const newLogItem: JobLog = {
|
||||
id: new ObjectId().toHexString(),
|
||||
completedAt: getCurrentDate().toISOString(),
|
||||
executedAt: executedAt.toISOString(),
|
||||
@@ -194,10 +195,14 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
||||
state: 'succeeded',
|
||||
taskID,
|
||||
taskSlug,
|
||||
})
|
||||
}
|
||||
|
||||
await updateJob({
|
||||
log: job.log,
|
||||
log: {
|
||||
$push: newLogItem,
|
||||
} as any,
|
||||
// Set to null to skip main row update on postgres. 2 => 1 db round trips
|
||||
updatedAt: null as any,
|
||||
})
|
||||
|
||||
return output
|
||||
|
||||
@@ -83,8 +83,10 @@ export async function updateJobs({
|
||||
: undefined,
|
||||
}
|
||||
|
||||
// Ensure updatedAt date is always updated
|
||||
data.updatedAt = new Date().toISOString()
|
||||
if (typeof data.updatedAt === 'undefined') {
|
||||
// Ensure updatedAt date is always updated
|
||||
data.updatedAt = new Date().toISOString()
|
||||
}
|
||||
|
||||
const args: UpdateJobsArgs = id
|
||||
? {
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import type { ClientConfig } from '../config/client.js'
|
||||
import type { ClientField } from '../fields/config/client.js'
|
||||
import type { Field, FieldTypes } from '../fields/config/types.js'
|
||||
|
||||
import { fieldAffectsData } from '../fields/config/types.js'
|
||||
|
||||
export type FieldSchemaJSON = {
|
||||
blocks?: FieldSchemaJSON // TODO: conditionally add based on `type`
|
||||
fields?: FieldSchemaJSON // TODO: conditionally add based on `type`
|
||||
hasMany?: boolean // TODO: conditionally add based on `type`
|
||||
name: string
|
||||
relationTo?: string // TODO: conditionally add based on `type`
|
||||
slug?: string // TODO: conditionally add based on `type`
|
||||
type: FieldTypes
|
||||
}[]
|
||||
|
||||
export const fieldSchemaToJSON = (
|
||||
fields: (ClientField | Field)[],
|
||||
config: ClientConfig,
|
||||
): FieldSchemaJSON => {
|
||||
return fields.reduce((acc, field) => {
|
||||
let result = acc
|
||||
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(
|
||||
[
|
||||
...field.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
config,
|
||||
),
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
blocks: (field.blockReferences ?? field.blocks).reduce((acc, _block) => {
|
||||
const block = typeof _block === 'string' ? config.blocksMap[_block]! : _block
|
||||
;(acc as any)[block.slug] = {
|
||||
fields: fieldSchemaToJSON(
|
||||
[
|
||||
...block.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
config,
|
||||
),
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {} as FieldSchemaJSON),
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'collapsible': // eslint-disable no-fallthrough
|
||||
case 'row':
|
||||
result = result.concat(fieldSchemaToJSON(field.fields, config))
|
||||
break
|
||||
|
||||
case 'group':
|
||||
if (fieldAffectsData(field)) {
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(field.fields, config),
|
||||
})
|
||||
} else {
|
||||
result = result.concat(fieldSchemaToJSON(field.fields, config))
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'relationship': // eslint-disable no-fallthrough
|
||||
case 'upload':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
hasMany: 'hasMany' in field ? Boolean(field.hasMany) : false, // TODO: type this
|
||||
relationTo: field.relationTo as string,
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'tabs': {
|
||||
let tabFields: FieldSchemaJSON = []
|
||||
|
||||
field.tabs.forEach((tab) => {
|
||||
if ('name' in tab) {
|
||||
tabFields.push({
|
||||
name: tab.name,
|
||||
type: field.type,
|
||||
fields: fieldSchemaToJSON(tab.fields, config),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tabFields = tabFields.concat(fieldSchemaToJSON(tab.fields, config))
|
||||
})
|
||||
|
||||
result = result.concat(tabFields)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
if ('name' in field) {
|
||||
acc.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [] as FieldSchemaJSON)
|
||||
}
|
||||
@@ -84,21 +84,44 @@ export const handleEndpoints = async ({
|
||||
(request.headers.get('X-Payload-HTTP-Method-Override') === 'GET' ||
|
||||
request.headers.get('X-HTTP-Method-Override') === 'GET')
|
||||
) {
|
||||
const search = await request.text()
|
||||
let url = request.url
|
||||
let data: any = undefined
|
||||
if (request.headers.get('Content-Type') === 'application/x-www-form-urlencoded') {
|
||||
const search = await request.text()
|
||||
url = `${request.url}?${search}`
|
||||
} else if (request.headers.get('Content-Type') === 'application/json') {
|
||||
// May not be supported by every endpoint
|
||||
data = await request.json()
|
||||
|
||||
// locale and fallbackLocale is read by createPayloadRequest to populate req.locale and req.fallbackLocale
|
||||
// => add to searchParams
|
||||
if (data?.locale) {
|
||||
url += `?locale=${data.locale}`
|
||||
}
|
||||
if (data?.fallbackLocale) {
|
||||
url += `&fallbackLocale=${data.depth}`
|
||||
}
|
||||
}
|
||||
|
||||
const req = new Request(url, {
|
||||
// @ts-expect-error // TODO: check if this is required
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
headers: request.headers,
|
||||
method: 'GET',
|
||||
signal: request.signal,
|
||||
})
|
||||
|
||||
if (data) {
|
||||
// @ts-expect-error attach data to request - less overhead than using urlencoded
|
||||
req.data = data
|
||||
}
|
||||
|
||||
const url = `${request.url}?${new URLSearchParams(search).toString()}`
|
||||
const response = await handleEndpoints({
|
||||
basePath,
|
||||
config: incomingConfig,
|
||||
path,
|
||||
request: new Request(url, {
|
||||
// @ts-expect-error // TODO: check if this is required
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
headers: request.headers,
|
||||
method: 'GET',
|
||||
signal: request.signal,
|
||||
}),
|
||||
request: req,
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
@@ -17,6 +17,7 @@ export const headersWithCors = ({ headers, req }: CorsArgs): Headers => {
|
||||
'Authorization',
|
||||
'Content-Encoding',
|
||||
'x-apollo-tracing',
|
||||
'X-Payload-HTTP-Method-Override',
|
||||
]
|
||||
|
||||
headers.set('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { CheckboxField, Field, Option } from '../fields/config/types.js'
|
||||
|
||||
export const statuses: Option[] = [
|
||||
@@ -43,3 +44,23 @@ export const versionSnapshotField: CheckboxField = {
|
||||
},
|
||||
index: true,
|
||||
}
|
||||
|
||||
export function buildLocaleStatusField(config: SanitizedConfig): Field[] {
|
||||
if (!config.localization || !config.localization.locales) {
|
||||
return []
|
||||
}
|
||||
|
||||
return config.localization.locales.map((locale) => {
|
||||
const code = typeof locale === 'string' ? locale : locale.code
|
||||
|
||||
return {
|
||||
name: code,
|
||||
type: 'select',
|
||||
index: true,
|
||||
options: [
|
||||
{ label: ({ t }) => t('version:draft'), value: 'draft' },
|
||||
{ label: ({ t }) => t('version:published'), value: 'published' },
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
||||
|
||||
import { versionSnapshotField } from './baseFields.js'
|
||||
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
|
||||
|
||||
export const buildVersionCollectionFields = <T extends boolean = false>(
|
||||
config: SanitizedConfig,
|
||||
@@ -62,6 +62,23 @@ export const buildVersionCollectionFields = <T extends boolean = false>(
|
||||
return locale.code
|
||||
}),
|
||||
})
|
||||
|
||||
if (config.experimental?.localizeStatus) {
|
||||
const localeStatusFields = buildLocaleStatusField(config)
|
||||
|
||||
fields.push({
|
||||
name: 'localeStatus',
|
||||
type: 'group',
|
||||
admin: {
|
||||
disableBulkEdit: true,
|
||||
disabled: true,
|
||||
},
|
||||
fields: localeStatusFields,
|
||||
...(flatten && {
|
||||
flattenedFields: localeStatusFields as FlattenedField[],
|
||||
})!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
|
||||
import { versionSnapshotField } from './baseFields.js'
|
||||
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
|
||||
|
||||
export const buildVersionGlobalFields = <T extends boolean = false>(
|
||||
config: SanitizedConfig,
|
||||
@@ -56,6 +56,23 @@ export const buildVersionGlobalFields = <T extends boolean = false>(
|
||||
return locale.code
|
||||
}),
|
||||
})
|
||||
|
||||
if (config.experimental.localizeStatus) {
|
||||
const localeStatusFields = buildLocaleStatusField(config)
|
||||
|
||||
fields.push({
|
||||
name: 'localeStatus',
|
||||
type: 'group',
|
||||
admin: {
|
||||
disableBulkEdit: true,
|
||||
disabled: true,
|
||||
},
|
||||
fields: localeStatusFields,
|
||||
...(flatten && {
|
||||
flattenedFields: localeStatusFields as FlattenedField[],
|
||||
})!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({
|
||||
|
||||
@@ -111,6 +111,12 @@ export const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
||||
draft.version = {} as T
|
||||
}
|
||||
|
||||
// Lift locale status from version data if available
|
||||
const localeStatus = draft.localeStatus || {}
|
||||
if (locale && localeStatus[locale]) {
|
||||
;(draft.version as { _status?: string })['_status'] = localeStatus[locale]
|
||||
}
|
||||
|
||||
// Disregard all other draft content at this point,
|
||||
// Only interested in the version itself.
|
||||
// Operations will handle firing hooks, etc.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { version } from 'os'
|
||||
|
||||
// @ts-strict-ignore
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
@@ -16,6 +18,7 @@ type Args = {
|
||||
draft?: boolean
|
||||
global?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
locale?: null | string
|
||||
operation?: 'create' | 'restoreVersion' | 'update'
|
||||
payload: Payload
|
||||
publishSpecificLocale?: string
|
||||
@@ -31,6 +34,7 @@ export const saveVersion = async ({
|
||||
docWithLocales: doc,
|
||||
draft,
|
||||
global,
|
||||
locale,
|
||||
operation,
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
@@ -42,6 +46,7 @@ export const saveVersion = async ({
|
||||
let createNewVersion = true
|
||||
const now = new Date().toISOString()
|
||||
const versionData = deepCopyObjectSimple(doc)
|
||||
|
||||
if (draft) {
|
||||
versionData._status = 'draft'
|
||||
}
|
||||
@@ -55,39 +60,39 @@ export const saveVersion = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
if (autosave) {
|
||||
let docs
|
||||
const findVersionArgs = {
|
||||
let docs
|
||||
const findVersionArgs = {
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
;({ docs } = await payload.db.findVersions({
|
||||
...findVersionArgs,
|
||||
collection: collection.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
;({ docs } = await payload.db.findVersions({
|
||||
...findVersionArgs,
|
||||
collection: collection.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
;({ docs } = await payload.db.findGlobalVersions({
|
||||
...findVersionArgs,
|
||||
global: global!.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
}))
|
||||
}
|
||||
const [latestVersion] = docs
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
;({ docs } = await payload.db.findGlobalVersions({
|
||||
...findVersionArgs,
|
||||
global: global!.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
}))
|
||||
}
|
||||
const [latestVersion] = docs
|
||||
|
||||
if (autosave) {
|
||||
// overwrite the latest version if it's set to autosave
|
||||
if (latestVersion && 'autosave' in latestVersion && latestVersion.autosave === true) {
|
||||
createNewVersion = false
|
||||
@@ -125,11 +130,53 @@ export const saveVersion = async ({
|
||||
}
|
||||
|
||||
if (createNewVersion) {
|
||||
let localeStatus = {}
|
||||
const localizationEnabled =
|
||||
payload.config.localization && payload.config.localization.locales.length > 0
|
||||
|
||||
if (
|
||||
localizationEnabled &&
|
||||
payload.config.localization !== false &&
|
||||
payload.config.experimental?.localizeStatus
|
||||
) {
|
||||
const allLocales = (
|
||||
(payload.config.localization && payload.config.localization?.locales) ||
|
||||
[]
|
||||
).map((locale) => (typeof locale === 'string' ? locale : locale.code))
|
||||
|
||||
// If `publish all`, set all locales to published
|
||||
if (versionData._status === 'published' && !publishSpecificLocale) {
|
||||
localeStatus = Object.fromEntries(allLocales.map((code) => [code, 'published']))
|
||||
} else if (publishSpecificLocale || (locale && versionData._status === 'draft')) {
|
||||
const status: 'draft' | 'published' = publishSpecificLocale ? 'published' : 'draft'
|
||||
const incomingLocale = String(publishSpecificLocale || locale)
|
||||
const existing = latestVersion?.localeStatus
|
||||
|
||||
// If no locale statuses are set, set it and set all others to draft
|
||||
if (!existing) {
|
||||
localeStatus = {
|
||||
...Object.fromEntries(
|
||||
allLocales.filter((code) => code !== incomingLocale).map((code) => [code, 'draft']),
|
||||
),
|
||||
[incomingLocale]: status,
|
||||
}
|
||||
} else {
|
||||
// If locales already exist, update the status for the incoming locale
|
||||
const { [incomingLocale]: _, ...rest } = existing
|
||||
localeStatus = {
|
||||
...rest,
|
||||
[incomingLocale]: status,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createVersionArgs = {
|
||||
autosave: Boolean(autosave),
|
||||
collectionSlug: undefined as string | undefined,
|
||||
createdAt: operation === 'restoreVersion' ? versionData.createdAt : now,
|
||||
globalSlug: undefined as string | undefined,
|
||||
localeStatus,
|
||||
parent: collection ? id : undefined,
|
||||
publishedLocale: publishSpecificLocale || undefined,
|
||||
req,
|
||||
|
||||
@@ -122,6 +122,7 @@ export type SanitizedGlobalVersions = {
|
||||
export type TypeWithVersion<T> = {
|
||||
createdAt: string
|
||||
id: string
|
||||
localeStatus: Record<string, 'draft' | 'published'>
|
||||
parent: number | string
|
||||
publishedLocale?: string
|
||||
snapshot?: boolean
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-multi-tenant",
|
||||
"version": "3.53.0",
|
||||
"version": "3.54.0",
|
||||
"description": "Multi Tenant plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user