Compare commits

..

2 Commits

Author SHA1 Message Date
James
e37897f7a5 chore: pnpm i 2024-05-10 16:05:26 -07:00
James
dbf1a1bd8b chore: adds a bit of safety to form-state endpoint 2024-05-10 16:01:29 -07:00
213 changed files with 2392 additions and 3468 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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.

View File

@@ -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).

View File

@@ -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
},
},
}),
```

View File

@@ -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,
},

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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-')),

View File

@@ -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: {

View File

@@ -53,9 +53,6 @@ export class Main {
'--use-pnpm': Boolean,
'--use-yarn': Boolean,
// Other
'--no-git': Boolean,
// Flags
'--beta': Boolean,
'--debug': Boolean,

View File

@@ -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

View File

@@ -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
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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)
}

View File

@@ -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
})
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env node
import { bin } from './dist/bin/index.js'
bin()

View File

@@ -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",

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -1 +0,0 @@
export { generateSchema } from '../bin/generateSchema.js'

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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)

View File

@@ -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

View File

@@ -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[]

View File

@@ -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.
*/

View File

@@ -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,

View File

@@ -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}`,
})
}
}

View File

@@ -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` : ''}`}

View File

@@ -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}
/>

View File

@@ -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',
})
}

View File

@@ -77,7 +77,6 @@ export const Account: React.FC<AdminViewProps> = ({ initPageResult, params, sear
}
DefaultComponent={EditView}
componentProps={viewComponentProps}
payload={payload}
/>
</FormQueryParamsProvider>
</DocumentInfoProvider>

View File

@@ -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'),
})
}

View File

@@ -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'),
})
}

View File

@@ -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>
)

View File

@@ -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>
)

View File

@@ -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'),
})
}

View File

@@ -220,7 +220,6 @@ export const Document: React.FC<AdminViewProps> = async ({
CustomComponent={ViewOverride || CustomView}
DefaultComponent={DefaultView}
componentProps={viewComponentProps}
payload={payload}
/>
)}
</FormQueryParamsProvider>

View File

@@ -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'),
})
}

View File

@@ -142,7 +142,6 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
CustomComponent={CustomListView}
DefaultComponent={DefaultListView}
componentProps={viewComponentProps}
payload={payload}
/>
</TableColumnsProvider>
</ListQueryProvider>

View File

@@ -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} />}

View File

@@ -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'),
})
}

View File

@@ -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'),
})
}

View File

@@ -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 />

View File

@@ -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'),
})
}

View File

@@ -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'),
})
}

View File

@@ -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>
)}

View File

@@ -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'),
})
}

View File

@@ -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>

View File

@@ -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'),
})
}

View File

@@ -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

View File

@@ -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[]

View File

@@ -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>
}

View File

@@ -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",

View File

@@ -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[]>

View File

@@ -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
}

View File

@@ -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 {}'

View File

@@ -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'

View File

@@ -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: {},

View File

@@ -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

View File

@@ -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(),

View File

@@ -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
/**

View File

@@ -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'

View File

@@ -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

View File

@@ -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 })
}
}

View File

@@ -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',
})
})
})

View File

@@ -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)

View File

@@ -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!!

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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 =>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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()

View File

@@ -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()

View File

@@ -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,

View File

@@ -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>

View File

@@ -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

View File

@@ -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>()

View File

@@ -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()

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 & {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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