Compare commits
2 Commits
v3.0.0-bet
...
fix/form-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e37897f7a5 | ||
|
|
dbf1a1bd8b |
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
@@ -12,8 +12,7 @@ on:
|
||||
- beta
|
||||
|
||||
concurrency:
|
||||
# <workflow_name>-<branch_name>-<true || commit_sha if branch is protected>
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref_protected && github.sha || ''}}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -301,7 +300,6 @@ jobs:
|
||||
- fields__collections__Lexical
|
||||
- live-preview
|
||||
- localization
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-nested-docs
|
||||
@@ -346,7 +344,7 @@ jobs:
|
||||
|
||||
- name: Cache Playwright Browsers for Playwright's Version
|
||||
id: cache-playwright-browsers
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
|
||||
@@ -425,6 +423,7 @@ jobs:
|
||||
pnpm run build
|
||||
|
||||
tests-type-generation:
|
||||
if: false # This should be replaced with gen on a real Payload project
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
|
||||
@@ -565,11 +565,10 @@ These are the props that will be passed to your custom Label.
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { getTranslation } from 'payload/utilities/getTranslation'
|
||||
|
||||
type Props = {
|
||||
htmlFor?: string
|
||||
@@ -681,21 +680,21 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo
|
||||
|
||||
### Getting the current language
|
||||
|
||||
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `@payloadcms/ui/providers/Translation` in your components.
|
||||
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `react-i18next` in your components.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const CustomComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { t, i18n } = useTranslation()
|
||||
const { t, i18n } = useTranslation('namespace1')
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li>{t('namespace1:key', { variable: 'value' })}</li>
|
||||
<li>{t('key', { variable: 'value' })}</li>
|
||||
<li>{t('namespace2:key', { variable: 'value' })}</li>
|
||||
<li>{i18n.language}</li>
|
||||
</ul>
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Manage and customize internationalization support in your CMS editor exper
|
||||
keywords: internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. It comes included by default and can be extended in your config.
|
||||
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. Payload's i18n support is built on top of [i18next](https://www.i18next.com). It comes included by default and can be extended in your config.
|
||||
|
||||
While Payload's built-in features come translated, you may want to also translate parts of your project's configuration too. This is possible in places like collections and globals labels and groups, field labels, descriptions and input placeholder text. The admin UI will display all the correct translations you provide based on the user's language.
|
||||
|
||||
@@ -72,7 +72,9 @@ After a user logs in, they can change their language selection in the `/account`
|
||||
|
||||
Payload's backend uses express middleware to set the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language.
|
||||
|
||||
Anywhere in your Payload app that you have access to the `req` object, you can access payload's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
|
||||
Anywhere in your Payload app that you have access to the `req` object, you can access i18next's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
|
||||
|
||||
Read the i18next [API documentation](https://www.i18next.com/overview/api) to learn more.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
@@ -86,8 +88,9 @@ import { buildConfig } from 'payload/config'
|
||||
export default buildConfig({
|
||||
//...
|
||||
i18n: {
|
||||
fallbackLanguage: 'en', // default
|
||||
translations: {
|
||||
fallbackLng: 'en', // default
|
||||
debug: false, // default
|
||||
resources: {
|
||||
en: {
|
||||
custom: {
|
||||
// namespace can be anything you want
|
||||
@@ -104,63 +107,4 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
## Types for custom translations
|
||||
|
||||
In order to use custom translations in your project, you need to provide the types for the translations. Here is an example of how you can define the types for the custom translations in a custom react component:
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
import type { NestedKeysStripped } from '@payloadcms/translations'
|
||||
import type React from 'react'
|
||||
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
|
||||
const customTranslations = {
|
||||
en: {
|
||||
general: {
|
||||
test: 'Custom Translation',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type CustomTranslationObject = typeof customTranslations.en
|
||||
type CustomTranslationKeys = NestedKeysStripped<CustomTranslationObject>
|
||||
|
||||
export const MyComponent: React.FC = () => {
|
||||
const { i18n, t } = useTranslation<CustomTranslationObject, CustomTranslationKeys>() // These generics merge your custom translations with the default client translations
|
||||
|
||||
return t('general:test')
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Additionally, payload exposes the `t` function in various places, for example in labels. Here is how you would type those:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DefaultTranslationKeys,
|
||||
NestedKeysStripped,
|
||||
TFunction,
|
||||
} from '@payloadcms/translations'
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
const customTranslations = {
|
||||
en: {
|
||||
general: {
|
||||
test: 'Custom Translation',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type CustomTranslationObject = typeof customTranslations.en
|
||||
type CustomTranslationKeys = NestedKeysStripped<CustomTranslationObject>
|
||||
|
||||
const field: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
label: (
|
||||
{ t }: { t: TFunction<CustomTranslationKeys | DefaultTranslationKeys> }, // The generic passed to TFunction does not automatically merge the custom translations with the default translations. We need to merge them ourselves here
|
||||
) => t('fields:addLabel'),
|
||||
}
|
||||
```
|
||||
|
||||
See the i18next [configuration options](https://www.i18next.com/overview/configuration-options) to learn more.
|
||||
|
||||
@@ -2,165 +2,166 @@
|
||||
title: Email Functionality
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Payload uses an adapter pattern to enable email functionality. Set up email functions such as password resets, order confirmations and more.
|
||||
desc: Payload uses NodeMailer to allow you to send emails smoothly from your app. Set up email functions such as password resets, order confirmations and more.
|
||||
keywords: email, overview, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
### Introduction
|
||||
|
||||
Payload has a few email adapters that can be imported to enable email functionality. The [@payloadcms/email-nodemailer](https://www.npmjs.com/package/@payloadcms/email-nodemailer) package will be the package most will want to install. This package provides an easy way to use [Nodemailer](https://nodemailer.com) for email and won't get in your way for those already familiar.
|
||||
Payload comes ready to send your application's email. Whether you simply need built-in password reset
|
||||
email to work or you want customers to get an order confirmation email, you're almost there. Payload makes use of
|
||||
[NodeMailer](https://nodemailer.com) for email and won't get in your way for those already familiar.
|
||||
|
||||
The email adapter should be passed into the `email` property of the Payload config. This will allow Payload to send emails for things like password resets, new user verification, and any other email sending needs you may have.
|
||||
For email to send from your Payload server, some configuration is required. The settings you provide will be set
|
||||
in the `email` property object of your payload init call. Payload will make use of the transport that you have configured for it for things like reset password or verifying new user accounts and email send methods are available to you as well on your payload instance.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Default Configuration
|
||||
**Three ways to set it up**
|
||||
|
||||
When email is not needed or desired, Payload will log a warning on startup notifying that email is not configured. A warning message will also be logged on any attempt to send an email.
|
||||
1. **Default**: When email is not needed, a mock email handler will be created and used when nothing is provided. This is ideal for development environments and can be changed later when ready to [go to production](/docs/production/deployment).
|
||||
1. **Recommended**: Set the `transportOptions` and Payload will do the set up for you.
|
||||
1. **Advanced**: The `transport` object can be assigned a nodemailer transport object set up in your server scripts and given for Payload to use.
|
||||
|
||||
#### Email Adapter
|
||||
The following options are configurable in the `email` property object as part of the options object when calling payload.init().
|
||||
|
||||
An email adapter will require at least the following fields:
|
||||
| Option | Description |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`fromName`** \* | The name part of the From field that will be seen on the delivered email |
|
||||
| **`fromAddress`** \* | The email address part of the From field that will be used when delivering email |
|
||||
| **`transport`** | The NodeMailer transport object for when you want to do it yourself, not needed when transportOptions is set |
|
||||
| **`transportOptions`** | An object that configures the transporter that Payload will create. For all the available options see the [NodeMailer documentation](https://nodemailer.com) or see the examples below |
|
||||
| **`logMockCredentials`** | If set to true and no transport/transportOptions, ethereal credentials will be logged to console on startup |
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| **`defaultFromName`** \* | The name part of the From field that will be seen on the delivered email |
|
||||
| **`defaultFromAddress`** \* | The email address part of the From field that will be used when delivering email |
|
||||
|
||||
|
||||
#### Officlal Email Adapters
|
||||
|
||||
| Name | Package | Description |
|
||||
| ---------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Nodemailer | [@payloadcms/email-nodemailer](https://www.npmjs.com/package/@payloadcms/email-nodemailer) | Use any [Nodemailer transport](https://nodemailer.com/transports), including SMTP, Resend, SendGrid, and more. This was provided by default in Payload 2.x. This is the easiest migration path. |
|
||||
| Resend | [@payloadcms/email-resend](https://www.npmjs.com/package/@payloadcms/email-resend) | Resend email via their REST API. This is preferred for serverless platforms such as Vercel because it is much more lightweight than the nodemailer adapter. |
|
||||
|
||||
### Nodemailer Configuration
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`transport`** | The Nodemailer transport object for when you want to do it yourself, not needed when transportOptions is set |
|
||||
| **`transportOptions`** | An object that configures the transporter that Payload will create. For all the available options see the [Nodemailer documentation](https://nodemailer.com) or see the examples below |
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
### Use SMTP
|
||||
|
||||
Simple Mail Transfer Protocol (SMTP) options can be passed in using the `transportOptions` object on the `email` options. See the [Nodemailer SMTP documentation](https://nodemailer.com/smtp/) for more information, including details on when `secure` should and should not be set to `true`.
|
||||
Simple Mail Transfer Protocol (SMTP) options can be passed in using the `transportOptions` object on the `email` options. See the [NodeMailer SMTP documentation](https://nodemailer.com/smtp/) for more information, including details on when `secure` should and should not be set to `true`.
|
||||
|
||||
**Example email options using SMTP:**
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
|
||||
export default buildConfig({
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: 'info@payloadcms.com',
|
||||
defaultFromName: 'Payload',
|
||||
// Nodemailer transportOptions
|
||||
payload.init({
|
||||
email: {
|
||||
transportOptions: {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
port: Number(process.env.SMTP_HOST),
|
||||
secure: Number(process.env.SMTP_PORT) === 465, // true for port 465, false (the default) for 587 and others
|
||||
requireTLS: true,
|
||||
},
|
||||
}),
|
||||
fromName: 'hello',
|
||||
fromAddress: 'hello@example.com',
|
||||
},
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
**Example email options using nodemailer.createTransport:**
|
||||
<Banner type="warning">
|
||||
It is best practice to avoid saving credentials or API keys directly in your code, use
|
||||
[environment variables](/docs/configuration/overview#using-environment-variables-in-your-config).
|
||||
</Banner>
|
||||
|
||||
### Use an email service
|
||||
|
||||
Many third party mail providers are available and offer benefits beyond basic SMTP. As an example, your payload init could look like this if you wanted to use SendGrid.com, though the same approach would work for any other [NodeMailer transports](https://nodemailer.com/transports/) shown here or provided by another third party.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
export default buildConfig({
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: 'info@payloadcms.com',
|
||||
defaultFromName: 'Payload',
|
||||
// Any Nodemailer transport can be used
|
||||
transport: nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
**Custom Transport:**
|
||||
|
||||
You also have the ability to bring your own nodemailer transport. This is an example of using the SendGrid nodemailer transport.
|
||||
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import payload from 'payload'
|
||||
import nodemailerSendgrid from 'nodemailer-sendgrid'
|
||||
|
||||
const sendGridAPIKey = process.env.SENDGRID_API_KEY
|
||||
|
||||
export default buildConfig({
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: 'info@payloadcms.com',
|
||||
defaultFromName: 'Payload',
|
||||
transportOptions: nodemailerSendgrid({
|
||||
apiKey: process.env.SENDGRID_API_KEY,
|
||||
}),
|
||||
}),
|
||||
payload.init({
|
||||
...(sendGridAPIKey
|
||||
? {
|
||||
email: {
|
||||
transportOptions: nodemailerSendgrid({
|
||||
apiKey: sendGridAPIKey,
|
||||
}),
|
||||
fromName: 'Admin',
|
||||
fromAddress: 'admin@example.com',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
```
|
||||
|
||||
During development, if you pass nothing to `nodemailerAdapter`, it will use the [ethereal.email](https://ethereal.email) service.
|
||||
### Use a custom NodeMailer transport
|
||||
|
||||
This will log the ethereal.email details to console on startup.
|
||||
To take full control of the mail transport you may wish to use `nodemailer.createTransport()` on your server and provide it to Payload init.
|
||||
|
||||
```ts
|
||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import payload from 'payload'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
export default buildConfig({
|
||||
email: nodemailerAdapter(),
|
||||
const payload = require('payload')
|
||||
const nodemailer = require('nodemailer')
|
||||
|
||||
const transport = await nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Resend Configuration
|
||||
|
||||
The Resend adapter requires an API key to be passed in the options. This can be found in the Resend dashboard. This is the preferred package if you are deploying on Vercel because this is much more lightweight than the Nodemailer adapter.
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------------------------------- |
|
||||
| apiKey | The API key for the Resend service. |
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { resendAdapter } from '@payloadcms/email-resend'
|
||||
|
||||
export default buildConfig({
|
||||
email: resendAdapter({
|
||||
defaultFromAddress: 'dev@payloadcms.com',
|
||||
defaultFromName: 'Payload CMS',
|
||||
apiKey: process.env.RESEND_API_KEY || '',
|
||||
}),
|
||||
payload.init({
|
||||
email: {
|
||||
fromName: 'Admin',
|
||||
fromAddress: 'admin@example.com',
|
||||
transport,
|
||||
},
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### Sending Mail
|
||||
|
||||
With a working transport you can call it anywhere you have access to payload by calling `payload.sendEmail(message)`. The `message` will contain the `to`, `subject` and `html` or `text` for the email being sent. Other options are also available and can be seen in the sendEmail args. Support for these will depend on the adapter being used.
|
||||
With a working transport you can call it anywhere you have access to payload by calling `payload.sendEmail(message)`. The `message` will contain the `to`, `subject` and `email` or `text` for the email being sent. To see all available message configuration options see [NodeMailer](https://nodemailer.com/message).
|
||||
|
||||
### Mock transport
|
||||
|
||||
By default, Payload uses a mock implementation that only sends mail to the [ethereal](https://ethereal.email) capture service that will never reach a user's inbox. While in development you may wish to make use of the captured messages which is why the payload output during server output helpfully logs this out on the server console.
|
||||
|
||||
To see ethereal credentials, add `logMockCredentials: true` to the email options. This will cause them to be logged to console on startup.
|
||||
|
||||
```ts
|
||||
// Example of sending an email
|
||||
const email = await payload.sendEmail({
|
||||
to: 'test@example.com',
|
||||
subject: 'This is a test email',
|
||||
text: 'This is my message body',
|
||||
payload.init({
|
||||
email: {
|
||||
fromName: 'Admin',
|
||||
fromAddress: 'admin@example.com',
|
||||
logMockCredentials: true, // Optional
|
||||
},
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
**Console output when starting payload with a mock email instance and logMockCredentials: true**
|
||||
|
||||
```
|
||||
[06:37:21] INFO (payload): Starting Payload...
|
||||
[06:37:22] INFO (payload): Payload Demo Initialized
|
||||
[06:37:22] INFO (payload): listening on 3000...
|
||||
[06:37:22] INFO (payload): Connected to MongoDB server successfully!
|
||||
[06:37:23] INFO (payload): E-mail configured with mock configuration
|
||||
[06:37:23] INFO (payload): Log into mock email provider at https://ethereal.email
|
||||
[06:37:23] INFO (payload): Mock email account username: hhav5jw7doo4euev@ethereal.email
|
||||
[06:37:23] INFO (payload): Mock email account password: VNdGcvDZeyEhtuPBqf
|
||||
```
|
||||
|
||||
The mock email handler is used when payload is started with neither `transport` or `transportOptions` to know how to deliver email.
|
||||
|
||||
<Banner type="warning">
|
||||
The randomly generated email account username and password will be different each time the Payload
|
||||
server starts.
|
||||
</Banner>
|
||||
|
||||
### Using multiple mail providers
|
||||
|
||||
Payload supports the use of a single transporter of email, but there is nothing stopping you from having more. Consider a use case where sending bulk email is handled differently than transactional email and could be done using a [hook](/docs/hooks/overview).
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
---
|
||||
title: Storage Adapters
|
||||
label: Overview
|
||||
order: 20
|
||||
desc: Payload provides additional storage adapters to handle file uploads. These adapters allow you to store files in different locations, such as Amazon S3, Vercel Blob Storage, Google Cloud Storage, Uploadthing, and more.
|
||||
keywords: uploads, images, media, storage, adapters, s3, vercel, google cloud, azure
|
||||
---
|
||||
|
||||
Payload offers additional storage adapters to handle file uploads. These adapters allow you to store files in different locations, such as Amazon S3, Vercel Blob Storage, Google Cloud Storage, and more.
|
||||
|
||||
| Service | Package |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| Vercel Blob | [`@payloadcms/storage-vercel-blob`](https://github.com/payloadcms/payload/tree/beta/packages/storage-vercel-blob) |
|
||||
| AWS S3 | [`@payloadcms/storage-s3`](https://github.com/payloadcms/payload/tree/beta/packages/storage-s3) |
|
||||
| Azure | [`@payloadcms/storage-azure`](https://github.com/payloadcms/payload/tree/beta/packages/storage-azure) |
|
||||
| Google Cloud Storage | [`@payloadcms/storage-gcs`](https://github.com/payloadcms/payload/tree/beta/packages/storage-gcs) |
|
||||
|
||||
|
||||
### Vercel Blob Storage [`@payloadcms/storage-vercel-blob`](https://www.npmjs.com/package/@payloadcms/storage-vercel-blob)
|
||||
|
||||
#### Installation
|
||||
|
||||
```sh
|
||||
pnpm add @payloadcms/storage-vercel-blob
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
|
||||
```ts
|
||||
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
|
||||
import { Media } from './collections/Media'
|
||||
import { MediaWithPrefix } from './collections/MediaWithPrefix'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Media, MediaWithPrefix],
|
||||
plugins: [
|
||||
vercelBlobStorage({
|
||||
enabled: true, // Optional, defaults to true
|
||||
// Specify which collections should use Vercel Blob
|
||||
collections: {
|
||||
[Media.slug]: true,
|
||||
[MediaWithPrefix.slug]: {
|
||||
prefix: 'my-prefix',
|
||||
},
|
||||
},
|
||||
// Token provided by Vercel once Blob storage is added to your Vercel project
|
||||
token: process.env.BLOB_READ_WRITE_TOKEN,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| -------------------- | -------------------------------------------------------------------- | ----------------------------- |
|
||||
| `enabled` | Whether or not to enable the plugin | `true` |
|
||||
| `collections` | Collections to apply the Vercel Blob adapter to | |
|
||||
| `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` |
|
||||
| `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) |
|
||||
| `token` | Vercel Blob storage read/write token | `''` |
|
||||
|
||||
### S3 Storage [`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3)
|
||||
|
||||
#### Installation
|
||||
|
||||
```sh
|
||||
pnpm add @payloadcms/storage-s3
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
|
||||
```ts
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
import { Media } from './collections/Media'
|
||||
import { MediaWithPrefix } from './collections/MediaWithPrefix'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Media, MediaWithPrefix],
|
||||
plugins: [
|
||||
s3Storage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
[mediaWithPrefixSlug]: {
|
||||
prefix,
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET,
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
region: process.env.S3_REGION,
|
||||
// ... Other S3 configuration
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
##### Configuration Options
|
||||
|
||||
See the the [AWS SDK Package](https://github.com/aws/aws-sdk-js-v3) and [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object for guidance on AWS S3 configuration.
|
||||
|
||||
### Azure Blob Storage - [`@payloadcms/storage-azure`](https://www.npmjs.com/package/@payloadcms/storage-azure)
|
||||
|
||||
#### Installation
|
||||
|
||||
```sh
|
||||
pnpm add @payloadcms/storage-azure
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
|
||||
```ts
|
||||
import { azureStorage } from '@payloadcms/storage-azure'
|
||||
import { Media } from './collections/Media'
|
||||
import { MediaWithPrefix } from './collections/MediaWithPrefix'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Media, MediaWithPrefix],
|
||||
plugins: [
|
||||
azureStorage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
[mediaWithPrefixSlug]: {
|
||||
prefix,
|
||||
},
|
||||
},
|
||||
allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
|
||||
baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL,
|
||||
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||
containerName: process.env.AZURE_STORAGE_CONTAINER_NAME,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| ---------------------- | ------------------------------------------------------------------------ | ------- |
|
||||
| `enabled` | Whether or not to enable the plugin | `true` |
|
||||
| `collections` | Collections to apply the Azure Blob adapter to | |
|
||||
| `allowContainerCreate` | Whether or not to allow the container to be created if it does not exist | `false` |
|
||||
| `baseURL` | Base URL for the Azure Blob storage account | |
|
||||
| `connectionString` | Azure Blob storage connection string | |
|
||||
| `containerName` | Azure Blob storage container name | |
|
||||
|
||||
### Google Cloud Storage [`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs)
|
||||
|
||||
#### Installation
|
||||
|
||||
```sh
|
||||
pnpm add @payloadcms/storage-gcs
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
|
||||
```ts
|
||||
import { gcsStorage } from '@payloadcms/storage-gcs'
|
||||
import { Media } from './collections/Media'
|
||||
import { MediaWithPrefix } from './collections/MediaWithPrefix'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Media, MediaWithPrefix],
|
||||
plugins: [
|
||||
gcsStorage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
[mediaWithPrefixSlug]: {
|
||||
prefix,
|
||||
},
|
||||
},
|
||||
bucket: process.env.GCS_BUCKET,
|
||||
options: {
|
||||
apiEndpoint: process.env.GCS_ENDPOINT,
|
||||
projectId: process.env.GCS_PROJECT_ID,
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------- | --------- |
|
||||
| `enabled` | Whether or not to enable the plugin | `true` |
|
||||
| `collections` | Collections to apply the storage to | |
|
||||
| `bucket` | The name of the bucket to use | |
|
||||
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
|
||||
| `acl` | Access control list for files that are uploaded | `Private` |
|
||||
|
||||
|
||||
### Uploadthing Storage [`@payloadcms/storage-uploadthing`](https://www.npmjs.com/package/@payloadcms/storage-uploadthing)
|
||||
|
||||
#### Installation
|
||||
|
||||
```sh
|
||||
pnpm add @paylaodcms/storage-uploadthing
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
|
||||
- Get an API key from Uploadthing and set it as `apiKey` in the `options` object.
|
||||
- `acl` is optional and defaults to `public-read`.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
collections: [Media],
|
||||
plugins: [
|
||||
uploadthingStorage({
|
||||
collections: {
|
||||
[mediaSlug]: true,
|
||||
},
|
||||
options: {
|
||||
apiKey: process.env.UPLOADTHING_SECRET,
|
||||
acl: 'public-read',
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| ---------------- | ----------------------------------------------- | ------------- |
|
||||
| `apiKey` | API key from Uploadthing. Required. | |
|
||||
| `acl` | Access control list for files that are uploaded | `public-read` |
|
||||
| `logLevel` | Log level for Uploadthing | `info` |
|
||||
| `fetch` | Custom fetch function | `fetch` |
|
||||
| `defaultKeyType` | Default key type for file operations | `fileKey` |
|
||||
|
||||
|
||||
### Custom Storage Adapters
|
||||
|
||||
If you need to create a custom storage adapter, you can use the [`@payloadcms/plugin-cloud-storage`](https://www.npmjs.com/package/@payloadcms/plugin-cloud-storage) package. This package is used internally by the storage adapters mentioned above.
|
||||
|
||||
#### Installation
|
||||
|
||||
`pnpm add @payloadcms/plugin-cloud-storage`
|
||||
|
||||
#### Usage
|
||||
|
||||
Reference any of the existing storage adapters for guidance on how this should be structured. Create an adapter following the `GeneratedAdapter` interface. Then, pass the adapter to the `cloudStorage` plugin.
|
||||
|
||||
```ts
|
||||
export interface GeneratedAdapter {
|
||||
/**
|
||||
* Additional fields to be injected into the base collection and image sizes
|
||||
*/
|
||||
fields?: Field[]
|
||||
/**
|
||||
* Generates the public URL for a file
|
||||
*/
|
||||
generateURL?: GenerateURL
|
||||
handleDelete: HandleDelete
|
||||
handleUpload: HandleUpload
|
||||
name: string
|
||||
onInit?: () => void
|
||||
staticHandler: StaticHandler
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
|
||||
|
||||
export default buildConfig({
|
||||
plugins: [
|
||||
cloudStorage({
|
||||
collections: {
|
||||
'my-collection-slug': {
|
||||
adapter: theAdapterToUse, // see docs for the adapter you want to use
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
// The rest of your config goes here
|
||||
})
|
||||
```
|
||||
|
||||
### Plugin options
|
||||
|
||||
This plugin is configurable to work across many different Payload collections. A `*` denotes that the property is required.
|
||||
|
||||
| Option | Type | Description |
|
||||
| ---------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collections` \* | `Record<string, CollectionOptions>` | Object with keys set to the slug of collections you want to enable the plugin for, and values set to collection-specific options. |
|
||||
| `enabled` | | `boolean` to conditionally enable/disable plugin. Default: true. |
|
||||
|
||||
### Collection-specific options
|
||||
|
||||
| Option | Type | Description |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `adapter` \* | [Adapter](https://github.com/payloadcms/plugin-cloud-storage/blob/master/src/types.ts#L51) | Pass in the adapter that you'd like to use for this collection. You can also set this field to `null` for local development if you'd like to bypass cloud storage in certain scenarios and use local storage. |
|
||||
| `disableLocalStorage` | `boolean` | Choose to disable local storage on this collection. Defaults to `true`. |
|
||||
| `disablePayloadAccessControl` | `true` | Set to `true` to disable Payload's access control. [More](#payload-access-control) |
|
||||
| `prefix` | `string` | Set to `media/images` to upload files inside `media/images` folder in the bucket. |
|
||||
| `generateFileURL` | [GenerateFileURL](https://github.com/payloadcms/plugin-cloud-storage/blob/master/src/types.ts#L53) | Override the generated file URL with one that you create. |
|
||||
|
||||
### Payload Access Control
|
||||
|
||||
Payload ships with access control that runs _even on statically served files_. The same `read` access control property on your `upload`-enabled collections is used, and it allows you to restrict who can request your uploaded files.
|
||||
|
||||
To preserve this feature, by default, this plugin _keeps all file URLs exactly the same_. Your file URLs won't be updated to point directly to your cloud storage source, as in that case, Payload's access control will be completely bypassed and you would need public readability on your cloud-hosted files.
|
||||
|
||||
Instead, all uploads will still be reached from the default `/collectionSlug/staticURL/filename` path. This plugin will "pass through" all files that are hosted on your third-party cloud service—with the added benefit of keeping your existing access control in place.
|
||||
|
||||
If this does not apply to you (your upload collection has `read: () => true` or similar) you can disable this functionality by setting `disablePayloadAccessControl` to `true`. When this setting is in place, this plugin will update your file URLs to point directly to your cloud host.
|
||||
|
||||
### Conditionally Enabling/Disabling
|
||||
|
||||
The proper way to conditionally enable/disable this plugin is to use the `enabled` property.
|
||||
|
||||
```ts
|
||||
cloudStoragePlugin({
|
||||
enabled: process.env.MY_CONDITION === 'true',
|
||||
collections: {
|
||||
'my-collection-slug': {
|
||||
adapter: theAdapterToUse, // see docs for the adapter you want to use
|
||||
},
|
||||
},
|
||||
}),
|
||||
```
|
||||
@@ -9,7 +9,7 @@ const withBundleAnalyzer = bundleAnalyzer({
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default withBundleAnalyzer(
|
||||
withPayload({
|
||||
reactStrictMode: false,
|
||||
// reactStrictMode: false,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -54,6 +54,7 @@
|
||||
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
|
||||
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
|
||||
"fix": "eslint \"packages/**/*.ts\" --fix",
|
||||
"generate:types": "PAYLOAD_CONFIG_PATH=./test/_community/config.ts node --no-deprecation ./packages/payload/bin.js generate:types",
|
||||
"lint": "eslint \"packages/**/*.ts\"",
|
||||
"lint-staged": "lint-staged",
|
||||
"obliterate-playwright-cache": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
@@ -132,7 +133,7 @@
|
||||
"lint-staged": "^14.0.1",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^9.0",
|
||||
"next": "^14.3.0-canary.7",
|
||||
"next": "^14.3.0-canary.37",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
"nodemon": "3.0.3",
|
||||
"open": "^10.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -55,7 +55,6 @@
|
||||
"@types/esprima": "^4.0.6",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/node": "20.12.5",
|
||||
"temp-dir": "2.0.0"
|
||||
"@types/node": "20.12.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,25 @@ import fse from 'fs-extra'
|
||||
import path from 'path'
|
||||
import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
|
||||
import { createProject } from './create-project.js'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dbReplacements } from './packages.js'
|
||||
import { getValidTemplates } from './templates.js'
|
||||
import globby from 'globby'
|
||||
|
||||
import tempDirectory from 'temp-dir'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const projectDir = path.resolve(dirname, './tmp')
|
||||
describe('createProject', () => {
|
||||
let projectDir: string
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
projectDir = `${tempDirectory}/${Math.random().toString(36).substring(7)}`
|
||||
if (fse.existsSync(projectDir)) {
|
||||
fse.rmdirSync(projectDir, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (fse.existsSync(projectDir)) {
|
||||
fse.rmSync(projectDir, { recursive: true })
|
||||
@@ -97,9 +100,6 @@ describe('createProject', () => {
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
const packageJson = fse.readJsonSync(packageJsonPath)
|
||||
|
||||
// Verify git was initialized
|
||||
expect(fse.existsSync(path.resolve(projectDir, '.git'))).toBe(true)
|
||||
|
||||
// Should only have one db adapter
|
||||
expect(
|
||||
Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')),
|
||||
|
||||
@@ -8,7 +8,6 @@ import path from 'path'
|
||||
|
||||
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types.js'
|
||||
|
||||
import { tryInitRepoAndCommit } from '../utils/git.js'
|
||||
import { debug, error, warning } from '../utils/log.js'
|
||||
import { configurePayloadConfig } from './configure-payload-config.js'
|
||||
|
||||
@@ -109,10 +108,6 @@ export async function createProject(args: {
|
||||
} else {
|
||||
spinner.stop('Dependency installation skipped')
|
||||
}
|
||||
|
||||
if (!cliArgs['--no-git']) {
|
||||
tryInitRepoAndCommit({ cwd: projectDir })
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePackageJSON(args: {
|
||||
|
||||
@@ -53,9 +53,6 @@ export class Main {
|
||||
'--use-pnpm': Boolean,
|
||||
'--use-yarn': Boolean,
|
||||
|
||||
// Other
|
||||
'--no-git': Boolean,
|
||||
|
||||
// Flags
|
||||
'--beta': Boolean,
|
||||
'--debug': Boolean,
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface Args extends arg.Spec {
|
||||
'--local-template': StringConstructor
|
||||
'--name': StringConstructor
|
||||
'--no-deps': BooleanConstructor
|
||||
'--no-git': BooleanConstructor
|
||||
'--secret': StringConstructor
|
||||
'--template': StringConstructor
|
||||
'--template-branch': StringConstructor
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { ExecSyncOptions } from 'child_process'
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
import { warning } from './log.js'
|
||||
|
||||
export function tryInitRepoAndCommit(args: { cwd: string }): void {
|
||||
const execOpts: ExecSyncOptions = { cwd: args.cwd, stdio: 'ignore' }
|
||||
try {
|
||||
// Check if git is available
|
||||
execSync('git -v', execOpts)
|
||||
|
||||
// Do nothing if already in a git repo
|
||||
if (isGitRepo(execOpts)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize
|
||||
execSync('git init', execOpts)
|
||||
if (!ensureHasDefaultBranch(execOpts)) {
|
||||
execSync('git checkout -b main', execOpts)
|
||||
}
|
||||
|
||||
// Add and commit files
|
||||
execSync('git add -A', execOpts)
|
||||
execSync('git commit -m "feat: initial commit"', execOpts)
|
||||
} catch (_) {
|
||||
warning('Failed to initialize git repository.')
|
||||
}
|
||||
}
|
||||
|
||||
function isGitRepo(opts: ExecSyncOptions): boolean {
|
||||
try {
|
||||
execSync('git rev-parse --is-inside-work-tree', opts)
|
||||
return true
|
||||
} catch (_) {
|
||||
// Ignore errors
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function ensureHasDefaultBranch(opts: ExecSyncOptions): boolean {
|
||||
try {
|
||||
execSync(`git config init.defaultBranch`, opts)
|
||||
return true
|
||||
} catch (_) {
|
||||
// Ignore errros
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -61,12 +61,11 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.pool) {
|
||||
this.pool = new pg.Pool(this.poolOptions)
|
||||
await connectWithReconnect({ adapter: this, payload: this.payload })
|
||||
}
|
||||
this.pool = new pg.Pool(this.poolOptions)
|
||||
await connectWithReconnect({ adapter: this, payload: this.payload })
|
||||
|
||||
const logger = this.logger || false
|
||||
|
||||
this.drizzle = drizzle(this.pool, { logger, schema: this.schema })
|
||||
|
||||
if (!hotReload) {
|
||||
@@ -83,18 +82,16 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
|
||||
if (typeof this.rejectInitializing === 'function') this.rejectInitializing()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Only push schema if not in production
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_MIGRATING !== 'true' &&
|
||||
this.push !== false
|
||||
) {
|
||||
await pushDevSchema(this)
|
||||
}
|
||||
process.env.NODE_ENV === 'production' ||
|
||||
process.env.PAYLOAD_MIGRATING === 'true' ||
|
||||
this.push === false
|
||||
)
|
||||
return
|
||||
|
||||
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
|
||||
await pushDevSchema(this)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,4 @@ export const destroy: Destroy = async function destroy(this: PostgresAdapter) {
|
||||
this.relations = {}
|
||||
this.fieldConstraints = {}
|
||||
this.drizzle = undefined
|
||||
this.initializing = new Promise((res, rej) => {
|
||||
this.resolveInitializing = res
|
||||
this.rejectInitializing = rej
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,13 +49,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
|
||||
function adapter({ payload }: { payload: Payload }) {
|
||||
const migrationDir = findMigrationDir(args.migrationDir)
|
||||
let resolveInitializing
|
||||
let rejectInitializing
|
||||
|
||||
const initializing = new Promise<void>((res, rej) => {
|
||||
resolveInitializing = res
|
||||
rejectInitializing = rej
|
||||
})
|
||||
|
||||
return createDatabaseAdapter<PostgresAdapter>({
|
||||
name: 'postgres',
|
||||
@@ -63,7 +56,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
enums: {},
|
||||
fieldConstraints: {},
|
||||
idType: postgresIDType,
|
||||
initializing,
|
||||
localesSuffix: args.localesSuffix || '_locales',
|
||||
logger: args.logger,
|
||||
pgSchema: undefined,
|
||||
@@ -109,8 +101,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
migrationDir,
|
||||
payload,
|
||||
queryDrafts,
|
||||
rejectInitializing,
|
||||
resolveInitializing,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
|
||||
@@ -17,11 +17,6 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
|
||||
|
||||
let transactionReady: () => void
|
||||
|
||||
// Await initialization here
|
||||
// Prevent race conditions where the adapter may be
|
||||
// re-initializing, and `this.drizzle` is potentially undefined
|
||||
await this.initializing
|
||||
|
||||
// Drizzle only exposes a transactions API that is sufficient if you
|
||||
// can directly pass around the `tx` argument. But our operations are spread
|
||||
// over many files and we don't want to pass the `tx` around like that,
|
||||
|
||||
@@ -69,17 +69,14 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
*/
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
idType: Args['idType']
|
||||
initializing: Promise<void>
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
resolveInitializing: () => void
|
||||
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
|
||||
schemaName?: Args['schemaName']
|
||||
sessions: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { bin } from './dist/bin/index.js'
|
||||
|
||||
bin()
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -13,18 +13,10 @@
|
||||
"import": "./src/index.ts",
|
||||
"require": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./src/exports/*.ts",
|
||||
"require": "./src/exports/*.ts",
|
||||
"types": "./src/exports/*.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.d.ts",
|
||||
"bin": {
|
||||
"payload-graphql": "bin.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@@ -56,11 +48,6 @@
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./dist/exports/*.js",
|
||||
"require": "./dist/exports/*.js",
|
||||
"types": "./dist/exports/*.d.ts"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import fs from 'fs'
|
||||
import { printSchema } from 'graphql'
|
||||
|
||||
import { configToSchema } from '../index.js'
|
||||
export function generateSchema(config: SanitizedConfig): void {
|
||||
const outputFile = process.env.PAYLOAD_GRAPHQL_SCHEMA_PATH || config.graphQL.schemaOutputFile
|
||||
|
||||
const { schema } = configToSchema(config)
|
||||
|
||||
fs.writeFileSync(outputFile, printSchema(schema))
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
import minimist from 'minimist'
|
||||
import { findConfig, importConfig, loadEnv } from 'payload/node'
|
||||
|
||||
import { generateSchema } from './generateSchema.js'
|
||||
|
||||
export const bin = async () => {
|
||||
loadEnv()
|
||||
const configPath = findConfig()
|
||||
const config = await importConfig(configPath)
|
||||
|
||||
const args = minimist(process.argv.slice(2))
|
||||
const script = (typeof args._[0] === 'string' ? args._[0] : '').toLowerCase()
|
||||
|
||||
if (script === 'generate:schema') {
|
||||
return generateSchema(config)
|
||||
}
|
||||
|
||||
console.log(`Unknown script: "${script}".`)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { generateSchema } from '../bin/generateSchema.js'
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
|
||||
import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { rtlLanguages } from '@payloadcms/translations'
|
||||
@@ -6,7 +6,6 @@ import { initI18n } from '@payloadcms/translations'
|
||||
import { RootProvider } from '@payloadcms/ui/providers/Root'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import { Merriweather } from 'next/font/google'
|
||||
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { parseCookies } from 'payload/auth'
|
||||
import { createClientConfig } from 'payload/config'
|
||||
@@ -18,6 +17,13 @@ import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
|
||||
import { DefaultEditView } from '../../views/Edit/Default/index.js'
|
||||
import { DefaultListView } from '../../views/List/Default/index.js'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
title: 'Next.js',
|
||||
}
|
||||
|
||||
import { Merriweather } from 'next/font/google'
|
||||
|
||||
const merriweather = Merriweather({
|
||||
display: 'swap',
|
||||
style: ['normal', 'italic'],
|
||||
@@ -26,11 +32,6 @@ const merriweather = Merriweather({
|
||||
weight: ['400', '900'],
|
||||
})
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
title: 'Next.js',
|
||||
}
|
||||
|
||||
export const RootLayout = async ({
|
||||
children,
|
||||
config: configPromise,
|
||||
@@ -50,11 +51,7 @@ export const RootLayout = async ({
|
||||
})
|
||||
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const i18n: I18nClient = await initI18n({
|
||||
config: config.i18n,
|
||||
context: 'client',
|
||||
language: languageCode,
|
||||
})
|
||||
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
|
||||
const clientConfig = await createClientConfig({ config, t: i18n.t })
|
||||
|
||||
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||
|
||||
@@ -36,6 +36,19 @@ export const buildFormState = async ({ req }: { req: PayloadRequestWithData }) =
|
||||
|
||||
try {
|
||||
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
|
||||
|
||||
if (!reqData) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'No options provided',
|
||||
},
|
||||
{
|
||||
headers,
|
||||
status: httpStatus.BAD_REQUEST,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData
|
||||
|
||||
const incomingUserSlug = req.user?.collection
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { FieldSchemaMap } from './types.js'
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
fields: Field[]
|
||||
i18n: I18n<any, any>
|
||||
i18n: I18n
|
||||
schemaMap: FieldSchemaMap
|
||||
schemaPath: string
|
||||
validRelationships: string[]
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cookies, headers } from 'next/headers.js'
|
||||
import { getRequestLanguage } from './getRequestLanguage.js'
|
||||
|
||||
/**
|
||||
* In the context of Next.js, this function initializes the i18n object for the current request.
|
||||
* In the context of NextJS, this function initializes the i18n object for the current request.
|
||||
*
|
||||
* It must be called on the server side, and within the lifecycle of a request since it relies on the request headers and cookies.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { InitPageResult, PayloadRequestWithData, VisibleEntities } from 'payload/types'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
@@ -41,7 +40,7 @@ export const initPage = async ({
|
||||
const cookies = parseCookies(headers)
|
||||
const language = getRequestLanguage({ config: payload.config, cookies, headers })
|
||||
|
||||
const i18n: I18nClient = await initI18n({
|
||||
const i18n = await initI18n({
|
||||
config: i18nConfig,
|
||||
context: 'client',
|
||||
language,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { payloadFaviconDark, payloadFaviconLight, payloadOgImage } from '@payloadcms/ui/assets'
|
||||
import { payloadFavicon, payloadOgImage } from '@payloadcms/ui/assets'
|
||||
|
||||
export const meta = async (args: {
|
||||
config: SanitizedConfig
|
||||
@@ -13,36 +12,18 @@ export const meta = async (args: {
|
||||
const { config, description = '', keywords = 'CMS, Admin, Dashboard', title } = args
|
||||
|
||||
const titleSuffix = config.admin.meta?.titleSuffix ?? '- Payload'
|
||||
|
||||
const favicon = config?.admin?.meta?.favicon ?? payloadFavicon?.src
|
||||
const ogImage = config.admin?.meta?.ogImage ?? payloadOgImage?.src
|
||||
|
||||
const customIcons = config.admin.meta.icons as Metadata['icons']
|
||||
|
||||
let icons = customIcons ?? []
|
||||
|
||||
const payloadIcons: Icon[] = [
|
||||
{
|
||||
type: 'image/png',
|
||||
rel: 'icon',
|
||||
sizes: '32x32',
|
||||
url: payloadFaviconDark?.src,
|
||||
},
|
||||
{
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
rel: 'icon',
|
||||
sizes: '32x32',
|
||||
url: payloadFaviconLight?.src,
|
||||
},
|
||||
]
|
||||
|
||||
if (customIcons && typeof customIcons === 'object' && Array.isArray(customIcons)) {
|
||||
icons = payloadIcons.concat(customIcons)
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
return {
|
||||
description,
|
||||
icons,
|
||||
icons: [
|
||||
{
|
||||
type: 'image/svg',
|
||||
rel: 'icon',
|
||||
url: favicon,
|
||||
},
|
||||
],
|
||||
keywords,
|
||||
metadataBase: new URL(
|
||||
config?.serverURL ||
|
||||
@@ -63,5 +44,5 @@ export const meta = async (args: {
|
||||
title: `${title} ${titleSuffix}`,
|
||||
},
|
||||
title: `${title} ${titleSuffix}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,9 @@ export const RenderJSON = ({
|
||||
const objectKeys = object ? Object.keys(object) : []
|
||||
const objectLength = objectKeys.length
|
||||
const [isOpen, setIsOpen] = React.useState<boolean>(true)
|
||||
const isNestedAndEmpty = isEmpty && (parentType === 'object' || parentType === 'array')
|
||||
|
||||
return (
|
||||
<li className={isNestedAndEmpty ? `${baseClass}__row-line--nested` : ''}>
|
||||
<li>
|
||||
<button
|
||||
aria-label="toggle"
|
||||
className={`${baseClass}__list-toggle ${isEmpty ? `${baseClass}__list-toggle--empty` : ''}`}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const APIViewClient: React.FC = () => {
|
||||
const { id, collectionSlug, globalSlug, initialData } = useDocumentInfo()
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { i18n } = useTranslation()
|
||||
const { code } = useLocale()
|
||||
|
||||
const { getComponentMap } = useComponentMap()
|
||||
@@ -160,14 +160,14 @@ export const APIViewClient: React.FC = () => {
|
||||
<div className={`${baseClass}__filter-query-checkboxes`}>
|
||||
{draftsEnabled && (
|
||||
<Checkbox
|
||||
label={t('version:draft')}
|
||||
label="Draft"
|
||||
name="draft"
|
||||
onChange={() => setDraft(!draft)}
|
||||
path="draft"
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
label={t('authentication:authenticated')}
|
||||
label="Authenticated"
|
||||
name="authenticated"
|
||||
onChange={() => setAuthenticated(!authenticated)}
|
||||
path="authenticated"
|
||||
@@ -175,7 +175,7 @@ export const APIViewClient: React.FC = () => {
|
||||
</div>
|
||||
{localeOptions && (
|
||||
<Select
|
||||
label={t('general:locale')}
|
||||
label="Locale"
|
||||
name="locale"
|
||||
onChange={(value) => setLocale(value)}
|
||||
options={localeOptions}
|
||||
@@ -183,11 +183,11 @@ export const APIViewClient: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<NumberInput
|
||||
label={t('general:depth')}
|
||||
label="Depth"
|
||||
max={10}
|
||||
min={0}
|
||||
name="depth"
|
||||
onChange={(value) => setDepth(value?.toString())}
|
||||
onChange={(value) => setDepth(value.toString())}
|
||||
path="depth"
|
||||
step={1}
|
||||
/>
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({ config }) =>
|
||||
meta({
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({ config }) => {
|
||||
return meta({
|
||||
config,
|
||||
description: 'API',
|
||||
keywords: 'API',
|
||||
title: 'API',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, sear
|
||||
}
|
||||
DefaultComponent={EditView}
|
||||
componentProps={viewComponentProps}
|
||||
payload={payload}
|
||||
/>
|
||||
</FormQueryParamsProvider>
|
||||
</DocumentInfoProvider>
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateAccountMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateAccountMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) => {
|
||||
return meta({
|
||||
config,
|
||||
description: `${t('authentication:accountOfCurrentUser')}`,
|
||||
keywords: `${t('authentication:account')}`,
|
||||
title: t('authentication:account'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import { meta } from '../../utilities/meta.js'
|
||||
export const generateCreateFirstUserMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}) =>
|
||||
meta({
|
||||
}) => {
|
||||
return meta({
|
||||
config,
|
||||
description: t('authentication:createFirstUser'),
|
||||
keywords: t('general:create'),
|
||||
title: t('authentication:createFirstUser'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Permissions } from 'payload/auth'
|
||||
import type { Payload, SanitizedConfig, VisibleEntities } from 'payload/types'
|
||||
import type { SanitizedConfig, VisibleEntities } from 'payload/types'
|
||||
|
||||
import { Gutter } from '@payloadcms/ui/elements/Gutter'
|
||||
import { SetStepNav } from '@payloadcms/ui/elements/StepNav'
|
||||
import { WithServerSideProps } from '@payloadcms/ui/elements/WithServerSideProps'
|
||||
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
||||
import React from 'react'
|
||||
|
||||
@@ -15,7 +14,6 @@ const baseClass = 'dashboard'
|
||||
export type DashboardProps = {
|
||||
Link: React.ComponentType<any>
|
||||
config: SanitizedConfig
|
||||
payload: Payload
|
||||
permissions: Permissions
|
||||
visibleEntities: VisibleEntities
|
||||
}
|
||||
@@ -28,36 +26,24 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
components: { afterDashboard, beforeDashboard },
|
||||
},
|
||||
},
|
||||
payload,
|
||||
permissions,
|
||||
visibleEntities,
|
||||
} = props
|
||||
|
||||
const BeforeDashboards = Array.isArray(beforeDashboard)
|
||||
? beforeDashboard.map((Component, i) => (
|
||||
<WithServerSideProps Component={Component} key={i} payload={payload} />
|
||||
))
|
||||
: null
|
||||
|
||||
const AfterDashboards = Array.isArray(afterDashboard)
|
||||
? afterDashboard.map((Component, i) => (
|
||||
<WithServerSideProps Component={Component} key={i} payload={payload} />
|
||||
))
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SetStepNav nav={[]} />
|
||||
<SetViewActions actions={[]} />
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
{Array.isArray(BeforeDashboards) && BeforeDashboards.map((Component) => Component)}
|
||||
|
||||
{Array.isArray(beforeDashboard) &&
|
||||
beforeDashboard.map((Component, i) => <Component key={i} />)}
|
||||
<DefaultDashboardClient
|
||||
Link={Link}
|
||||
permissions={permissions}
|
||||
visibleEntities={visibleEntities}
|
||||
/>
|
||||
{Array.isArray(AfterDashboards) && AfterDashboards.map((Component) => Component)}
|
||||
{Array.isArray(afterDashboard) &&
|
||||
afterDashboard.map((Component, i) => <Component key={i} />)}
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
|
||||
permissions,
|
||||
req: {
|
||||
payload: { config },
|
||||
payload,
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
@@ -26,7 +25,7 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
|
||||
|
||||
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
|
||||
|
||||
const viewComponentProps: Omit<DashboardProps, 'payload'> = {
|
||||
const viewComponentProps: DashboardProps = {
|
||||
Link,
|
||||
config,
|
||||
permissions,
|
||||
@@ -42,7 +41,6 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
|
||||
}
|
||||
DefaultComponent={DefaultDashboard}
|
||||
componentProps={viewComponentProps}
|
||||
payload={payload}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateDashboardMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateDashboardMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) => {
|
||||
return meta({
|
||||
config,
|
||||
description: `${t('general:dashboard')} Payload`,
|
||||
keywords: `${t('general:dashboard')}, Payload`,
|
||||
title: t('general:dashboard'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -220,7 +220,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
CustomComponent={ViewOverride || CustomView}
|
||||
DefaultComponent={DefaultView}
|
||||
componentProps={viewComponentProps}
|
||||
payload={payload}
|
||||
/>
|
||||
)}
|
||||
</FormQueryParamsProvider>
|
||||
|
||||
@@ -5,10 +5,11 @@ import { meta } from '../../utilities/meta.js'
|
||||
export const generateForgotPasswordMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}) =>
|
||||
meta({
|
||||
}) => {
|
||||
return meta({
|
||||
config,
|
||||
description: t('authentication:forgotPassword'),
|
||||
keywords: t('authentication:forgotPassword'),
|
||||
title: t('authentication:forgotPassword'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
|
||||
CustomComponent={CustomListView}
|
||||
DefaultComponent={DefaultListView}
|
||||
componentProps={viewComponentProps}
|
||||
payload={payload}
|
||||
/>
|
||||
</TableColumnsProvider>
|
||||
</ListQueryProvider>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, searchPara
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={`${loginBaseClass}__brand`}>
|
||||
<Logo payload={payload} />
|
||||
<Logo config={config} />
|
||||
</div>
|
||||
{Array.isArray(BeforeLogins) && BeforeLogins.map((Component) => Component)}
|
||||
{!collectionConfig?.auth?.disableLocalStrategy && <LoginForm searchParams={searchParams} />}
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateLoginMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateLoginMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) => {
|
||||
return meta({
|
||||
config,
|
||||
description: `${t('authentication:login')}`,
|
||||
keywords: `${t('authentication:login')}`,
|
||||
title: t('authentication:login'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateLogoutMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateLogoutMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) => {
|
||||
return meta({
|
||||
config,
|
||||
description: `${t('authentication:logoutUser')}`,
|
||||
keywords: `${t('authentication:logout')}`,
|
||||
title: t('authentication:logout'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const NotFoundPage = async ({
|
||||
<Fragment>
|
||||
<HydrateClientUser permissions={initPageResult.permissions} user={initPageResult.req.user} />
|
||||
<DefaultTemplate
|
||||
payload={initPageResult.req.payload}
|
||||
config={initPageResult.req.payload.config}
|
||||
visibleEntities={initPageResult.visibleEntities}
|
||||
>
|
||||
<NotFoundClient />
|
||||
|
||||
@@ -10,10 +10,11 @@ export const generateNotFoundMeta = ({
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
}): Promise<Metadata> =>
|
||||
meta({
|
||||
}): Promise<Metadata> => {
|
||||
return meta({
|
||||
config,
|
||||
description: i18n.t('general:pageNotFound'),
|
||||
keywords: `404 ${i18n.t('general:notFound')}`,
|
||||
title: i18n.t('general:notFound'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ import { meta } from '../../utilities/meta.js'
|
||||
export const generateResetPasswordMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}): Promise<Metadata> =>
|
||||
meta({
|
||||
}): Promise<Metadata> => {
|
||||
return meta({
|
||||
config,
|
||||
description: t('authentication:resetPassword'),
|
||||
keywords: t('authentication:resetPassword'),
|
||||
title: t('authentication:resetPassword'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,10 +87,7 @@ export const RootPage = async ({
|
||||
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
|
||||
)}
|
||||
{templateType === 'default' && (
|
||||
<DefaultTemplate
|
||||
payload={initPageResult?.req.payload}
|
||||
visibleEntities={initPageResult.visibleEntities}
|
||||
>
|
||||
<DefaultTemplate config={config} visibleEntities={initPageResult.visibleEntities}>
|
||||
{RenderedView}
|
||||
</DefaultTemplate>
|
||||
)}
|
||||
|
||||
@@ -2,10 +2,14 @@ import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateUnauthorizedMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateUnauthorizedMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}) => {
|
||||
return meta({
|
||||
config,
|
||||
description: t('error:unauthorized'),
|
||||
keywords: t('error:unauthorized'),
|
||||
title: t('error:unauthorized'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ export const Verify: React.FC<AdminViewProps> = async ({ initPageResult, params
|
||||
|
||||
const {
|
||||
payload: { config },
|
||||
payload,
|
||||
} = req
|
||||
|
||||
const {
|
||||
@@ -43,7 +42,7 @@ export const Verify: React.FC<AdminViewProps> = async ({ initPageResult, params
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={`${verifyBaseClass}__brand`}>
|
||||
<Logo payload={payload} />
|
||||
<Logo config={config} />
|
||||
</div>
|
||||
<h2>{textToRender}</h2>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateVerifyMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateVerifyMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) => {
|
||||
return meta({
|
||||
config,
|
||||
description: t('authentication:verifyUser'),
|
||||
keywords: t('authentication:verify'),
|
||||
title: t('authentication:verify'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SelectFieldProps } from '@payloadcms/ui/fields/Select'
|
||||
import type { MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import type { OptionObject, SelectField } from 'payload/types'
|
||||
@@ -35,7 +35,7 @@ const getOptionsToRender = (
|
||||
|
||||
const getTranslatedOptions = (
|
||||
options: (OptionObject | string)[] | OptionObject | string,
|
||||
i18n: I18nClient,
|
||||
i18n: I18n,
|
||||
): string => {
|
||||
if (Array.isArray(options)) {
|
||||
return options
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { FieldMap, MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import type { FieldPermissions } from 'payload/auth'
|
||||
import type React from 'react'
|
||||
@@ -13,7 +13,7 @@ export type Props = {
|
||||
disableGutter?: boolean
|
||||
field: MappedField
|
||||
fieldMap: FieldMap
|
||||
i18n: I18nClient
|
||||
i18n: I18n
|
||||
isRichText?: boolean
|
||||
locale?: string
|
||||
locales?: string[]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { FieldMap, MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import type { FieldPermissions } from 'payload/auth'
|
||||
import type { DiffMethod } from 'react-diff-viewer-continued'
|
||||
@@ -10,7 +10,7 @@ export type Props = {
|
||||
diffComponents: DiffComponents
|
||||
fieldMap: FieldMap
|
||||
fieldPermissions: Record<string, FieldPermissions>
|
||||
i18n: I18nClient
|
||||
i18n: I18n
|
||||
locales: string[]
|
||||
version: Record<string, any>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
@@ -149,7 +149,6 @@
|
||||
"get-port": "5.1.1",
|
||||
"graphql-http": "^1.22.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"next": "^14.3.0-canary.7",
|
||||
"nodemon": "3.0.3",
|
||||
"object.assign": "4.1.4",
|
||||
"object.entries": "1.1.6",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
|
||||
import type { PayloadRequestWithData, RequestContext } from '../types/index.js'
|
||||
import type { WithServerSideProps } from './elements/WithServerSideProps.js'
|
||||
@@ -22,12 +22,12 @@ type RichTextAdapterBase<
|
||||
generateComponentMap: (args: {
|
||||
WithServerSideProps: WithServerSideProps
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
i18n: I18n
|
||||
schemaPath: string
|
||||
}) => Map<string, React.ReactNode>
|
||||
generateSchemaMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
i18n: I18n
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaPath: string
|
||||
}) => Map<string, Field[]>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClientTranslationsObject } from '@payloadcms/translations'
|
||||
import type { SupportedLanguages } from '@payloadcms/translations'
|
||||
|
||||
import type { Permissions } from '../../auth/index.js'
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
@@ -43,7 +43,7 @@ export type InitPageResult = {
|
||||
locale: Locale
|
||||
permissions: Permissions
|
||||
req: PayloadRequestWithData
|
||||
translations: ClientTranslationsObject
|
||||
translations: SupportedLanguages
|
||||
visibleEntities: VisibleEntities
|
||||
}
|
||||
|
||||
|
||||
@@ -144,9 +144,17 @@ type LoadFn = (...args: Required<LoadArgs>) => Promise<LoadResult>
|
||||
|
||||
const swcOptions = {
|
||||
...tsconfig,
|
||||
baseUrl: undefined,
|
||||
baseUrl: path.resolve(''),
|
||||
paths: undefined,
|
||||
}
|
||||
|
||||
if (tsconfig.paths) {
|
||||
swcOptions.paths = tsconfig.paths
|
||||
if (tsconfig.baseUrl) {
|
||||
swcOptions.baseUrl = path.resolve(tsconfig.baseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
export const load: LoadFn = async (url, context, nextLoad) => {
|
||||
if (context.format === 'client') {
|
||||
const rawSource = 'export default {}'
|
||||
|
||||
@@ -1,29 +1,3 @@
|
||||
/**
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present LongYinan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { Options } from '@swc-node/core'
|
||||
|
||||
import { resolve } from 'path'
|
||||
|
||||
@@ -26,7 +26,6 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
||||
graphQL: {
|
||||
disablePlaygroundInProduction: true,
|
||||
maxComplexity: 1000,
|
||||
schemaOutputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/schema.graphql`,
|
||||
},
|
||||
hooks: {},
|
||||
i18n: {},
|
||||
|
||||
@@ -108,9 +108,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
i18nConfig.fallbackLanguage = supportedLangKeys.includes(fallbackLang)
|
||||
? fallbackLang
|
||||
: supportedLangKeys[0]
|
||||
i18nConfig.translations =
|
||||
(incomingConfig.i18n?.translations as SanitizedConfig['i18n']['translations']) ||
|
||||
i18nConfig.translations
|
||||
i18nConfig.translations = incomingConfig.i18n?.translations || i18nConfig.translations
|
||||
}
|
||||
|
||||
config.i18n = i18nConfig
|
||||
|
||||
@@ -68,13 +68,7 @@ export default joi.object({
|
||||
}),
|
||||
logoutRoute: joi.string(),
|
||||
meta: joi.object().keys({
|
||||
icons: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.array().items(joi.alternatives().try(joi.string(), joi.object())),
|
||||
joi.object(),
|
||||
joi.string().allow(null),
|
||||
),
|
||||
favicon: joi.string(),
|
||||
ogImage: joi.string(),
|
||||
titleSuffix: joi.string(),
|
||||
}),
|
||||
@@ -116,7 +110,6 @@ export default joi.object({
|
||||
maxComplexity: joi.number(),
|
||||
mutations: joi.function(),
|
||||
queries: joi.function(),
|
||||
schemaOutputFile: joi.string(),
|
||||
}),
|
||||
hooks: joi.object().keys({
|
||||
afterError: joi.func(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { DefaultTranslationsObject, I18nOptions, TFunction } from '@payloadcms/translations'
|
||||
import type { I18nOptions, TFunction } from '@payloadcms/translations'
|
||||
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
|
||||
import type GraphQL from 'graphql'
|
||||
import type { Metadata as NextMetadata } from 'next'
|
||||
import type { DestinationStream, LoggerOptions, P } from 'pino'
|
||||
import type React from 'react'
|
||||
import type { default as sharp } from 'sharp'
|
||||
@@ -461,15 +460,14 @@ export type Config = {
|
||||
}
|
||||
/** The route for the logout page. */
|
||||
logoutRoute?: string
|
||||
/** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */
|
||||
/** Base meta data to use for the Admin panel. Included properties are titleSuffix, ogImage, and favicon. */
|
||||
meta?: {
|
||||
/**
|
||||
* An array of Next.js metadata objects that represent icons to be used by devices and browsers.
|
||||
* Public path to an icon
|
||||
*
|
||||
* For example browser tabs, phone home screens, and search engine results.
|
||||
* @reference https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons
|
||||
* This image may be displayed in the browser next to the title of the page
|
||||
*/
|
||||
icons?: NextMetadata['icons']
|
||||
favicon?: string
|
||||
/**
|
||||
* Public path to an image
|
||||
*
|
||||
@@ -566,10 +564,6 @@ export type Config = {
|
||||
* @see https://payloadcms.com/docs/graphql/extending
|
||||
*/
|
||||
queries?: GraphQLExtension
|
||||
/**
|
||||
* Filepath to write the generated schema to
|
||||
*/
|
||||
schemaOutputFile?: string
|
||||
}
|
||||
/**
|
||||
* Tap into Payload-wide hooks.
|
||||
@@ -580,7 +574,7 @@ export type Config = {
|
||||
afterError?: AfterErrorHook
|
||||
}
|
||||
/** i18n config settings */
|
||||
i18n?: I18nOptions<{} | DefaultTranslationsObject> // loosen the type here to allow for custom translations
|
||||
i18n?: I18nOptions
|
||||
/** Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. */
|
||||
indexSortableFields?: boolean
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,4 @@
|
||||
*/
|
||||
|
||||
export { generateTypes } from '../bin/generateTypes.js'
|
||||
export { loadEnv } from '../bin/loadEnv.js'
|
||||
export { findConfig } from '../config/find.js'
|
||||
export { importConfig, importWithoutClientFiles } from '../utilities/importWithoutClientFiles.js'
|
||||
|
||||
@@ -121,9 +121,6 @@ type Admin = {
|
||||
Cell?: CustomComponent
|
||||
Description?: DescriptionComponent
|
||||
Field?: CustomComponent
|
||||
/**
|
||||
* The Filter component has to be a client component
|
||||
*/
|
||||
Filter?: React.ComponentType<any>
|
||||
}
|
||||
/**
|
||||
@@ -450,9 +447,6 @@ export type UIField = {
|
||||
components?: {
|
||||
Cell?: CustomComponent
|
||||
Field: CustomComponent
|
||||
/**
|
||||
* The Filter component has to be a client component
|
||||
*/
|
||||
Filter?: React.ComponentType<any>
|
||||
}
|
||||
condition?: Condition
|
||||
|
||||
@@ -65,11 +65,11 @@ export const text: Validate<string | string[], unknown, unknown, TextField> = (
|
||||
const length = stringValue?.length || 0
|
||||
|
||||
if (typeof maxLength === 'number' && length > maxLength) {
|
||||
return t('validation:shorterThanMax', { label: t('general:value'), maxLength, stringValue })
|
||||
return t('validation:shorterThanMax', { label: t('value'), maxLength, stringValue })
|
||||
}
|
||||
|
||||
if (typeof minLength === 'number' && length < minLength) {
|
||||
return t('validation:longerThanMin', { label: t('general:value'), minLength, stringValue })
|
||||
return t('validation:longerThanMin', { label: t('value'), minLength, stringValue })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { configToJSONSchema } from './configToJSONSchema.js'
|
||||
|
||||
describe('configToJSONSchema', () => {
|
||||
it('should handle optional arrays with required fields', async () => {
|
||||
// @ts-expect-error
|
||||
const config: Config = {
|
||||
collections: [
|
||||
{
|
||||
@@ -59,91 +58,4 @@ describe('configToJSONSchema', () => {
|
||||
type: 'object',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle tabs and named tabs with required fields', async () => {
|
||||
// @ts-expect-error
|
||||
const config: Config = {
|
||||
collections: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'unnamedTab',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'fieldInUnnamedTab',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'namedTab',
|
||||
name: 'namedTab',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'fieldInNamedTab',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'namedTabWithRequired',
|
||||
name: 'namedTabWithRequired',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'fieldInNamedTab',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
slug: 'test',
|
||||
timestamps: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const sanitizedConfig = await sanitizeConfig(config)
|
||||
const schema = configToJSONSchema(sanitizedConfig, 'text')
|
||||
|
||||
expect(schema?.definitions?.test).toStrictEqual({
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
fieldInUnnamedTab: {
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
namedTab: {
|
||||
additionalProperties: false,
|
||||
type: 'object',
|
||||
properties: {
|
||||
fieldInNamedTab: {
|
||||
type: ['string', 'null'],
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
namedTabWithRequired: {
|
||||
additionalProperties: false,
|
||||
type: 'object',
|
||||
properties: {
|
||||
fieldInNamedTab: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['fieldInNamedTab'],
|
||||
},
|
||||
},
|
||||
required: ['id', 'namedTabWithRequired'],
|
||||
title: 'Test',
|
||||
type: 'object',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -466,13 +466,7 @@ export function fieldsToJSONSchema(
|
||||
additionalProperties: false,
|
||||
...childSchema,
|
||||
})
|
||||
|
||||
// If the named tab has any required fields then we mark this as required otherwise it should be optional
|
||||
const hasRequiredFields = tab.fields.some((subField) => fieldIsRequired(subField))
|
||||
|
||||
if (hasRequiredFields) {
|
||||
requiredFieldNames.add(tab.name)
|
||||
}
|
||||
requiredFieldNames.add(tab.name)
|
||||
} else {
|
||||
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
|
||||
fieldSchemas.set(propName, propSchema)
|
||||
|
||||
@@ -19,13 +19,20 @@ This package is now best used for implementing custom storage solutions or third
|
||||
|
||||
## Usage
|
||||
|
||||
Add this package into your dependencies executing this code in your command line:
|
||||
|
||||
`yarn add @payloadcms/plugin-cloud-storage`
|
||||
|
||||
Now install this plugin within your Payload as follows:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
|
||||
import path from 'path'
|
||||
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
|
||||
|
||||
export default buildConfig({
|
||||
plugins: [
|
||||
cloudStoragePlugin({
|
||||
cloudStorage({
|
||||
collections: {
|
||||
'my-collection-slug': {
|
||||
adapter: theAdapterToUse, // see docs for the adapter you want to use
|
||||
@@ -42,7 +49,7 @@ export default buildConfig({
|
||||
The proper way to conditionally enable/disable this plugin is to use the `enabled` property.
|
||||
|
||||
```ts
|
||||
cloudStoragePlugin({
|
||||
cloudStorage({
|
||||
enabled: process.env.MY_CONDITION === 'true',
|
||||
collections: {
|
||||
'my-collection-slug': {
|
||||
@@ -98,6 +105,14 @@ Instead, all uploads will still be reached from the default `/collectionSlug/sta
|
||||
|
||||
If this does not apply to you (your upload collection has `read: () => true` or similar) you can disable this functionality by setting `disablePayloadAccessControl` to `true`. When this setting is in place, this plugin will update your file URLs to point directly to your cloud host.
|
||||
|
||||
## Local development
|
||||
|
||||
For instructions regarding how to develop with this plugin locally, [click here](https://github.com/payloadcms/plugin-cloud-storage/blob/master/docs/local-dev.md).
|
||||
|
||||
## Questions
|
||||
|
||||
Please contact [Payload](mailto:dev@payloadcms.com) with any questions about using this plugin.
|
||||
|
||||
## Credit
|
||||
|
||||
This plugin was created with significant help, and code, from [Alex Bechmann](https://github.com/alexbechmann) and [Richard VanBergen](https://github.com/richardvanbergen). Thank you!!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -8,10 +8,9 @@ import { createParentField } from './fields/parent.js'
|
||||
import { parentFilterOptions } from './fields/parentFilterOptions.js'
|
||||
import { resaveChildren } from './hooks/resaveChildren.js'
|
||||
import { resaveSelfAfterCreate } from './hooks/resaveSelfAfterCreate.js'
|
||||
import { getParents } from './utilities/getParents.js'
|
||||
import { populateBreadcrumbs } from './utilities/populateBreadcrumbs.js'
|
||||
|
||||
export { createBreadcrumbsField, createParentField, getParents }
|
||||
export { createBreadcrumbsField, createParentField }
|
||||
|
||||
export const nestedDocsPlugin =
|
||||
(pluginConfig: NestedDocsPluginConfig): Plugin =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-relationship-object-ids",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.18",
|
||||
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateDescription } from '../types.js'
|
||||
|
||||
import { defaults } from '../defaults.js'
|
||||
@@ -31,7 +30,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, path, required } = props
|
||||
const { path: pathFromContext } = useFieldProps()
|
||||
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
const [fields] = useAllFormFields()
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateImage } from '../types.js'
|
||||
|
||||
import { Pill } from '../ui/Pill.js'
|
||||
@@ -28,7 +27,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
|
||||
const field: FieldType<string> = useField(props as Options)
|
||||
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
const [fields] = useAllFormFields()
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateTitle } from '../types.js'
|
||||
|
||||
import { defaults } from '../defaults.js'
|
||||
@@ -32,7 +31,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
const { CustomLabel, hasGenerateTitleFn, label, labelProps, path, required } = props || {}
|
||||
const { path: pathFromContext } = useFieldProps()
|
||||
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const field: FieldType<string> = useField({
|
||||
path,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { NestedKeysStripped } from '@payloadcms/translations'
|
||||
|
||||
export const translations = {
|
||||
en: {
|
||||
$schema: './translation-schema.json',
|
||||
@@ -164,20 +162,13 @@ export const translations = {
|
||||
checksPassing: '{{current}}/{{max}} перевірок пройдено',
|
||||
good: 'Чудово',
|
||||
imageAutoGenerationTip: 'Автоматична генерація використає зображення з головного блоку',
|
||||
lengthTipDescription:
|
||||
'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метаописи — перегляньте ',
|
||||
lengthTipTitle:
|
||||
'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метазаголовки — перегляньте ',
|
||||
lengthTipDescription: 'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метаописи — перегляньте ',
|
||||
lengthTipTitle: 'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метазаголовки — перегляньте ',
|
||||
noImage: 'Немає зображення',
|
||||
preview: 'Попередній перегляд',
|
||||
previewDescription:
|
||||
'Реальне відображення може відрізнятися в залежності від вмісту та релевантності пошуку.',
|
||||
previewDescription: 'Реальне відображення може відрізнятися в залежності від вмісту та релевантності пошуку.',
|
||||
tooLong: 'Задовгий',
|
||||
tooShort: 'Закороткий',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export type PluginSEOTranslations = typeof translations.en
|
||||
|
||||
export type PluginSEOTranslationKeys = NestedKeysStripped<PluginSEOTranslations>
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
|
||||
import { Pill } from './Pill.js'
|
||||
|
||||
export const LengthIndicator: React.FC<{
|
||||
@@ -21,7 +19,7 @@ export const LengthIndicator: React.FC<{
|
||||
|
||||
const [label, setLabel] = useState('')
|
||||
const [barWidth, setBarWidth] = useState<number>(0)
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const textLength = text?.length || 0
|
||||
|
||||
@@ -6,8 +6,6 @@ import { useAllFormFields, useForm } from '@payloadcms/ui/forms/Form'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
|
||||
import { defaults } from '../defaults.js'
|
||||
|
||||
const {
|
||||
@@ -28,7 +26,7 @@ export const Overview: React.FC = () => {
|
||||
'meta.title': { value: metaTitle } = {} as FormField,
|
||||
},
|
||||
] = useAllFormFields()
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [titleIsValid, setTitleIsValid] = useState<boolean | undefined>()
|
||||
const [descIsValid, setDescIsValid] = useState<boolean | undefined>()
|
||||
|
||||
@@ -8,15 +8,15 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateURL } from '../types.js'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
type PreviewProps = UIField & {
|
||||
hasGenerateURLFn: boolean
|
||||
}
|
||||
|
||||
export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const locale = useLocale()
|
||||
const [fields] = useAllFormFields()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { BaseSelection, LexicalEditor } from 'lexical'
|
||||
import type React from 'react'
|
||||
|
||||
@@ -49,7 +49,7 @@ export type ToolbarGroupItem = {
|
||||
}) => boolean
|
||||
key: string
|
||||
/** The label is displayed as text if the item is part of a dropdown group */
|
||||
label?: (({ i18n }: { i18n: I18nClient }) => string) | string
|
||||
label?: (({ i18n }: { i18n: I18n }) => string) | string
|
||||
onSelect?: ({ editor, isActive }: { editor: LexicalEditor; isActive: boolean }) => void
|
||||
order?: number
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Transformer } from '@lexical/markdown'
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
@@ -274,7 +274,7 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
||||
clientFeatureProps?: ClientFeatureProps
|
||||
generateComponentMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
i18n: I18n
|
||||
props: ServerProps
|
||||
schemaPath: string
|
||||
}) => {
|
||||
@@ -282,7 +282,7 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
|
||||
}
|
||||
generateSchemaMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
i18n: I18n
|
||||
props: ServerProps
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaPath: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type { MutableRefObject } from 'react'
|
||||
import type React from 'react'
|
||||
@@ -13,7 +13,7 @@ export type SlashMenuItem = {
|
||||
keyboardShortcut?: string
|
||||
// For extra searching.
|
||||
keywords?: Array<string>
|
||||
label?: (({ i18n }: { i18n: I18nClient }) => string) | string
|
||||
label?: (({ i18n }: { i18n: I18n }) => string) | string
|
||||
// What happens when you select this item?
|
||||
onSelect: ({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => void
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export type SlashMenuGroup = {
|
||||
items: Array<SlashMenuItem>
|
||||
key: string
|
||||
// Used for class names and, if label is not provided, for display.
|
||||
label?: (({ i18n }: { i18n: I18nClient }) => string) | string
|
||||
label?: (({ i18n }: { i18n: I18n }) => string) | string
|
||||
}
|
||||
|
||||
export type SlashMenuItemInternal = SlashMenuItem & {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.0.0-beta.31",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user