Compare commits
45 Commits
fix/s3-sto
...
chore/grap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7e036e1b8 | ||
|
|
c4fd27de01 | ||
|
|
b44603b253 | ||
|
|
9d6583d9de | ||
|
|
7be02194d6 | ||
|
|
1da50f5684 | ||
|
|
f2abc80a00 | ||
|
|
88eeeaa8dd | ||
|
|
d14bc44c63 | ||
|
|
9c53a62503 | ||
|
|
bc79608db4 | ||
|
|
d959d843a2 | ||
|
|
eb09ce9a3e | ||
|
|
f2da72b4d0 | ||
|
|
5285518562 | ||
|
|
243cdb1901 | ||
|
|
c7bb694249 | ||
|
|
8f3d1bd871 | ||
|
|
85f88a0194 | ||
|
|
38f61e91b8 | ||
|
|
ac1e3cf69e | ||
|
|
397c1f1ae7 | ||
|
|
c8f01e31a1 | ||
|
|
9ac7a3ed49 | ||
|
|
051c1fe015 | ||
|
|
6d0924ef37 | ||
|
|
fc5876a602 | ||
|
|
72efc843cc | ||
|
|
3ede7abe00 | ||
|
|
5d65cb002b | ||
|
|
814ced463b | ||
|
|
3de1636e92 | ||
|
|
e9afb367b5 | ||
|
|
029cac3cd3 | ||
|
|
a53876d741 | ||
|
|
6f90d62fc2 | ||
|
|
6699844d7b | ||
|
|
657ad20278 | ||
|
|
30af889e3b | ||
|
|
8378654fd0 | ||
|
|
b0da85dfea | ||
|
|
48115311e7 | ||
|
|
7cef8900a7 | ||
|
|
557ac9931a | ||
|
|
9f7e8f47d2 |
27
.github/actions/release-commenter/LICENSE
vendored
Normal file
27
.github/actions/release-commenter/LICENSE
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2025 Cameron Little <cameron@camlittle.com>
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
Modifications made by Payload CMS, Inc. <info@payloadcms.com>, 2025
|
||||
Details in README.md
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm build:typecheck && pnpm build:ncc",
|
||||
|
||||
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
@@ -41,14 +41,14 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
needs_build:
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/main.yml'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package.json'
|
||||
- 'templates/**'
|
||||
needs_tests:
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/main.yml'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
@@ -308,6 +308,7 @@ jobs:
|
||||
- fields__collections__Text
|
||||
- fields__collections__UI
|
||||
- fields__collections__Upload
|
||||
- form-state
|
||||
- live-preview
|
||||
- localization
|
||||
- locked-documents
|
||||
|
||||
1
.github/workflows/pr-title.yml
vendored
1
.github/workflows/pr-title.yml
vendored
@@ -53,6 +53,7 @@ jobs:
|
||||
plugin-cloud
|
||||
plugin-cloud-storage
|
||||
plugin-form-builder
|
||||
plugin-import-export
|
||||
plugin-multi-tenant
|
||||
plugin-nested-docs
|
||||
plugin-redirects
|
||||
|
||||
@@ -6,7 +6,11 @@ desc: Customize the metadata of your pages within the Admin Panel
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more, without any additional configuration. This includes the page title, description, og:image and everything in between. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views, allowing for the ability to control metadata on any page with high precision.
|
||||
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more. This includes the page title, description, og:image, etc. and requires no additional configuration.
|
||||
|
||||
Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views. This allows for the ability to control metadata on any page with high precision, while also providing sensible defaults.
|
||||
|
||||
All metadata is injected into Next.js' [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function. This used to generate the `<head>` of pages within the Admin Panel. All metadata options that are available in Next.js are exposed by Payload.
|
||||
|
||||
Within the Admin Panel, metadata can be customized at the following levels:
|
||||
|
||||
@@ -48,13 +52,9 @@ The following options are available for Root Metadata:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`defaultOGImageType`** | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
|
||||
| **`icons`** | `IconConfig[]` | An array of icon objects. [More details](#icons) |
|
||||
| **`keywords`** | `string` | A comma-separated list of keywords to include in the metadata of the Admin Panel. |
|
||||
| **`openGraph`** | `OpenGraphConfig` | An object containing Open Graph metadata. [More details](#open-graph) |
|
||||
| **`titleSuffix`** | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
|
||||
| `defaultOGImageType` | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
|
||||
| `titleSuffix` | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
|
||||
| `[keyof Metadata]` | `unknown` | Any other properties that Next.js supports within the `generateMetadata` function. [More details](https://nextjs.org/docs/app/api-reference/functions/generate-metadata). |
|
||||
|
||||
<Banner type="success">
|
||||
**Reminder:**
|
||||
@@ -67,7 +67,7 @@ The Icons Config corresponds to the `<link>` tags that are used to specify icons
|
||||
|
||||
The most common icon type is the favicon, which is displayed in the browser tab. This is specified by the `rel` attribute `icon`. Other common icon types include `apple-touch-icon`, which is used by Apple devices when the Admin Panel is saved to the home screen, and `mask-icon`, which is used by Safari to mask the Admin Panel icon.
|
||||
|
||||
To customize icons, use the `icons` key within the `admin.meta` object in your Payload Config:
|
||||
To customize icons, use the `admin.meta.icons` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
@@ -93,23 +93,13 @@ To customize icons, use the `icons` key within the `admin.meta` object in your P
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Icons:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`rel`** | `string` | The HTML `rel` attribute of the icon. |
|
||||
| **`type`** | `string` | The MIME type of the icon. |
|
||||
| **`color`** | `string` | The color of the icon. |
|
||||
| **`fetchPriority`** | `string` | The [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) of the icon. |
|
||||
| **`media`** | `string` | The [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) of the icon. |
|
||||
| **`sizes`** | `string` | The [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) of the icon. |
|
||||
| **`url`** | `string` | The URL pointing the resource of the icon. |
|
||||
For a full list of all available Icon options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons).
|
||||
|
||||
### Open Graph
|
||||
|
||||
Open Graph metadata is a set of tags that are used to control how URLs are displayed when shared on social media platforms. Open Graph metadata is automatically generated by Payload, but can be customized at the Root level.
|
||||
|
||||
To customize Open Graph metadata, use the `openGraph` key within the `admin.meta` object in your Payload Config:
|
||||
To customize Open Graph metadata, use the `admin.meta.openGraph` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
@@ -135,14 +125,46 @@ To customize Open Graph metadata, use the `openGraph` key within the `admin.meta
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Open Graph Metadata:
|
||||
For a full list of all available Open Graph options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#opengraph).
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`images`** | `OGImageConfig` or `OGImageConfig[]` | An array of image objects. |
|
||||
| **`siteName`** | `string` | The name of the site. |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
### Robots
|
||||
|
||||
Setting the `robots` property will allow you to control the `robots` meta tag that is rendered within the `<head>` of the Admin Panel. This can be used to control how search engines index pages and displays them in search results.
|
||||
|
||||
By default, the Admin Panel is set to prevent search engines from indexing pages within the Admin Panel.
|
||||
|
||||
To customize the Robots Config, use the `admin.meta.robots` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
meta: {
|
||||
// highlight-start
|
||||
robots: 'noindex, nofollow',
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For a full list of all available Robots options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#robots).
|
||||
|
||||
##### Prevent Crawling
|
||||
|
||||
While setting meta tags via `admin.meta.robots` can prevent search engines from _indexing_ web pages, it does not prevent them from being _crawled_.
|
||||
|
||||
To prevent your pages from being crawled altogether, add a `robots.txt` file to your root directory.
|
||||
|
||||
```text
|
||||
User-agent: *
|
||||
Disallow: /admin/
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Note:**
|
||||
If you've customized the path to your Admin Panel via `config.routes`, be sure to update the `Disallow` directive to match your custom path.
|
||||
</Banner>
|
||||
|
||||
## Collection Metadata
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ The following options are available:
|
||||
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
|
||||
| `description` | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
|
||||
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this Collection's List View. |
|
||||
| `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. |
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
|
||||
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
|
||||
@@ -188,13 +188,15 @@ In order to use [Custom Translations](#custom-translations) in your project, you
|
||||
|
||||
Here we create a shareable translations object. We will import this in both our custom components and in our Payload config.
|
||||
|
||||
In this example we show how to extend English, but you can do the same for any language you want.
|
||||
|
||||
```ts
|
||||
// <rootDir>/custom-translations.ts
|
||||
|
||||
import type { Config } from 'payload'
|
||||
import { enTranslations } from '@payloadcms/translations/languages/en'
|
||||
import type { NestedKeysStripped } from '@payloadcms/translations'
|
||||
|
||||
export const customTranslations: Config['i18n']['translations'] = {
|
||||
export const customTranslations = {
|
||||
en: {
|
||||
general: {
|
||||
myCustomKey: 'My custom english translation',
|
||||
@@ -205,7 +207,7 @@ export const customTranslations: Config['i18n']['translations'] = {
|
||||
},
|
||||
}
|
||||
|
||||
export type CustomTranslationsObject = typeof customTranslations.en
|
||||
export type CustomTranslationsObject = typeof customTranslations.en & typeof enTranslations
|
||||
export type CustomTranslationsKeys = NestedKeysStripped<CustomTranslationsObject>
|
||||
```
|
||||
|
||||
@@ -259,7 +261,10 @@ const field: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
label: (
|
||||
{ t }: { t: TFunction<CustomTranslationsKeys | 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'),
|
||||
{ t: defaultT }
|
||||
) => {
|
||||
const t = defaultT as TFunction<CustomTranslationsKeys>
|
||||
return t('fields:addLabel')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -118,6 +118,10 @@ default, migrations will be named using a timestamp.
|
||||
npm run payload migrate:create optional-name-here
|
||||
```
|
||||
|
||||
Flags:
|
||||
* `--skip-empty`: with Postgres, it skips the "no schema changes detected. Would you like to create a blank migration file?" prompt which can be useful for generating migration in CI.
|
||||
* `--force-accept-warning`: accepts any command prompts, creates a blank migration even if there weren't any changes to the schema.
|
||||
|
||||
### Status
|
||||
|
||||
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run,
|
||||
|
||||
@@ -286,14 +286,15 @@ export const MyField: Field = {
|
||||
|
||||
The following additional properties are provided in the `ctx` object:
|
||||
|
||||
| Property | Description |
|
||||
| Property | Description |
|
||||
| --- | --- |
|
||||
| `data` | An object containing the full collection or global document currently being edited. |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
|
||||
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
|
||||
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
|
||||
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
|
||||
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
|
||||
| `data` | An object containing the full collection or global document currently being edited. |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. |
|
||||
| `operation` | Will be `create` or `update` depending on the UI action or API call. |
|
||||
| `path` | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
|
||||
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
|
||||
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
|
||||
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
|
||||
|
||||
#### Reusing Default Field Validations
|
||||
|
||||
@@ -522,11 +523,11 @@ You can show and hide fields based on what other fields are doing by utilizing c
|
||||
|
||||
The `ctx` object:
|
||||
|
||||
| Property | Description |
|
||||
| Property | Description |
|
||||
| --- | --- |
|
||||
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
|
||||
| **`path`** | The full path to the field in the schema, including array indexes. Useful for dynamic lookups. |
|
||||
| **`user`** | The currently authenticated user object. |
|
||||
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
|
||||
| **`path`** | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
|
||||
| **`user`** | The currently authenticated user object. |
|
||||
|
||||
The `condition` function should return a boolean that will control if the field should be displayed or not.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ If your front-end is statically generated then you may also want to regenerate t
|
||||
|
||||
### Admin Bar
|
||||
|
||||
You might also want to render an admin bar on your front-end so that logged-in users can quickly navigate between the front-end and Payload as they're editing. For React apps, check out the official [Payload Admin Bar](https://github.com/payloadcms/payload-admin-bar). For other frameworks, simply hit the `/me` route with `credentials: 'include'` and render your own admin bar if the user is logged in.
|
||||
You might also want to render an admin bar on your front-end so that logged-in users can quickly navigate between the front-end and Payload as they're editing. For React apps, check out the official [Payload Admin Bar](https://github.com/payloadcms/payload/tree/main/packages/admin-bar). For other frameworks, simply hit the `/me` route with `credentials: 'include'` and render your own admin bar if the user is logged in.
|
||||
|
||||
### CORS
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/admin-bar": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
@@ -25,7 +26,6 @@
|
||||
"graphql": "^16.9.0",
|
||||
"next": "^15.0.0",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.7",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
|
||||
5671
examples/draft-preview/pnpm-lock.yaml
generated
Normal file
5671
examples/draft-preview/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadAdminBarProps } from 'payload-admin-bar'
|
||||
import type { PayloadAdminBarProps } from '@payloadcms/admin-bar'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { PayloadAdminBar } from 'payload-admin-bar'
|
||||
import { PayloadAdminBar } from '@payloadcms/admin-bar'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
@@ -42,7 +42,7 @@ export const AdminBar: React.FC<{
|
||||
user: classes.user,
|
||||
}}
|
||||
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
|
||||
collection={collection}
|
||||
collectionSlug={collection}
|
||||
collectionLabels={{
|
||||
plural: collectionLabels[collection]?.plural || 'Pages',
|
||||
singular: collectionLabels[collection]?.singular || 'Page',
|
||||
|
||||
@@ -52,7 +52,7 @@ export const home: Page = {
|
||||
children: [{ text: 'Payload Admin Bar' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload-admin-bar',
|
||||
url: 'https://github.com/payloadcms/payload/tree/main/packages/admin-bar',
|
||||
},
|
||||
{
|
||||
text: ' appear at the top of this site. This will allow you to seamlessly navigate between the two apps. Then, navigate to the ',
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"next": "^15.1.0",
|
||||
"next-intl": "^3.23.2",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.7",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
14
examples/localization/pnpm-lock.yaml
generated
14
examples/localization/pnpm-lock.yaml
generated
@@ -83,9 +83,6 @@ importers:
|
||||
payload:
|
||||
specifier: latest
|
||||
version: 3.25.0(graphql@16.10.0)(typescript@5.7.3)
|
||||
payload-admin-bar:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
prism-react-renderer:
|
||||
specifier: ^2.3.1
|
||||
version: 2.4.1(react@19.0.0)
|
||||
@@ -3354,12 +3351,6 @@ packages:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
payload-admin-bar@1.0.7:
|
||||
resolution: {integrity: sha512-eY/FjfCGkyXOxRupv4IPZ+HFh8CQnJBQS++VItgTXe/g9H0B4RqxfdpU3g3tART3e8MzmZYGOBxV5EGGO2+jbg==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
payload@3.25.0:
|
||||
resolution: {integrity: sha512-azT1qtirV8QqIPpyWaxbF5TJPoWT5fpYoxin83wZxF5gmg0O06bL5YKCGFfCpzgCcw4FrFtLSzD68zGMc5m5Eg==}
|
||||
engines: {node: ^18.20.2 || >=20.9.0}
|
||||
@@ -8386,11 +8377,6 @@ snapshots:
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
payload-admin-bar@1.0.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
payload@3.25.0(graphql@16.10.0)(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@next/env': 15.2.0
|
||||
|
||||
@@ -5,7 +5,6 @@ import { GeistMono } from 'geist/font/mono'
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import React from 'react'
|
||||
|
||||
import { AdminBar } from '@/components/AdminBar'
|
||||
import { Footer } from '@/globals/Footer/Component'
|
||||
import { Header } from '@/globals/Header/Component'
|
||||
import { LivePreviewListener } from '@/components/LivePreviewListener'
|
||||
@@ -57,13 +56,7 @@ export default async function RootLayout({ children, params }: Args) {
|
||||
<body>
|
||||
<Providers>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<AdminBar
|
||||
adminBarProps={{
|
||||
preview: isEnabled,
|
||||
}}
|
||||
/>
|
||||
<LivePreviewListener />
|
||||
|
||||
<Header locale={locale} />
|
||||
{children}
|
||||
<Footer locale={locale} />
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
.admin-bar {
|
||||
@include small-break {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadAdminBarProps } from 'payload-admin-bar'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
import { useSelectedLayoutSegments } from 'next/navigation'
|
||||
import { PayloadAdminBar } from 'payload-admin-bar'
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import './index.scss'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
const baseClass = 'admin-bar'
|
||||
|
||||
const collectionLabels = {
|
||||
pages: {
|
||||
plural: 'Pages',
|
||||
singular: 'Page',
|
||||
},
|
||||
posts: {
|
||||
plural: 'Posts',
|
||||
singular: 'Post',
|
||||
},
|
||||
projects: {
|
||||
plural: 'Projects',
|
||||
singular: 'Project',
|
||||
},
|
||||
}
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
}> = (props) => {
|
||||
const { adminBarProps } = props || {}
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const [show, setShow] = useState(false)
|
||||
const collection = collectionLabels?.[segments?.[1]] ? segments?.[1] : 'pages'
|
||||
const router = useRouter()
|
||||
const t = useTranslations()
|
||||
|
||||
const onAuthChange = React.useCallback((user) => {
|
||||
setShow(user?.id)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(baseClass, 'py-2 bg-black text-white', {
|
||||
block: show,
|
||||
hidden: !show,
|
||||
})}
|
||||
>
|
||||
<div className="container">
|
||||
<PayloadAdminBar
|
||||
{...adminBarProps}
|
||||
className="py-2 text-white"
|
||||
classNames={{
|
||||
controls: 'font-medium text-white',
|
||||
logo: 'text-white',
|
||||
user: 'text-white',
|
||||
}}
|
||||
cmsURL={process.env.NEXT_PUBLIC_SERVER_URL}
|
||||
collection={collection}
|
||||
collectionLabels={{
|
||||
plural: collectionLabels[collection]?.plural || 'Pages',
|
||||
singular: collectionLabels[collection]?.singular || 'Page',
|
||||
}}
|
||||
logo={<span>{t('dashboard')}</span>}
|
||||
onAuthChange={onAuthChange}
|
||||
onPreviewExit={() => {
|
||||
fetch('/next/exit-preview').then(() => {
|
||||
router.push('/')
|
||||
router.refresh()
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
zIndex: 'unset',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,54 +12,57 @@ const withBundleAnalyzer = bundleAnalyzer({
|
||||
})
|
||||
|
||||
const config = withBundleAnalyzer(
|
||||
withPayload({
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
fullySpecified: true,
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
withPayload(
|
||||
{
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
fullySpecified: true,
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
// @todo remove in 4.0 - will behave like this by default in 4.0
|
||||
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
destination: '/admin',
|
||||
permanent: false,
|
||||
source: '/',
|
||||
},
|
||||
]
|
||||
},
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
},
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
'.cjs': ['.cts', '.cjs'],
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
// Ignore sentry warnings when not wrapped with withSentryConfig
|
||||
webpackConfig.ignoreWarnings = [
|
||||
...(webpackConfig.ignoreWarnings ?? []),
|
||||
{ file: /esm\/platform\/node\/instrumentation.js/ },
|
||||
{ module: /esm\/platform\/node\/instrumentation.js/ },
|
||||
]
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
// @todo remove in 4.0 - will behave like this by default in 4.0
|
||||
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
destination: '/admin',
|
||||
permanent: false,
|
||||
source: '/',
|
||||
},
|
||||
]
|
||||
},
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
},
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
'.cjs': ['.cts', '.cjs'],
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
// Ignore sentry warnings when not wrapped with withSentryConfig
|
||||
webpackConfig.ignoreWarnings = [
|
||||
...(webpackConfig.ignoreWarnings ?? []),
|
||||
{ file: /esm\/platform\/node\/instrumentation.js/ },
|
||||
{ module: /esm\/platform\/node\/instrumentation.js/ },
|
||||
]
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
}),
|
||||
{ devBundleServerPackages: false },
|
||||
),
|
||||
)
|
||||
|
||||
export default process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -118,7 +118,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@next/bundle-analyzer": "15.2.0",
|
||||
"@next/bundle-analyzer": "15.2.2",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
@@ -154,7 +154,7 @@
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^10",
|
||||
"next": "15.2.0",
|
||||
"next": "15.2.2",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"playwright": "1.50.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -67,7 +67,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
return null
|
||||
}
|
||||
|
||||
transform({ adapter: this, data: doc, fields, operation: 'write' })
|
||||
transform({ adapter: this, data: doc, fields, operation: 'read' })
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect, Payload } from 'payload'
|
||||
import type { Connect, Migration, Payload } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
@@ -75,7 +75,8 @@ export const connect: Connect = async function connect(
|
||||
this.payload.logger.info('---- DROPPED TABLES ----')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
if (err.message?.match(/database .* does not exist/i) && !this.disableCreateDatabase) {
|
||||
// capitalize first char of the err msg
|
||||
this.payload.logger.info(
|
||||
@@ -83,7 +84,7 @@ export const connect: Connect = async function connect(
|
||||
)
|
||||
const isCreated = await this.createDatabase()
|
||||
|
||||
if (isCreated) {
|
||||
if (isCreated && this.connect) {
|
||||
await this.connect(options)
|
||||
return
|
||||
}
|
||||
@@ -116,6 +117,6 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
await this.migrate({ migrations: this.prodMigrations as unknown as Migration[] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,13 +79,17 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
if (args.schemaName) {
|
||||
adapterSchema = pgSchema(args.schemaName)
|
||||
} else {
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
adapterSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
const extensions = (args.extensions ?? []).reduce((acc, name) => {
|
||||
acc[name] = true
|
||||
return acc
|
||||
}, {})
|
||||
const extensions = (args.extensions ?? []).reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = true
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, boolean>,
|
||||
)
|
||||
|
||||
return createDatabaseAdapter<PostgresAdapter>({
|
||||
name: 'postgres',
|
||||
@@ -102,6 +106,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
}),
|
||||
defaultDrizzleSnapshot,
|
||||
disableCreateDatabase: args.disableCreateDatabase ?? false,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
extensions,
|
||||
@@ -123,9 +128,11 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
logger: args.logger,
|
||||
operators: operatorMap,
|
||||
pgSchema: adapterSchema,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
pool: undefined,
|
||||
poolOptions: args.pool,
|
||||
prodMigrations: args.prodMigrations,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
push: args.push,
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
@@ -163,6 +170,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
findOne,
|
||||
findVersions,
|
||||
indexes: new Set<string>(),
|
||||
@@ -180,8 +188,10 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
queryDrafts,
|
||||
rawRelations: {},
|
||||
rawTables: {},
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
rejectInitializing,
|
||||
requireDrizzleKit,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
resolveInitializing,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../payload"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const index = deepMerge(
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off',
|
||||
'@eslint-react/naming-convention/use-state': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import perfectionist from 'eslint-plugin-perfectionist'
|
||||
import { configs as regexpPluginConfigs } from 'eslint-plugin-regexp'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
||||
import payloadPlugin from '@payloadcms/eslint-plugin'
|
||||
import reactExtends from './configs/react/index.mjs'
|
||||
import jestExtends from './configs/jest/index.mjs'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/eslint-config",
|
||||
"version": "3.9.0",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload styles for ESLint and Prettier",
|
||||
"keywords": [],
|
||||
"homepage": "https://payloadcms.com",
|
||||
@@ -24,23 +24,22 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint-react/eslint-plugin": "1.16.1",
|
||||
"@eslint/js": "9.14.0",
|
||||
"@eslint-react/eslint-plugin": "1.31.0",
|
||||
"@eslint/js": "9.22.0",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
"@types/eslint": "9.6.1",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@typescript-eslint/parser": "8.14.0",
|
||||
"eslint": "9.14.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import-x": "4.4.2",
|
||||
"eslint-plugin-jest": "28.9.0",
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"@typescript-eslint/parser": "8.26.1",
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-plugin-import-x": "4.6.1",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-jest-dom": "5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-d331ba04-20250307",
|
||||
"eslint-plugin-regexp": "2.7.0",
|
||||
"globals": "16.0.0",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.14.0"
|
||||
"typescript-eslint": "8.26.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/eslint-plugin",
|
||||
"version": "3.9.0",
|
||||
"version": "3.28.0",
|
||||
"description": "Payload plugin for ESLint",
|
||||
"keywords": [],
|
||||
"homepage": "https://payloadcms.com",
|
||||
@@ -24,22 +24,21 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint-react/eslint-plugin": "1.16.1",
|
||||
"@eslint/js": "9.14.0",
|
||||
"@eslint-react/eslint-plugin": "1.31.0",
|
||||
"@eslint/js": "9.22.0",
|
||||
"@types/eslint": "9.6.1",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@typescript-eslint/parser": "8.14.0",
|
||||
"eslint": "9.14.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import-x": "4.4.2",
|
||||
"eslint-plugin-jest": "28.9.0",
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"@typescript-eslint/parser": "8.26.1",
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-plugin-import-x": "4.6.1",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-jest-dom": "5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-d331ba04-20250307",
|
||||
"eslint-plugin-regexp": "2.7.0",
|
||||
"globals": "16.0.0",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.14.0"
|
||||
"typescript-eslint": "8.26.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -107,20 +107,20 @@ export function buildMutationInputType({
|
||||
type = new GraphQLList(withNullableType({ type, field, forceNullable, parentIsLocalized }))
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type },
|
||||
[formatName(field.name)]: { type },
|
||||
}
|
||||
},
|
||||
blocks: (inputObjectTypeConfig: InputObjectTypeConfig, field: BlocksField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: GraphQLJSON },
|
||||
[formatName(field.name)]: { type: GraphQLJSON },
|
||||
}),
|
||||
checkbox: (inputObjectTypeConfig: InputObjectTypeConfig, field: CheckboxField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: GraphQLBoolean },
|
||||
[formatName(field.name)]: { type: GraphQLBoolean },
|
||||
}),
|
||||
code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -134,13 +134,13 @@ export function buildMutationInputType({
|
||||
}, inputObjectTypeConfig),
|
||||
date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -165,12 +165,12 @@ export function buildMutationInputType({
|
||||
}
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type },
|
||||
[formatName(field.name)]: { type },
|
||||
}
|
||||
},
|
||||
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -178,7 +178,7 @@ export function buildMutationInputType({
|
||||
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({
|
||||
type: field.hasMany === true ? new GraphQLList(type) : type,
|
||||
field,
|
||||
@@ -190,7 +190,7 @@ export function buildMutationInputType({
|
||||
},
|
||||
point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({
|
||||
type: new GraphQLList(GraphQLFloat),
|
||||
field,
|
||||
@@ -201,7 +201,7 @@ export function buildMutationInputType({
|
||||
}),
|
||||
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -247,12 +247,12 @@ export function buildMutationInputType({
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
[formatName(field.name)]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
}
|
||||
},
|
||||
richText: (inputObjectTypeConfig: InputObjectTypeConfig, field: RichTextField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -292,7 +292,7 @@ export function buildMutationInputType({
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type },
|
||||
[formatName(field.name)]: { type },
|
||||
}
|
||||
},
|
||||
tabs: (inputObjectTypeConfig: InputObjectTypeConfig, field: TabsField) => {
|
||||
@@ -336,7 +336,7 @@ export function buildMutationInputType({
|
||||
},
|
||||
text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({
|
||||
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
||||
field,
|
||||
@@ -347,7 +347,7 @@ export function buildMutationInputType({
|
||||
}),
|
||||
textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -393,7 +393,7 @@ export function buildMutationInputType({
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
[formatName(field.name)]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,58 +1,12 @@
|
||||
import type { GraphQLFieldConfig, GraphQLType } from 'graphql'
|
||||
import type {
|
||||
ArrayField,
|
||||
BlocksField,
|
||||
CheckboxField,
|
||||
CodeField,
|
||||
CollapsibleField,
|
||||
DateField,
|
||||
EmailField,
|
||||
Field,
|
||||
GraphQLInfo,
|
||||
GroupField,
|
||||
JoinField,
|
||||
JSONField,
|
||||
NumberField,
|
||||
PointField,
|
||||
RadioField,
|
||||
RelationshipField,
|
||||
RichTextAdapter,
|
||||
RichTextField,
|
||||
RowField,
|
||||
SanitizedConfig,
|
||||
SelectField,
|
||||
TabsField,
|
||||
TextareaField,
|
||||
TextField,
|
||||
UploadField,
|
||||
} from 'payload'
|
||||
import type { GraphQLFieldConfig } from 'graphql'
|
||||
import type { Field, GraphQLInfo, SanitizedConfig } from 'payload'
|
||||
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLEnumType,
|
||||
GraphQLFloat,
|
||||
GraphQLInt,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
GraphQLString,
|
||||
GraphQLUnionType,
|
||||
} from 'graphql'
|
||||
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
|
||||
import { combineQueries, createDataloaderCacheKey, MissingEditorProp, toWords } from 'payload'
|
||||
import { tabHasName } from 'payload/shared'
|
||||
import { GraphQLObjectType } from 'graphql'
|
||||
|
||||
import type { Context } from '../resolvers/types.js'
|
||||
|
||||
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
|
||||
import { combineParentName } from '../utilities/combineParentName.js'
|
||||
import { formatName } from '../utilities/formatName.js'
|
||||
import { formatOptions } from '../utilities/formatOptions.js'
|
||||
import { isFieldNullable } from './isFieldNullable.js'
|
||||
import { withNullableType } from './withNullableType.js'
|
||||
import { fieldToSchemaMap } from './fieldToSchemaMap.js'
|
||||
|
||||
export type ObjectTypeConfig = {
|
||||
[path: string]: GraphQLFieldConfig<any, any>
|
||||
[path: string]: GraphQLFieldConfig<any, any, any>
|
||||
}
|
||||
|
||||
type Args = {
|
||||
@@ -76,867 +30,6 @@ export function buildObjectType({
|
||||
parentIsLocalized,
|
||||
parentName,
|
||||
}: Args): GraphQLObjectType {
|
||||
const fieldToSchemaMap = {
|
||||
array: (objectTypeConfig: ObjectTypeConfig, field: ArrayField) => {
|
||||
const interfaceName =
|
||||
field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
if (!graphqlResult.types.arrayTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
|
||||
graphqlResult,
|
||||
parentIsLocalized: field.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.arrayTypes[interfaceName] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphqlResult.types.arrayTypes[interfaceName]) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
|
||||
const arrayType = new GraphQLList(
|
||||
new GraphQLNonNull(graphqlResult.types.arrayTypes[interfaceName]),
|
||||
)
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: { type: withNullableType({ type: arrayType, field, parentIsLocalized }) },
|
||||
}
|
||||
},
|
||||
blocks: (objectTypeConfig: ObjectTypeConfig, field: BlocksField) => {
|
||||
const blockTypes: GraphQLObjectType<any, any>[] = (
|
||||
field.blockReferences ?? field.blocks
|
||||
).reduce((acc, _block) => {
|
||||
const blockSlug = typeof _block === 'string' ? _block : _block.slug
|
||||
if (!graphqlResult.types.blockTypes[blockSlug]) {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
|
||||
const interfaceName =
|
||||
block?.interfaceName || block?.graphQL?.singularName || toWords(block.slug, true)
|
||||
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: [
|
||||
...block.fields,
|
||||
{
|
||||
name: 'blockType',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.blockTypes[block.slug] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (graphqlResult.types.blockTypes[blockSlug]) {
|
||||
acc.push(graphqlResult.types.blockTypes[blockSlug])
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
if (blockTypes.length === 0) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
|
||||
const fullName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
const type = new GraphQLList(
|
||||
new GraphQLNonNull(
|
||||
new GraphQLUnionType({
|
||||
name: fullName,
|
||||
resolveType: (data) => graphqlResult.types.blockTypes[data.blockType].name,
|
||||
types: blockTypes,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: { type: withNullableType({ type, field, parentIsLocalized }) },
|
||||
}
|
||||
},
|
||||
checkbox: (objectTypeConfig: ObjectTypeConfig, field: CheckboxField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLBoolean, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
code: (objectTypeConfig: ObjectTypeConfig, field: CodeField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
collapsible: (objectTypeConfig: ObjectTypeConfig, field: CollapsibleField) =>
|
||||
field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => {
|
||||
const addSubField = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField(objectTypeConfigWithCollapsibleFields, subField)
|
||||
}
|
||||
return objectTypeConfigWithCollapsibleFields
|
||||
}, objectTypeConfig),
|
||||
date: (objectTypeConfig: ObjectTypeConfig, field: DateField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: DateTimeResolver, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
email: (objectTypeConfig: ObjectTypeConfig, field: EmailField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: EmailAddressResolver,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
group: (objectTypeConfig: ObjectTypeConfig, field: GroupField) => {
|
||||
const interfaceName =
|
||||
field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
|
||||
graphqlResult,
|
||||
parentIsLocalized: field.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.groupTypes[interfaceName] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: graphqlResult.types.groupTypes[interfaceName],
|
||||
resolve: (parent, args, context: Context) => {
|
||||
return {
|
||||
...parent[field.name],
|
||||
_id: parent._id ?? parent.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
join: (objectTypeConfig: ObjectTypeConfig, field: JoinField) => {
|
||||
const joinName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
const joinType = {
|
||||
type: new GraphQLObjectType({
|
||||
name: joinName,
|
||||
fields: {
|
||||
docs: {
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
|
||||
},
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
limit: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
page: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
sort: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
where: {
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: graphqlResult.collections[field.collection].graphQL.whereInputType,
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
complexity:
|
||||
typeof field?.graphQL?.complexity === 'number' ? field.graphQL.complexity : 10,
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
const { collection } = field
|
||||
const { limit, page, sort, where } = args
|
||||
const { req } = context
|
||||
|
||||
const fullWhere = combineQueries(where, {
|
||||
[field.on]: { equals: parent._id ?? parent.id },
|
||||
})
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
throw new Error('GraphQL with array of join.field.collection is not implemented')
|
||||
}
|
||||
|
||||
return await req.payload.find({
|
||||
collection,
|
||||
depth: 0,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
limit,
|
||||
locale: req.locale,
|
||||
overrideAccess: false,
|
||||
page,
|
||||
req,
|
||||
sort,
|
||||
where: fullWhere,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: joinType,
|
||||
}
|
||||
},
|
||||
json: (objectTypeConfig: ObjectTypeConfig, field: JSONField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
number: (objectTypeConfig: ObjectTypeConfig, field: NumberField) => {
|
||||
const type = field?.name === 'id' ? GraphQLInt : GraphQLFloat
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: field?.hasMany === true ? new GraphQLList(type) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
point: (objectTypeConfig: ObjectTypeConfig, field: PointField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
radio: (objectTypeConfig: ObjectTypeConfig, field: RadioField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: new GraphQLEnumType({
|
||||
name: combineParentName(parentName, field.name),
|
||||
values: formatOptions(field),
|
||||
}),
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
relationship: (objectTypeConfig: ObjectTypeConfig, field: RelationshipField) => {
|
||||
const { relationTo } = field
|
||||
const isRelatedToManyCollections = Array.isArray(relationTo)
|
||||
const hasManyValues = field.hasMany
|
||||
const relationshipName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
let type
|
||||
let relationToType = null
|
||||
|
||||
const graphQLCollections = config.collections.filter(
|
||||
(collectionConfig) => collectionConfig.graphQL !== false,
|
||||
)
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
relationToType = new GraphQLEnumType({
|
||||
name: `${relationshipName}_RelationTo`,
|
||||
values: relationTo
|
||||
.filter((relation) =>
|
||||
graphQLCollections.some((collection) => collection.slug === relation),
|
||||
)
|
||||
.reduce(
|
||||
(relations, relation) => ({
|
||||
...relations,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
})
|
||||
|
||||
// Only pass collections that are GraphQL enabled
|
||||
const types = relationTo
|
||||
.filter((relation) =>
|
||||
graphQLCollections.some((collection) => collection.slug === relation),
|
||||
)
|
||||
.map((relation) => graphqlResult.collections[relation]?.graphQL.type)
|
||||
|
||||
type = new GraphQLObjectType({
|
||||
name: `${relationshipName}_Relationship`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: relationToType,
|
||||
},
|
||||
value: {
|
||||
type: new GraphQLUnionType({
|
||||
name: relationshipName,
|
||||
resolveType(data) {
|
||||
return graphqlResult.collections[data.collection].graphQL.type.name
|
||||
},
|
||||
types,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
;({ type } = graphqlResult.collections[relationTo].graphQL)
|
||||
}
|
||||
|
||||
// If the relationshipType is undefined at this point,
|
||||
// it can be assumed that this blockType can have a relationship
|
||||
// to itself. Therefore, we set the relationshipType equal to the blockType
|
||||
// that is currently being created.
|
||||
|
||||
type = type || newlyCreatedBlockType
|
||||
|
||||
const relationshipArgs: {
|
||||
draft?: unknown
|
||||
fallbackLocale?: unknown
|
||||
limit?: unknown
|
||||
locale?: unknown
|
||||
page?: unknown
|
||||
where?: unknown
|
||||
} = {}
|
||||
|
||||
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo])
|
||||
.filter((relation) => graphQLCollections.some((collection) => collection.slug === relation))
|
||||
.some((relation) => graphqlResult.collections[relation].config.versions?.drafts)
|
||||
|
||||
if (relationsUseDrafts) {
|
||||
relationshipArgs.draft = {
|
||||
type: GraphQLBoolean,
|
||||
}
|
||||
}
|
||||
|
||||
if (config.localization) {
|
||||
relationshipArgs.locale = {
|
||||
type: graphqlResult.types.localeInputType,
|
||||
}
|
||||
|
||||
relationshipArgs.fallbackLocale = {
|
||||
type: graphqlResult.types.fallbackLocaleInputType,
|
||||
}
|
||||
}
|
||||
|
||||
const relationship = {
|
||||
type: withNullableType({
|
||||
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
args: relationshipArgs,
|
||||
extensions: {
|
||||
complexity:
|
||||
typeof field?.graphQL?.complexity === 'number' ? field.graphQL.complexity : 10,
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
const value = parent[field.name]
|
||||
const locale = args.locale || context.req.locale
|
||||
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
||||
let relatedCollectionSlug = field.relationTo
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
if (hasManyValues) {
|
||||
const results = []
|
||||
const resultPromises = []
|
||||
|
||||
const createPopulationPromise = async (relatedDoc, i) => {
|
||||
let id = relatedDoc
|
||||
let collectionSlug = field.relationTo
|
||||
const isValidGraphQLCollection = isRelatedToManyCollections
|
||||
? graphQLCollections.some((collection) => collectionSlug.includes(collection.slug))
|
||||
: graphQLCollections.some((collection) => collectionSlug === collection.slug)
|
||||
|
||||
if (isValidGraphQLCollection) {
|
||||
if (isRelatedToManyCollections) {
|
||||
collectionSlug = relatedDoc.relationTo
|
||||
id = relatedDoc.value
|
||||
}
|
||||
|
||||
const result = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: collectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (result) {
|
||||
if (isRelatedToManyCollections) {
|
||||
results[i] = {
|
||||
relationTo: collectionSlug,
|
||||
value: {
|
||||
...result,
|
||||
collection: collectionSlug,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
results[i] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
value.forEach((relatedDoc, i) => {
|
||||
resultPromises.push(createPopulationPromise(relatedDoc, i))
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(resultPromises)
|
||||
return results
|
||||
}
|
||||
|
||||
let id = value
|
||||
if (isRelatedToManyCollections && value) {
|
||||
id = value.value
|
||||
relatedCollectionSlug = value.relationTo
|
||||
}
|
||||
|
||||
if (id) {
|
||||
if (
|
||||
graphQLCollections.some((collection) => collection.slug === relatedCollectionSlug)
|
||||
) {
|
||||
const relatedDocument = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: relatedCollectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (relatedDocument) {
|
||||
if (isRelatedToManyCollections) {
|
||||
return {
|
||||
relationTo: relatedCollectionSlug,
|
||||
value: {
|
||||
...relatedDocument,
|
||||
collection: relatedCollectionSlug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return relatedDocument
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: relationship,
|
||||
}
|
||||
},
|
||||
richText: (objectTypeConfig: ObjectTypeConfig, field: RichTextField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
args: {
|
||||
depth: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
let depth = config.defaultDepth
|
||||
if (typeof args.depth !== 'undefined') {
|
||||
depth = args.depth
|
||||
}
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
// RichText fields have their own depth argument in GraphQL.
|
||||
// This is why the populationPromise (which populates richtext fields like uploads and relationships)
|
||||
// is run here again, with the provided depth.
|
||||
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
|
||||
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
|
||||
if (editor?.graphQLPopulationPromises) {
|
||||
const fieldPromises = []
|
||||
const populationPromises = []
|
||||
const populateDepth =
|
||||
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
|
||||
|
||||
editor?.graphQLPopulationPromises({
|
||||
context,
|
||||
depth: populateDepth,
|
||||
draft: args.draft,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany: false,
|
||||
flattenLocales: false,
|
||||
overrideAccess: false,
|
||||
parentIsLocalized,
|
||||
populationPromises,
|
||||
req: context.req,
|
||||
showHiddenFields: false,
|
||||
siblingDoc: parent,
|
||||
})
|
||||
await Promise.all(fieldPromises)
|
||||
await Promise.all(populationPromises)
|
||||
}
|
||||
|
||||
return parent[field.name]
|
||||
},
|
||||
},
|
||||
}),
|
||||
row: (objectTypeConfig: ObjectTypeConfig, field: RowField) =>
|
||||
field.fields.reduce((objectTypeConfigWithRowFields, subField) => {
|
||||
const addSubField = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField(objectTypeConfigWithRowFields, subField)
|
||||
}
|
||||
return objectTypeConfigWithRowFields
|
||||
}, objectTypeConfig),
|
||||
select: (objectTypeConfig: ObjectTypeConfig, field: SelectField) => {
|
||||
const fullName = combineParentName(parentName, field.name)
|
||||
|
||||
let type: GraphQLType = new GraphQLEnumType({
|
||||
name: fullName,
|
||||
values: formatOptions(field),
|
||||
})
|
||||
|
||||
type = field.hasMany ? new GraphQLList(new GraphQLNonNull(type)) : type
|
||||
type = withNullableType({ type, field, forceNullable, parentIsLocalized })
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: { type },
|
||||
}
|
||||
},
|
||||
tabs: (objectTypeConfig: ObjectTypeConfig, field: TabsField) =>
|
||||
field.tabs.reduce((tabSchema, tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
const interfaceName =
|
||||
tab?.interfaceName || combineParentName(parentName, toWords(tab.name, true))
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: tab.fields,
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
parentIsLocalized: tab.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.groupTypes[interfaceName] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
return tabSchema
|
||||
}
|
||||
|
||||
return {
|
||||
...tabSchema,
|
||||
[tab.name]: {
|
||||
type: graphqlResult.types.groupTypes[interfaceName],
|
||||
resolve(parent, args, context: Context) {
|
||||
return {
|
||||
...parent[tab.name],
|
||||
_id: parent._id ?? parent.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...tabSchema,
|
||||
...tab.fields.reduce((subFieldSchema, subField) => {
|
||||
const addSubField = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField(subFieldSchema, subField)
|
||||
}
|
||||
return subFieldSchema
|
||||
}, tabSchema),
|
||||
}
|
||||
}, objectTypeConfig),
|
||||
text: (objectTypeConfig: ObjectTypeConfig, field: TextField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
textarea: (objectTypeConfig: ObjectTypeConfig, field: TextareaField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
|
||||
const { relationTo } = field
|
||||
const isRelatedToManyCollections = Array.isArray(relationTo)
|
||||
const hasManyValues = field.hasMany
|
||||
const relationshipName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
let type
|
||||
let relationToType = null
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
relationToType = new GraphQLEnumType({
|
||||
name: `${relationshipName}_RelationTo`,
|
||||
values: relationTo.reduce(
|
||||
(relations, relation) => ({
|
||||
...relations,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
})
|
||||
|
||||
const types = relationTo.map((relation) => graphqlResult.collections[relation].graphQL.type)
|
||||
|
||||
type = new GraphQLObjectType({
|
||||
name: `${relationshipName}_Relationship`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: relationToType,
|
||||
},
|
||||
value: {
|
||||
type: new GraphQLUnionType({
|
||||
name: relationshipName,
|
||||
resolveType(data) {
|
||||
return graphqlResult.collections[data.collection].graphQL.type.name
|
||||
},
|
||||
types,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
;({ type } = graphqlResult.collections[relationTo].graphQL)
|
||||
}
|
||||
|
||||
// If the relationshipType is undefined at this point,
|
||||
// it can be assumed that this blockType can have a relationship
|
||||
// to itself. Therefore, we set the relationshipType equal to the blockType
|
||||
// that is currently being created.
|
||||
|
||||
type = type || newlyCreatedBlockType
|
||||
|
||||
const relationshipArgs: {
|
||||
draft?: unknown
|
||||
fallbackLocale?: unknown
|
||||
limit?: unknown
|
||||
locale?: unknown
|
||||
page?: unknown
|
||||
where?: unknown
|
||||
} = {}
|
||||
|
||||
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo]).some(
|
||||
(relation) => graphqlResult.collections[relation].config.versions?.drafts,
|
||||
)
|
||||
|
||||
if (relationsUseDrafts) {
|
||||
relationshipArgs.draft = {
|
||||
type: GraphQLBoolean,
|
||||
}
|
||||
}
|
||||
|
||||
if (config.localization) {
|
||||
relationshipArgs.locale = {
|
||||
type: graphqlResult.types.localeInputType,
|
||||
}
|
||||
|
||||
relationshipArgs.fallbackLocale = {
|
||||
type: graphqlResult.types.fallbackLocaleInputType,
|
||||
}
|
||||
}
|
||||
|
||||
const relationship = {
|
||||
type: withNullableType({
|
||||
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
args: relationshipArgs,
|
||||
extensions: {
|
||||
complexity:
|
||||
typeof field?.graphQL?.complexity === 'number' ? field.graphQL.complexity : 10,
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
const value = parent[field.name]
|
||||
const locale = args.locale || context.req.locale
|
||||
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
||||
let relatedCollectionSlug = field.relationTo
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
if (hasManyValues) {
|
||||
const results = []
|
||||
const resultPromises = []
|
||||
|
||||
const createPopulationPromise = async (relatedDoc, i) => {
|
||||
let id = relatedDoc
|
||||
let collectionSlug = field.relationTo
|
||||
|
||||
if (isRelatedToManyCollections) {
|
||||
collectionSlug = relatedDoc.relationTo
|
||||
id = relatedDoc.value
|
||||
}
|
||||
|
||||
const result = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (result) {
|
||||
if (isRelatedToManyCollections) {
|
||||
results[i] = {
|
||||
relationTo: collectionSlug,
|
||||
value: {
|
||||
...result,
|
||||
collection: collectionSlug,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
results[i] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
value.forEach((relatedDoc, i) => {
|
||||
resultPromises.push(createPopulationPromise(relatedDoc, i))
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(resultPromises)
|
||||
return results
|
||||
}
|
||||
|
||||
let id = value
|
||||
if (isRelatedToManyCollections && value) {
|
||||
id = value.value
|
||||
relatedCollectionSlug = value.relationTo
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const relatedDocument = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: relatedCollectionSlug,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (relatedDocument) {
|
||||
if (isRelatedToManyCollections) {
|
||||
return {
|
||||
relationTo: relatedCollectionSlug,
|
||||
value: {
|
||||
...relatedDocument,
|
||||
collection: relatedCollectionSlug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return relatedDocument
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: relationship,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const objectSchema = {
|
||||
name,
|
||||
fields: () =>
|
||||
@@ -949,7 +42,16 @@ export function buildObjectType({
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
...fieldSchema(objectTypeConfig, field),
|
||||
...fieldSchema({
|
||||
config,
|
||||
field,
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
newlyCreatedBlockType,
|
||||
objectTypeConfig,
|
||||
parentIsLocalized,
|
||||
parentName,
|
||||
}),
|
||||
}
|
||||
}, baseFields),
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ const buildFields = (label, fieldsToBuild) =>
|
||||
|
||||
return {
|
||||
...builtFields,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: new GraphQLObjectType({
|
||||
name: `${label}_${fieldName}`,
|
||||
fields: objectTypeFields,
|
||||
|
||||
1086
packages/graphql/src/schema/fieldToSchemaMap.ts
Normal file
1086
packages/graphql/src/schema/fieldToSchemaMap.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { NotFoundPage } from '../views/NotFound/index.js'
|
||||
export { generatePageMetadata, type GenerateViewMetadata, RootPage } from '../views/Root/index.js'
|
||||
export { type GenerateViewMetadata, RootPage } from '../views/Root/index.js'
|
||||
export { generatePageMetadata } from '../views/Root/metadata.js'
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { IconConfig, MetaConfig } from 'payload'
|
||||
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
|
||||
import type { MetaConfig } from 'payload'
|
||||
|
||||
import { payloadFaviconDark, payloadFaviconLight, staticOGImage } from '@payloadcms/ui/assets'
|
||||
import * as qs from 'qs-esm'
|
||||
|
||||
const defaultOpenGraph = {
|
||||
const defaultOpenGraph: Metadata['openGraph'] = {
|
||||
description:
|
||||
'Payload is a headless CMS and application framework built with TypeScript, Node.js, and React.',
|
||||
siteName: 'Payload App',
|
||||
title: 'Payload App',
|
||||
}
|
||||
|
||||
export const meta = async (args: { serverURL: string } & MetaConfig): Promise<any> => {
|
||||
const {
|
||||
defaultOGImageType,
|
||||
description,
|
||||
icons: customIcons,
|
||||
keywords,
|
||||
openGraph: openGraphFromProps,
|
||||
serverURL,
|
||||
title,
|
||||
titleSuffix,
|
||||
} = args
|
||||
export const generateMetadata = async (
|
||||
args: { serverURL: string } & MetaConfig,
|
||||
): Promise<Metadata> => {
|
||||
const { defaultOGImageType, serverURL, titleSuffix, ...rest } = args
|
||||
|
||||
const payloadIcons: IconConfig[] = [
|
||||
{
|
||||
type: 'image/png',
|
||||
rel: 'icon',
|
||||
sizes: '32x32',
|
||||
url: typeof payloadFaviconDark === 'object' ? payloadFaviconDark?.src : payloadFaviconDark,
|
||||
},
|
||||
{
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
rel: 'icon',
|
||||
sizes: '32x32',
|
||||
url: typeof payloadFaviconLight === 'object' ? payloadFaviconLight?.src : payloadFaviconLight,
|
||||
},
|
||||
]
|
||||
/**
|
||||
* @todo find a way to remove the type assertion here.
|
||||
* It is a result of needing to `DeepCopy` the `MetaConfig` type from Payload.
|
||||
* This is required for the `DeepRequired` from `Config` to `SanitizedConfig`.
|
||||
*/
|
||||
const incomingMetadata = rest as Metadata
|
||||
|
||||
let icons = payloadIcons
|
||||
const icons: Metadata['icons'] =
|
||||
incomingMetadata.icons ||
|
||||
([
|
||||
{
|
||||
type: 'image/png',
|
||||
rel: 'icon',
|
||||
sizes: '32x32',
|
||||
url: typeof payloadFaviconDark === 'object' ? payloadFaviconDark?.src : payloadFaviconDark,
|
||||
},
|
||||
{
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
rel: 'icon',
|
||||
sizes: '32x32',
|
||||
url:
|
||||
typeof payloadFaviconLight === 'object' ? payloadFaviconLight?.src : payloadFaviconLight,
|
||||
},
|
||||
] satisfies Array<Icon>)
|
||||
|
||||
if (customIcons && typeof customIcons === 'object' && Array.isArray(customIcons)) {
|
||||
icons = customIcons
|
||||
}
|
||||
const metaTitle: Metadata['title'] = [incomingMetadata.title, titleSuffix]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const metaTitle = [title, titleSuffix].filter(Boolean).join(' ')
|
||||
|
||||
const ogTitle = `${typeof openGraphFromProps?.title === 'string' ? openGraphFromProps.title : title} ${titleSuffix}`
|
||||
const ogTitle = `${typeof incomingMetadata.openGraph?.title === 'string' ? incomingMetadata.openGraph.title : incomingMetadata.title} ${titleSuffix}`
|
||||
|
||||
const mergedOpenGraph: Metadata['openGraph'] = {
|
||||
...(defaultOpenGraph || {}),
|
||||
@@ -59,7 +59,8 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
|
||||
height: 630,
|
||||
url: `/api/og${qs.stringify(
|
||||
{
|
||||
description: openGraphFromProps?.description || defaultOpenGraph.description,
|
||||
description:
|
||||
incomingMetadata.openGraph?.description || defaultOpenGraph.description,
|
||||
title: ogTitle,
|
||||
},
|
||||
{
|
||||
@@ -84,13 +85,12 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
|
||||
}
|
||||
: {}),
|
||||
title: ogTitle,
|
||||
...(openGraphFromProps || {}),
|
||||
...(incomingMetadata.openGraph || {}),
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
description,
|
||||
...incomingMetadata,
|
||||
icons,
|
||||
keywords,
|
||||
metadataBase: new URL(
|
||||
serverURL ||
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL ||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { MetaConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
/**
|
||||
* @todo Remove the `MetaConfig` type assertions. They are currently required because of how the `Metadata` type from `next` consumes the `URL` type.
|
||||
*/
|
||||
export const generateAPIViewMetadata: GenerateEditViewMetadata = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
@@ -17,24 +22,24 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
: ''
|
||||
|
||||
return Promise.resolve(
|
||||
meta({
|
||||
generateMetadata({
|
||||
...(config.admin.meta || {}),
|
||||
description: `API - ${entityLabel}`,
|
||||
keywords: 'API',
|
||||
serverURL: config.serverURL,
|
||||
title: `API - ${entityLabel}`,
|
||||
...(collectionConfig
|
||||
...((collectionConfig
|
||||
? {
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.api?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
...(globalConfig
|
||||
: {}) as MetaConfig),
|
||||
...((globalConfig
|
||||
? {
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.api?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
: {}) as MetaConfig),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -16,8 +16,6 @@ import { EditView } from '../Edit/index.js'
|
||||
import { AccountClient } from './index.client.js'
|
||||
import { Settings } from './Settings/index.js'
|
||||
|
||||
export { generateAccountMetadata } from './meta.js'
|
||||
|
||||
export async function Account({ initPageResult, params, searchParams }: AdminViewServerProps) {
|
||||
const {
|
||||
languageOptions,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateAccountMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateAccountViewMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
generateMetadata({
|
||||
description: `${t('authentication:accountOfCurrentUser')}`,
|
||||
keywords: `${t('authentication:account')}`,
|
||||
serverURL: config.serverURL,
|
||||
@@ -9,8 +9,6 @@ import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { CreateFirstUserClient } from './index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
export { generateCreateFirstUserMetadata } from './meta.js'
|
||||
|
||||
export async function CreateFirstUserView({ initPageResult }: AdminViewServerProps) {
|
||||
const {
|
||||
locale,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateCreateFirstUserMetadata: GenerateViewMetadata = async ({
|
||||
export const generateCreateFirstUserViewMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}) =>
|
||||
meta({
|
||||
generateMetadata({
|
||||
description: t('authentication:createFirstUser'),
|
||||
keywords: t('general:create'),
|
||||
serverURL: config.serverURL,
|
||||
@@ -10,8 +10,6 @@ import type { DashboardViewClientProps, DashboardViewServerPropsOnly } from './D
|
||||
|
||||
import { DefaultDashboard } from './Default/index.js'
|
||||
|
||||
export { generateDashboardMetadata } from './meta.js'
|
||||
|
||||
export async function Dashboard({ initPageResult, params, searchParams }: AdminViewServerProps) {
|
||||
const {
|
||||
locale,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateDashboardMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
description: `${t('general:dashboard')} Payload`,
|
||||
keywords: `${t('general:dashboard')}, Payload`,
|
||||
serverURL: config.serverURL,
|
||||
title: t('general:dashboard'),
|
||||
...(config.admin.meta || {}),
|
||||
openGraph: {
|
||||
title: t('general:dashboard'),
|
||||
...(config.admin.meta?.openGraph || {}),
|
||||
},
|
||||
})
|
||||
17
packages/next/src/views/Dashboard/metadata.ts
Normal file
17
packages/next/src/views/Dashboard/metadata.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateDashboardViewMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}) =>
|
||||
generateMetadata({
|
||||
serverURL: config.serverURL,
|
||||
title: t('general:dashboard'),
|
||||
...config.admin.meta,
|
||||
openGraph: {
|
||||
title: t('general:dashboard'),
|
||||
...(config.admin.meta?.openGraph || {}),
|
||||
},
|
||||
})
|
||||
@@ -4,12 +4,12 @@ import type { EditConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } fro
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
|
||||
import { generateMetadata as apiMeta } from '../API/meta.js'
|
||||
import { generateMetadata as editMeta } from '../Edit/meta.js'
|
||||
import { generateMetadata as livePreviewMeta } from '../LivePreview/meta.js'
|
||||
import { generateNotFoundMeta } from '../NotFound/meta.js'
|
||||
import { generateMetadata as versionMeta } from '../Version/meta.js'
|
||||
import { generateMetadata as versionsMeta } from '../Versions/meta.js'
|
||||
import { generateAPIViewMetadata } from '../API/metadata.js'
|
||||
import { generateEditViewMetadata } from '../Edit/metadata.js'
|
||||
import { generateLivePreviewViewMetadata } from '../LivePreview/metadata.js'
|
||||
import { generateNotFoundViewMetadata } from '../NotFound/metadata.js'
|
||||
import { generateVersionViewMetadata } from '../Version/metadata.js'
|
||||
import { generateVersionsViewMetadata } from '../Versions/metadata.js'
|
||||
import { getViewsFromConfig } from './getViewsFromConfig.js'
|
||||
|
||||
export type GenerateEditViewMetadata = (
|
||||
@@ -40,7 +40,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
if (isCollection) {
|
||||
// `/:collection/:id`
|
||||
if (params.segments.length === 3) {
|
||||
fn = editMeta
|
||||
fn = generateEditViewMetadata
|
||||
}
|
||||
|
||||
// `/:collection/:id/:view`
|
||||
@@ -48,15 +48,15 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
switch (params.segments[3]) {
|
||||
case 'api':
|
||||
// `/:collection/:id/api`
|
||||
fn = apiMeta
|
||||
fn = generateAPIViewMetadata
|
||||
break
|
||||
case 'preview':
|
||||
// `/:collection/:id/preview`
|
||||
fn = livePreviewMeta
|
||||
fn = generateLivePreviewViewMetadata
|
||||
break
|
||||
case 'versions':
|
||||
// `/:collection/:id/versions`
|
||||
fn = versionsMeta
|
||||
fn = generateVersionsViewMetadata
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -68,7 +68,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
switch (params.segments[3]) {
|
||||
case 'versions':
|
||||
// `/:collection/:id/versions/:version`
|
||||
fn = versionMeta
|
||||
fn = generateVersionViewMetadata
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -79,7 +79,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
if (isGlobal) {
|
||||
// `/:global`
|
||||
if (params.segments?.length === 2) {
|
||||
fn = editMeta
|
||||
fn = generateEditViewMetadata
|
||||
}
|
||||
|
||||
// `/:global/:view`
|
||||
@@ -87,15 +87,15 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
switch (params.segments[2]) {
|
||||
case 'api':
|
||||
// `/:global/api`
|
||||
fn = apiMeta
|
||||
fn = generateAPIViewMetadata
|
||||
break
|
||||
case 'preview':
|
||||
// `/:global/preview`
|
||||
fn = livePreviewMeta
|
||||
fn = generateLivePreviewViewMetadata
|
||||
break
|
||||
case 'versions':
|
||||
// `/:global/versions`
|
||||
fn = versionsMeta
|
||||
fn = generateVersionsViewMetadata
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -104,7 +104,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
|
||||
// `/:global/versions/:version`
|
||||
if (params.segments?.length === 4 && params.segments[2] === 'versions') {
|
||||
fn = versionMeta
|
||||
fn = generateVersionViewMetadata
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
globalConfig?.admin?.components?.views?.edit?.[viewKey]
|
||||
|
||||
if (customViewConfig) {
|
||||
return editMeta({
|
||||
return generateEditViewMetadata({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
@@ -147,5 +147,5 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
}
|
||||
}
|
||||
|
||||
return generateNotFoundMeta({ config, i18n })
|
||||
return generateNotFoundViewMetadata({ config, i18n })
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
|
||||
|
||||
import { getMetaBySegment } from './getMetaBySegment.js'
|
||||
|
||||
export const generateDocumentMetadata: GenerateEditViewMetadata = async (args) =>
|
||||
export const generateDocumentViewMetadata: GenerateEditViewMetadata = async (args) =>
|
||||
getMetaBySegment(args)
|
||||
@@ -5,9 +5,12 @@ import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
/**
|
||||
* @todo Remove the type assertion. This is currently required because of how the `Metadata` type from `next` consumes the `URL` type.
|
||||
*/
|
||||
export const generateEditViewMetadata: GenerateEditViewMetadata = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
@@ -33,35 +36,35 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
const ogToUse: MetaConfig['openGraph'] = {
|
||||
title: `${isEditing ? t('general:edit') : t('general:edit')} - ${entityLabel}`,
|
||||
...(config.admin.meta.openGraph || {}),
|
||||
...(collectionConfig
|
||||
...((collectionConfig
|
||||
? {
|
||||
...(collectionConfig?.admin.meta?.openGraph || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.[view]?.meta?.openGraph || {}),
|
||||
}
|
||||
: {}),
|
||||
...(globalConfig
|
||||
: {}) as MetaConfig['openGraph']),
|
||||
...((globalConfig
|
||||
? {
|
||||
...(globalConfig?.admin.meta?.openGraph || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.[view]?.meta?.openGraph || {}),
|
||||
}
|
||||
: {}),
|
||||
: {}) as MetaConfig['openGraph']),
|
||||
}
|
||||
|
||||
return meta({
|
||||
return generateMetadata({
|
||||
...metaToUse,
|
||||
openGraph: ogToUse,
|
||||
...(collectionConfig
|
||||
...((collectionConfig
|
||||
? {
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.[view]?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
...(globalConfig
|
||||
: {}) as MetaConfig),
|
||||
...((globalConfig
|
||||
? {
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.[view]?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
: {}) as MetaConfig),
|
||||
serverURL: config.serverURL,
|
||||
})
|
||||
}
|
||||
@@ -94,6 +94,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
path: ['username'],
|
||||
preferences: { fields: {} },
|
||||
req: {
|
||||
payload: {
|
||||
@@ -124,6 +125,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
path: ['email'],
|
||||
preferences: { fields: {} },
|
||||
req: { payload: { config }, t } as unknown as PayloadRequest,
|
||||
required: true,
|
||||
|
||||
@@ -8,8 +8,6 @@ import React, { Fragment } from 'react'
|
||||
import { FormHeader } from '../../elements/FormHeader/index.js'
|
||||
import { ForgotPasswordForm } from './ForgotPasswordForm/index.js'
|
||||
|
||||
export { generateForgotPasswordMetadata } from './meta.js'
|
||||
|
||||
export const forgotPasswordBaseClass = 'forgot-password'
|
||||
|
||||
export function ForgotPasswordView({ initPageResult }: AdminViewServerProps) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateForgotPasswordMetadata: GenerateViewMetadata = async ({
|
||||
export const generateForgotPasswordViewMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}) =>
|
||||
meta({
|
||||
generateMetadata({
|
||||
description: t('authentication:forgotPassword'),
|
||||
keywords: t('authentication:forgotPassword'),
|
||||
title: t('authentication:forgotPassword'),
|
||||
@@ -18,8 +18,6 @@ import React, { Fragment } from 'react'
|
||||
import { renderListViewSlots } from './renderListViewSlots.js'
|
||||
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
|
||||
|
||||
export { generateListMetadata } from './meta.js'
|
||||
|
||||
type RenderListViewArgs = {
|
||||
customCellProps?: Record<string, any>
|
||||
disableBulkDelete?: boolean
|
||||
|
||||
@@ -5,9 +5,9 @@ import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateListMetadata = async (
|
||||
export const generateListViewMetadata = async (
|
||||
args: {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
} & Parameters<GenerateViewMetadata>[0],
|
||||
@@ -22,7 +22,7 @@ export const generateListMetadata = async (
|
||||
title = getTranslation(collectionConfig.labels.plural, i18n)
|
||||
}
|
||||
|
||||
return meta({
|
||||
return generateMetadata({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
@@ -2,16 +2,16 @@ import type { Metadata } from 'next'
|
||||
|
||||
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { generateMetadata as generateDocumentMetadata } from '../Edit/meta.js'
|
||||
import { generateEditViewMetadata } from '../Edit/metadata.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
export const generateLivePreviewViewMetadata: GenerateEditViewMetadata = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
i18n,
|
||||
isEditing,
|
||||
}): Promise<Metadata> =>
|
||||
generateDocumentMetadata({
|
||||
generateEditViewMetadata({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
@@ -8,8 +8,6 @@ import { Logo } from '../../elements/Logo/index.js'
|
||||
import { LoginForm } from './LoginForm/index.js'
|
||||
import './index.scss'
|
||||
|
||||
export { generateLoginMetadata } from './meta.js'
|
||||
|
||||
export const loginBaseClass = 'login'
|
||||
|
||||
export function LoginView({ initPageResult, params, searchParams }: AdminViewServerProps) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateLoginMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateLoginViewMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
generateMetadata({
|
||||
description: `${t('authentication:login')}`,
|
||||
keywords: `${t('authentication:login')}`,
|
||||
serverURL: config.serverURL,
|
||||
@@ -7,8 +7,6 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'logout'
|
||||
|
||||
export { generateLogoutMetadata } from './meta.js'
|
||||
|
||||
export const LogoutView: React.FC<
|
||||
{
|
||||
inactivity?: boolean
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateLogoutMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateLogoutViewMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
generateMetadata({
|
||||
description: `${t('authentication:logoutUser')}`,
|
||||
keywords: `${t('authentication:logout')}`,
|
||||
serverURL: config.serverURL,
|
||||
@@ -10,7 +10,7 @@ import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { NotFoundClient } from './index.client.js'
|
||||
|
||||
export const generatePageMetadata = async ({
|
||||
export const generateNotFoundViewMetadata = async ({
|
||||
config: configPromise,
|
||||
}: {
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||
|
||||
@@ -2,16 +2,16 @@ import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateNotFoundMeta = async ({
|
||||
export const generateNotFoundViewMetadata = async ({
|
||||
config,
|
||||
i18n,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
}): Promise<Metadata> =>
|
||||
meta({
|
||||
generateMetadata({
|
||||
description: i18n.t('general:pageNotFound'),
|
||||
keywords: `404 ${i18n.t('general:notFound')}`,
|
||||
serverURL: config.serverURL,
|
||||
@@ -11,8 +11,6 @@ import './index.scss'
|
||||
|
||||
export const resetPasswordBaseClass = 'reset-password'
|
||||
|
||||
export { generateResetPasswordMetadata } from './meta.js'
|
||||
|
||||
export function ResetPassword({ initPageResult, params }: AdminViewServerProps) {
|
||||
const { req } = initPageResult
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { Metadata } from 'next'
|
||||
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateResetPasswordMetadata: GenerateViewMetadata = async ({
|
||||
export const generateResetPasswordViewMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}): Promise<Metadata> =>
|
||||
meta({
|
||||
generateMetadata({
|
||||
description: t('authentication:resetPassword'),
|
||||
keywords: t('authentication:resetPassword'),
|
||||
serverURL: config.serverURL,
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateCustomViewMetadata = async (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
@@ -26,7 +26,7 @@ export const generateCustomViewMetadata = async (args: {
|
||||
return null
|
||||
}
|
||||
|
||||
return meta({
|
||||
return generateMetadata({
|
||||
description: `Payload`,
|
||||
keywords: `Payload`,
|
||||
serverURL: config.serverURL,
|
||||
|
||||
@@ -18,8 +18,6 @@ import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getViewFromConfig } from './getViewFromConfig.js'
|
||||
|
||||
export { generatePageMetadata } from './meta.js'
|
||||
|
||||
export type GenerateViewMetadata = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
|
||||
@@ -2,27 +2,27 @@ import type { Metadata } from 'next'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
|
||||
import { generateAccountMetadata } from '../Account/index.js'
|
||||
import { generateCreateFirstUserMetadata } from '../CreateFirstUser/index.js'
|
||||
import { generateDashboardMetadata } from '../Dashboard/index.js'
|
||||
import { generateDocumentMetadata } from '../Document/meta.js'
|
||||
import { generateForgotPasswordMetadata } from '../ForgotPassword/index.js'
|
||||
import { generateListMetadata } from '../List/index.js'
|
||||
import { generateLoginMetadata } from '../Login/index.js'
|
||||
import { generateNotFoundMeta } from '../NotFound/meta.js'
|
||||
import { generateResetPasswordMetadata } from '../ResetPassword/index.js'
|
||||
import { generateUnauthorizedMetadata } from '../Unauthorized/index.js'
|
||||
import { generateVerifyMetadata } from '../Verify/index.js'
|
||||
import { generateAccountViewMetadata } from '../Account/metadata.js'
|
||||
import { generateCreateFirstUserViewMetadata } from '../CreateFirstUser/metadata.js'
|
||||
import { generateDashboardViewMetadata } from '../Dashboard/metadata.js'
|
||||
import { generateDocumentViewMetadata } from '../Document/metadata.js'
|
||||
import { generateForgotPasswordViewMetadata } from '../ForgotPassword/metadata.js'
|
||||
import { generateListViewMetadata } from '../List/metadata.js'
|
||||
import { generateLoginViewMetadata } from '../Login/metadata.js'
|
||||
import { generateNotFoundViewMetadata } from '../NotFound/metadata.js'
|
||||
import { generateResetPasswordViewMetadata } from '../ResetPassword/metadata.js'
|
||||
import { generateUnauthorizedViewMetadata } from '../Unauthorized/metadata.js'
|
||||
import { generateVerifyViewMetadata } from '../Verify/metadata.js'
|
||||
import { generateCustomViewMetadata } from './generateCustomViewMetadata.js'
|
||||
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
const oneSegmentMeta = {
|
||||
'create-first-user': generateCreateFirstUserMetadata,
|
||||
forgot: generateForgotPasswordMetadata,
|
||||
login: generateLoginMetadata,
|
||||
logout: generateUnauthorizedMetadata,
|
||||
'logout-inactivity': generateUnauthorizedMetadata,
|
||||
unauthorized: generateUnauthorizedMetadata,
|
||||
'create-first-user': generateCreateFirstUserViewMetadata,
|
||||
forgot: generateForgotPasswordViewMetadata,
|
||||
login: generateLoginViewMetadata,
|
||||
logout: generateUnauthorizedViewMetadata,
|
||||
'logout-inactivity': generateUnauthorizedViewMetadata,
|
||||
unauthorized: generateUnauthorizedViewMetadata,
|
||||
}
|
||||
|
||||
type Args = {
|
||||
@@ -68,7 +68,7 @@ export const generatePageMetadata = async ({
|
||||
|
||||
switch (segments.length) {
|
||||
case 0: {
|
||||
meta = await generateDashboardMetadata({ config, i18n })
|
||||
meta = await generateDashboardViewMetadata({ config, i18n })
|
||||
break
|
||||
}
|
||||
case 1: {
|
||||
@@ -83,7 +83,7 @@ export const generatePageMetadata = async ({
|
||||
break
|
||||
} else if (segmentOne === 'account') {
|
||||
// --> /account
|
||||
meta = await generateAccountMetadata({ config, i18n })
|
||||
meta = await generateAccountViewMetadata({ config, i18n })
|
||||
break
|
||||
}
|
||||
break
|
||||
@@ -91,14 +91,14 @@ export const generatePageMetadata = async ({
|
||||
case 2: {
|
||||
if (`/${segmentOne}` === config.admin.routes.reset) {
|
||||
// --> /reset/:token
|
||||
meta = await generateResetPasswordMetadata({ config, i18n })
|
||||
meta = await generateResetPasswordViewMetadata({ config, i18n })
|
||||
}
|
||||
if (isCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
meta = await generateListMetadata({ collectionConfig, config, i18n })
|
||||
meta = await generateListViewMetadata({ collectionConfig, config, i18n })
|
||||
} else if (isGlobal) {
|
||||
// --> /globals/:globalSlug
|
||||
meta = await generateDocumentMetadata({
|
||||
meta = await generateDocumentViewMetadata({
|
||||
config,
|
||||
globalConfig,
|
||||
i18n,
|
||||
@@ -110,7 +110,7 @@ export const generatePageMetadata = async ({
|
||||
default: {
|
||||
if (segmentTwo === 'verify') {
|
||||
// --> /:collectionSlug/verify/:token
|
||||
meta = await generateVerifyMetadata({ config, i18n })
|
||||
meta = await generateVerifyViewMetadata({ config, i18n })
|
||||
} else if (isCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
@@ -118,14 +118,14 @@ export const generatePageMetadata = async ({
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:version
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
meta = await generateDocumentMetadata({ collectionConfig, config, i18n, params })
|
||||
meta = await generateDocumentViewMetadata({ collectionConfig, config, i18n, params })
|
||||
} else if (isGlobal) {
|
||||
// Custom Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/versions/:version
|
||||
// --> /globals/:globalSlug/preview
|
||||
// --> /globals/:globalSlug/api
|
||||
meta = await generateDocumentMetadata({
|
||||
meta = await generateDocumentViewMetadata({
|
||||
config,
|
||||
globalConfig,
|
||||
i18n,
|
||||
@@ -151,7 +151,7 @@ export const generatePageMetadata = async ({
|
||||
viewConfig,
|
||||
})
|
||||
} else {
|
||||
meta = await generateNotFoundMeta({ config, i18n })
|
||||
meta = await generateNotFoundViewMetadata({ config, i18n })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import React from 'react'
|
||||
import { FormHeader } from '../../elements/FormHeader/index.js'
|
||||
import './index.scss'
|
||||
|
||||
export { generateUnauthorizedMetadata } from './meta.js'
|
||||
|
||||
const baseClass = 'unauthorized'
|
||||
|
||||
export function UnauthorizedView({ initPageResult }: AdminViewServerProps) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateUnauthorizedMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateUnauthorizedViewMetadata: GenerateViewMetadata = async ({
|
||||
config,
|
||||
i18n: { t },
|
||||
}) =>
|
||||
generateMetadata({
|
||||
description: t('error:unauthorized'),
|
||||
keywords: t('error:unauthorized'),
|
||||
serverURL: config.serverURL,
|
||||
@@ -9,8 +9,6 @@ import './index.scss'
|
||||
|
||||
export const verifyBaseClass = 'verify'
|
||||
|
||||
export { generateVerifyMetadata } from './meta.js'
|
||||
|
||||
export async function Verify({ initPageResult, params, searchParams }: AdminViewServerProps) {
|
||||
// /:collectionSlug/verify/:token
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateVerifyMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
export const generateVerifyViewMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
generateMetadata({
|
||||
description: t('authentication:verifyUser'),
|
||||
keywords: t('authentication:verify'),
|
||||
serverURL: config.serverURL,
|
||||
@@ -6,9 +6,12 @@ import { formatDate } from '@payloadcms/ui/shared'
|
||||
|
||||
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
/**
|
||||
* @todo Remove the `MetaConfig` type assertions. They are currently required because of how the `Metadata` type from `next` consumes the `URL` type.
|
||||
*/
|
||||
export const generateVersionViewMetadata: GenerateEditViewMetadata = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
@@ -47,12 +50,12 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
...(config.admin.meta || {}),
|
||||
description: t('version:viewingVersionGlobal', { entityLabel }),
|
||||
title: `${t('version:version')}${formattedCreatedAt ? ` - ${formattedCreatedAt}` : ''}${entityLabel}`,
|
||||
...(globalConfig?.admin?.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.version?.meta || {}),
|
||||
...((globalConfig?.admin?.meta || {}) as MetaConfig),
|
||||
...((globalConfig?.admin?.components?.views?.edit?.version?.meta || {}) as MetaConfig),
|
||||
}
|
||||
}
|
||||
|
||||
return meta({
|
||||
return generateMetadata({
|
||||
...metaToUse,
|
||||
serverURL: config.serverURL,
|
||||
})
|
||||
@@ -5,9 +5,12 @@ import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
/**
|
||||
* @todo Remove the `MetaConfig` type assertions. They are currently required because of how the `Metadata` type from `next` consumes the `URL` type.
|
||||
*/
|
||||
export const generateVersionsViewMetadata: GenerateEditViewMetadata = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
@@ -48,12 +51,12 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
...(config.admin.meta || {}),
|
||||
description: t('version:viewingVersionsGlobal', { entitySlug: globalConfig.slug }),
|
||||
title: `${t('version:versions')} - ${entityLabel}`,
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.versions?.meta || {}),
|
||||
...((globalConfig?.admin.meta || {}) as MetaConfig),
|
||||
...((globalConfig?.admin?.components?.views?.edit?.versions?.meta || {}) as MetaConfig),
|
||||
}
|
||||
}
|
||||
|
||||
return meta({
|
||||
return generateMetadata({
|
||||
...metaToUse,
|
||||
serverURL: config.serverURL,
|
||||
})
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* @param {import('next').NextConfig} nextConfig
|
||||
* @param {Object} [options] - Optional configuration options
|
||||
* @param {boolean} [options.devBundleServerPackages] - Whether to bundle server packages in development mode. @default true
|
||||
*
|
||||
* @returns {import('next').NextConfig}
|
||||
* */
|
||||
export const withPayload = (nextConfig = {}) => {
|
||||
export const withPayload = (nextConfig = {}, options = {}) => {
|
||||
const env = nextConfig?.env || {}
|
||||
|
||||
if (nextConfig.experimental?.staleTimes?.dynamic) {
|
||||
@@ -99,6 +101,32 @@ export const withPayload = (nextConfig = {}) => {
|
||||
'libsql',
|
||||
'pino-pretty',
|
||||
'graphql',
|
||||
// Do not bundle server-only packages during dev to improve compile speed
|
||||
...(process.env.npm_lifecycle_event === 'dev' && options.devBundleServerPackages === false
|
||||
? [
|
||||
'payload',
|
||||
'@payloadcms/db-mongodb',
|
||||
'@payloadcms/db-postgres',
|
||||
'@payloadcms/db-sqlite',
|
||||
'@payloadcms/db-vercel-postgres',
|
||||
'@payloadcms/drizzle',
|
||||
'@payloadcms/email-nodemailer',
|
||||
'@payloadcms/email-resend',
|
||||
'@payloadcms/graphql',
|
||||
'@payloadcms/payload-cloud',
|
||||
'@payloadcms/plugin-cloud-storage',
|
||||
'@payloadcms/plugin-redirects',
|
||||
'@payloadcms/plugin-sentry',
|
||||
'@payloadcms/plugin-stripe',
|
||||
// TODO: Add the following packages, excluding their /client subpath exports, once Next.js supports it
|
||||
// @payloadcms/richtext-lexical
|
||||
//'@payloadcms/storage-azure',
|
||||
//'@payloadcms/storage-gcs',
|
||||
//'@payloadcms/storage-s3',
|
||||
//'@payloadcms/storage-uploadthing',
|
||||
//'@payloadcms/storage-vercel-blob',
|
||||
]
|
||||
: []),
|
||||
],
|
||||
webpack: (webpackConfig, webpackOptions) => {
|
||||
const incomingWebpackConfig =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'
|
||||
import * as AWS from '@aws-sdk/client-s3'
|
||||
import { S3 } from '@aws-sdk/client-s3'
|
||||
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers'
|
||||
|
||||
import { authAsCognitoUser } from './authAsCognitoUser.js'
|
||||
|
||||
export type GetStorageClient = () => Promise<{
|
||||
identityID: string
|
||||
storageClient: AWS.S3
|
||||
storageClient: S3
|
||||
}>
|
||||
|
||||
export const refreshSession = async () => {
|
||||
@@ -33,7 +33,7 @@ export const refreshSession = async () => {
|
||||
// @ts-expect-error - Incorrect AWS types
|
||||
const identityID = credentials.identityId
|
||||
|
||||
const storageClient = new AWS.S3({
|
||||
const storageClient = new S3({
|
||||
credentials,
|
||||
region: process.env.PAYLOAD_CLOUD_BUCKET_REGION,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.27.0",
|
||||
"version": "3.28.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
|
||||
import type { ServerProps } from '../../config/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||
import type { ClientFieldWithOptionalType, ServerComponentProps } from './Field.js'
|
||||
|
||||
export type DescriptionFunction = ({ t }: { t: TFunction }) => string
|
||||
|
||||
@@ -33,7 +32,7 @@ export type FieldDescriptionServerProps<
|
||||
clientField: TFieldClient
|
||||
readonly field: TFieldServer
|
||||
} & GenericDescriptionProps &
|
||||
Partial<ServerProps>
|
||||
ServerComponentProps
|
||||
|
||||
export type FieldDescriptionClientProps<
|
||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ServerProps } from '../../config/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||
import type { ClientFieldWithOptionalType, ServerComponentProps } from './Field.js'
|
||||
|
||||
export type GenericErrorProps = {
|
||||
readonly alignCaret?: 'center' | 'left' | 'right'
|
||||
@@ -22,7 +21,7 @@ export type FieldErrorServerProps<
|
||||
clientField: TFieldClient
|
||||
readonly field: TFieldServer
|
||||
} & GenericErrorProps &
|
||||
Partial<ServerProps>
|
||||
ServerComponentProps
|
||||
|
||||
export type FieldErrorClientComponent<
|
||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ServerProps, StaticLabel } from '../../config/types.js'
|
||||
import type { StaticLabel } from '../../config/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||
import type { ClientFieldWithOptionalType, ServerComponentProps } from './Field.js'
|
||||
|
||||
export type GenericLabelProps = {
|
||||
readonly as?: 'label' | 'span'
|
||||
@@ -26,7 +26,7 @@ export type FieldLabelServerProps<
|
||||
clientField: TFieldClient
|
||||
readonly field: TFieldServer
|
||||
} & GenericLabelProps &
|
||||
Partial<ServerProps>
|
||||
ServerComponentProps
|
||||
|
||||
export type SanitizedLabelProps<TFieldClient extends ClientFieldWithOptionalType> = Omit<
|
||||
FieldLabelClientProps<TFieldClient>,
|
||||
|
||||
@@ -37,6 +37,7 @@ export const generatePasswordSaltHash = async ({
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'submit',
|
||||
path: ['password'],
|
||||
preferences: { fields: {} },
|
||||
req,
|
||||
required: true,
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import type { PayloadComponent } from '../../index.js'
|
||||
import { addPayloadComponentToImportMap } from './utilities/addPayloadComponentToImportMap.js'
|
||||
import { getImportMapToBaseDirPath } from './utilities/getImportMapToBaseDirPath.js'
|
||||
|
||||
describe('addPayloadComponentToImportMap', () => {
|
||||
let importMap: Record<string, string>
|
||||
let imports: Record<
|
||||
string,
|
||||
{
|
||||
path: string
|
||||
specifier: string
|
||||
}
|
||||
>
|
||||
|
||||
beforeEach(() => {
|
||||
importMap = {}
|
||||
imports = {}
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
function componentPathTest({
|
||||
baseDir,
|
||||
importMapFilePath,
|
||||
payloadComponent,
|
||||
expectedPath,
|
||||
expectedSpecifier,
|
||||
expectedImportMapToBaseDirPath,
|
||||
}: {
|
||||
baseDir: string
|
||||
importMapFilePath: string
|
||||
payloadComponent: PayloadComponent
|
||||
expectedPath: string
|
||||
expectedImportMapToBaseDirPath: string
|
||||
expectedSpecifier: string
|
||||
}) {
|
||||
const importMapToBaseDirPath = getImportMapToBaseDirPath({
|
||||
baseDir,
|
||||
importMapPath: importMapFilePath,
|
||||
})
|
||||
|
||||
expect(importMapToBaseDirPath).toBe(expectedImportMapToBaseDirPath)
|
||||
|
||||
const { path, specifier } =
|
||||
addPayloadComponentToImportMap({
|
||||
importMapToBaseDirPath,
|
||||
importMap,
|
||||
imports,
|
||||
payloadComponent,
|
||||
}) ?? {}
|
||||
|
||||
expect(path).toBe(expectedPath)
|
||||
expect(specifier).toBe(expectedSpecifier)
|
||||
}
|
||||
|
||||
it('relative path with import map partially in base dir', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/myPackage/test/myTest',
|
||||
importMapFilePath: '/myPackage/app/(payload)/importMap.js',
|
||||
payloadComponent: './MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../../test/myTest/',
|
||||
expectedPath: '../../test/myTest/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path with import map partially in base dir 2', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/myPackage/test/myTest',
|
||||
importMapFilePath: '/myPackage/test/prod/app/(payload)/importMap.js',
|
||||
payloadComponent: {
|
||||
path: './MyComponent.js#MyExport',
|
||||
},
|
||||
expectedImportMapToBaseDirPath: '../../../myTest/',
|
||||
expectedPath: '../../../myTest/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path with import map partially in base dir 3', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/myPackage/test/myTest',
|
||||
importMapFilePath: '/myPackage/test/prod/app/(payload)/importMap.js',
|
||||
payloadComponent: {
|
||||
path: '../otherTest/MyComponent.js',
|
||||
exportName: 'MyExport',
|
||||
},
|
||||
expectedImportMapToBaseDirPath: '../../../myTest/',
|
||||
expectedPath: '../../../otherTest/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path with import map within base dir', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/myPackage/test/myTest',
|
||||
importMapFilePath: '/myPackage/test/myTest/prod/app/(payload)/importMap.js',
|
||||
payloadComponent: './MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../../../',
|
||||
expectedPath: '../../../MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path with import map not in base dir', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest',
|
||||
importMapFilePath: '/app/(payload)/importMap.js',
|
||||
payloadComponent: './MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../../test/myTest/',
|
||||
expectedPath: '../../test/myTest/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path with import map not in base dir 2', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest',
|
||||
importMapFilePath: '/app/(payload)/importMap.js',
|
||||
payloadComponent: '../myOtherTest/MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../../test/myTest/',
|
||||
expectedPath: '../../test/myOtherTest/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path with import map not in base dir, baseDir ending with slash', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest/',
|
||||
importMapFilePath: '/app/(payload)/importMap.js',
|
||||
payloadComponent: './MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../../test/myTest/',
|
||||
expectedPath: '../../test/myTest/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path with import map not in base dir, component starting with slash', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest',
|
||||
importMapFilePath: '/app/(payload)/importMap.js',
|
||||
payloadComponent: '/MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../../test/myTest/',
|
||||
expectedPath: '../../test/myTest/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('aliased path', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest',
|
||||
importMapFilePath: '/app/(payload)/importMap.js',
|
||||
payloadComponent: '@components/MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../../test/myTest/',
|
||||
expectedPath: '@components/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
it('aliased path in PayloadComponent object', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/',
|
||||
importMapFilePath: '/app/(payload)/importMap.js',
|
||||
payloadComponent: {
|
||||
path: '@components/MyComponent.js',
|
||||
},
|
||||
expectedImportMapToBaseDirPath: '../../test/',
|
||||
expectedPath: '@components/MyComponent.js',
|
||||
expectedSpecifier: 'default',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path import starting with slash, going up', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest',
|
||||
importMapFilePath: '/test/myTest/app/importMap.js',
|
||||
payloadComponent: '/../MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../',
|
||||
expectedPath: '../../MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('relative path import starting with dot-slash, going up', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest',
|
||||
importMapFilePath: '/test/myTest/app/importMap.js',
|
||||
payloadComponent: './../MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: '../',
|
||||
expectedPath: '../../MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('importMap and baseDir in same directory', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest',
|
||||
importMapFilePath: '/test/myTest/importMap.js',
|
||||
payloadComponent: './MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: './',
|
||||
expectedPath: './MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
|
||||
it('baseDir within importMap dir', () => {
|
||||
componentPathTest({
|
||||
baseDir: '/test/myTest/components',
|
||||
importMapFilePath: '/test/myTest/importMap.js',
|
||||
payloadComponent: './MyComponent.js#MyExport',
|
||||
expectedImportMapToBaseDirPath: './components/',
|
||||
expectedPath: './components/MyComponent.js',
|
||||
expectedSpecifier: 'MyExport',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,13 @@
|
||||
import crypto from 'crypto'
|
||||
/* eslint-disable no-console */
|
||||
import fs from 'fs'
|
||||
import process from 'node:process'
|
||||
import path from 'path'
|
||||
|
||||
import type { PayloadComponent, SanitizedConfig } from '../../config/types.js'
|
||||
|
||||
import { iterateConfig } from './iterateConfig.js'
|
||||
import { parsePayloadComponent } from './parsePayloadComponent.js'
|
||||
import { addPayloadComponentToImportMap } from './utilities/addPayloadComponentToImportMap.js'
|
||||
import { getImportMapToBaseDirPath } from './utilities/getImportMapToBaseDirPath.js'
|
||||
import { resolveImportMapFilePath } from './utilities/resolveImportMapFilePath.js'
|
||||
|
||||
type ImportIdentifier = string
|
||||
type ImportSpecifier = string
|
||||
@@ -37,54 +38,6 @@ export type ImportMap = {
|
||||
[path: UserImportPath]: any
|
||||
}
|
||||
|
||||
export function addPayloadComponentToImportMap({
|
||||
baseDir,
|
||||
importMap,
|
||||
imports,
|
||||
payloadComponent,
|
||||
}: {
|
||||
baseDir: string
|
||||
importMap: InternalImportMap
|
||||
imports: Imports
|
||||
payloadComponent: PayloadComponent
|
||||
}) {
|
||||
if (!payloadComponent) {
|
||||
return
|
||||
}
|
||||
const { exportName, path: componentPath } = parsePayloadComponent(payloadComponent)
|
||||
|
||||
if (importMap[componentPath + '#' + exportName]) {
|
||||
return
|
||||
}
|
||||
|
||||
const importIdentifier =
|
||||
exportName + '_' + crypto.createHash('md5').update(componentPath).digest('hex')
|
||||
|
||||
// e.g. if baseDir is /test/fields and componentPath is /components/Field.tsx
|
||||
// then path needs to be /test/fields/components/Field.tsx NOT /users/username/project/test/fields/components/Field.tsx
|
||||
// so we need to append baseDir to componentPath
|
||||
|
||||
if (componentPath.startsWith('.') || componentPath.startsWith('/')) {
|
||||
const normalizedBaseDir = baseDir.replace(/\\/g, '/')
|
||||
|
||||
const finalPath = normalizedBaseDir.startsWith('/../')
|
||||
? `${normalizedBaseDir}${componentPath.slice(1)}`
|
||||
: path.posix.join(normalizedBaseDir, componentPath.slice(1))
|
||||
|
||||
imports[importIdentifier] = {
|
||||
path:
|
||||
componentPath.startsWith('.') || componentPath.startsWith('/') ? finalPath : componentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
} else {
|
||||
imports[importIdentifier] = {
|
||||
path: componentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
}
|
||||
importMap[componentPath + '#' + exportName] = importIdentifier
|
||||
}
|
||||
|
||||
export type AddToImportMap = (payloadComponent: PayloadComponent | PayloadComponent[]) => void
|
||||
|
||||
export async function generateImportMap(
|
||||
@@ -100,49 +53,21 @@ export async function generateImportMap(
|
||||
const importMap: InternalImportMap = {}
|
||||
const imports: Imports = {}
|
||||
|
||||
// Determine the root directory of the project - usually the directory where the src or app folder is located
|
||||
const rootDir = process.env.ROOT_DIR ?? process.cwd()
|
||||
|
||||
// get componentsBaseDir.
|
||||
// E.g.:
|
||||
// config.admin.importMap.baseDir = /test/fields/
|
||||
// rootDir: /
|
||||
// componentsBaseDir = /test/fields/
|
||||
const baseDir = config.admin.importMap.baseDir ?? process.cwd()
|
||||
|
||||
// or
|
||||
const importMapFilePath = resolveImportMapFilePath({
|
||||
adminRoute: config.routes.admin,
|
||||
importMapFile: config?.admin?.importMap?.importMapFile,
|
||||
rootDir,
|
||||
})
|
||||
|
||||
// E.g.:
|
||||
// config.admin.importMap.baseDir = /test/fields/
|
||||
// rootDir: /test
|
||||
// componentsBaseDir = /fields/
|
||||
|
||||
// or
|
||||
// config.admin.importMap.baseDir = /
|
||||
// rootDir: /
|
||||
// componentsBaseDir = /
|
||||
|
||||
// E.g.:
|
||||
// config.admin.importMap.baseDir = /test/fields/
|
||||
// rootDir: /test/fields/prod
|
||||
// componentsBaseDir = ../
|
||||
|
||||
// Check if rootDir is a subdirectory of baseDir
|
||||
const baseDir = config.admin.importMap.baseDir
|
||||
const isSubdirectory = path.relative(baseDir, rootDir).startsWith('..')
|
||||
|
||||
let componentsBaseDir
|
||||
|
||||
if (isSubdirectory) {
|
||||
// Get the relative path from rootDir to baseDir
|
||||
componentsBaseDir = path.relative(rootDir, baseDir)
|
||||
} else {
|
||||
// If rootDir is not a subdirectory, just return baseDir relative to rootDir
|
||||
componentsBaseDir = `/${path.relative(rootDir, baseDir)}`
|
||||
}
|
||||
|
||||
// Ensure result has a trailing slash
|
||||
if (!componentsBaseDir.endsWith('/')) {
|
||||
componentsBaseDir += '/'
|
||||
}
|
||||
const importMapToBaseDirPath = getImportMapToBaseDirPath({
|
||||
baseDir,
|
||||
importMapPath: importMapFilePath,
|
||||
})
|
||||
|
||||
const addToImportMap: AddToImportMap = (payloadComponent) => {
|
||||
if (!payloadComponent) {
|
||||
@@ -157,16 +82,16 @@ export async function generateImportMap(
|
||||
if (Array.isArray(payloadComponent)) {
|
||||
for (const component of payloadComponent) {
|
||||
addPayloadComponentToImportMap({
|
||||
baseDir: componentsBaseDir,
|
||||
importMap,
|
||||
importMapToBaseDirPath,
|
||||
imports,
|
||||
payloadComponent: component,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
addPayloadComponentToImportMap({
|
||||
baseDir: componentsBaseDir,
|
||||
importMap,
|
||||
importMapToBaseDirPath,
|
||||
imports,
|
||||
payloadComponent,
|
||||
})
|
||||
@@ -183,56 +108,26 @@ export async function generateImportMap(
|
||||
|
||||
await writeImportMap({
|
||||
componentMap: importMap,
|
||||
config,
|
||||
fileName: 'importMap.js',
|
||||
force: options?.force,
|
||||
importMap: imports,
|
||||
importMapFilePath,
|
||||
log: shouldLog,
|
||||
rootDir,
|
||||
})
|
||||
}
|
||||
|
||||
export async function writeImportMap({
|
||||
componentMap,
|
||||
config,
|
||||
fileName,
|
||||
force,
|
||||
importMap,
|
||||
importMapFilePath,
|
||||
log,
|
||||
rootDir,
|
||||
}: {
|
||||
componentMap: InternalImportMap
|
||||
config: SanitizedConfig
|
||||
fileName: string
|
||||
force?: boolean
|
||||
importMap: Imports
|
||||
importMapFilePath: string
|
||||
log?: boolean
|
||||
rootDir: string
|
||||
}) {
|
||||
let importMapFilePath: string | undefined = undefined
|
||||
|
||||
if (config?.admin?.importMap?.importMapFile?.length) {
|
||||
if (!fs.existsSync(config.admin.importMap.importMapFile)) {
|
||||
throw new Error(
|
||||
`Could not find the import map file at ${config.admin.importMap.importMapFile}`,
|
||||
)
|
||||
}
|
||||
importMapFilePath = config.admin.importMap.importMapFile
|
||||
} else {
|
||||
const appLocation = path.resolve(rootDir, `app/(payload)${config.routes.admin}/`)
|
||||
const srcAppLocation = path.resolve(rootDir, `src/app/(payload)${config.routes.admin}/`)
|
||||
|
||||
if (fs.existsSync(appLocation)) {
|
||||
importMapFilePath = path.resolve(appLocation, fileName)
|
||||
} else if (fs.existsSync(srcAppLocation)) {
|
||||
importMapFilePath = path.resolve(srcAppLocation, fileName)
|
||||
} else {
|
||||
throw new Error(
|
||||
`Could not find Payload import map folder. Looked in ${appLocation} and ${srcAppLocation}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const imports: string[] = []
|
||||
for (const [identifier, { path, specifier }] of Object.entries(importMap)) {
|
||||
imports.push(`import { ${specifier} as ${identifier} } from '${path}'`)
|
||||
|
||||
@@ -76,7 +76,7 @@ export function iterateConfig({
|
||||
if (config.admin?.components?.views) {
|
||||
if (Object.keys(config.admin?.components?.views)?.length) {
|
||||
for (const key in config.admin?.components?.views) {
|
||||
const adminViewConfig: AdminViewConfig = config.admin?.components?.views[key]
|
||||
const adminViewConfig = config.admin?.components?.views[key]
|
||||
addToImportMap(adminViewConfig?.Component)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
|
||||
import type { PayloadComponent } from '../../../config/types.js'
|
||||
import type { Imports, InternalImportMap } from '../index.js'
|
||||
|
||||
import { parsePayloadComponent } from './parsePayloadComponent.js'
|
||||
|
||||
/**
|
||||
* Normalizes the component path based on the import map's base directory path.
|
||||
*/
|
||||
function getAdjustedComponentPath(importMapToBaseDirPath: string, componentPath: string): string {
|
||||
// Normalize input paths to use forward slashes
|
||||
const normalizedBasePath = importMapToBaseDirPath.replace(/\\/g, '/')
|
||||
const normalizedComponentPath = componentPath.replace(/\\/g, '/')
|
||||
|
||||
// Base path starts with './' - preserve the './' prefix
|
||||
// => import map is in a subdirectory of the base directory, or in the same directory as the base directory
|
||||
if (normalizedBasePath.startsWith('./')) {
|
||||
// Remove './' from component path if it exists
|
||||
const cleanComponentPath = normalizedComponentPath.startsWith('./')
|
||||
? normalizedComponentPath.substring(2)
|
||||
: normalizedComponentPath
|
||||
|
||||
// Join the paths to preserve the './' prefix
|
||||
return `${normalizedBasePath}${cleanComponentPath}`
|
||||
}
|
||||
|
||||
return path.posix.join(normalizedBasePath, normalizedComponentPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a payload component to the import map.
|
||||
*/
|
||||
export function addPayloadComponentToImportMap({
|
||||
importMap,
|
||||
importMapToBaseDirPath,
|
||||
imports,
|
||||
payloadComponent,
|
||||
}: {
|
||||
importMap: InternalImportMap
|
||||
importMapToBaseDirPath: string
|
||||
imports: Imports
|
||||
payloadComponent: PayloadComponent
|
||||
}): {
|
||||
path: string
|
||||
specifier: string
|
||||
} | null {
|
||||
if (!payloadComponent) {
|
||||
return null
|
||||
}
|
||||
const { exportName, path: componentPath } = parsePayloadComponent(payloadComponent)
|
||||
|
||||
if (importMap[componentPath + '#' + exportName]) {
|
||||
return null
|
||||
}
|
||||
|
||||
const importIdentifier =
|
||||
exportName + '_' + crypto.createHash('md5').update(componentPath).digest('hex')
|
||||
|
||||
importMap[componentPath + '#' + exportName] = importIdentifier
|
||||
|
||||
const isRelativePath = componentPath.startsWith('.') || componentPath.startsWith('/')
|
||||
|
||||
if (isRelativePath) {
|
||||
const adjustedComponentPath = getAdjustedComponentPath(importMapToBaseDirPath, componentPath)
|
||||
|
||||
imports[importIdentifier] = {
|
||||
path: adjustedComponentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
return {
|
||||
path: adjustedComponentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
} else {
|
||||
// Tsconfig alias or package import, e.g. '@payloadcms/ui' or '@/components/MyComponent'
|
||||
imports[importIdentifier] = {
|
||||
path: componentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
return {
|
||||
path: componentPath,
|
||||
specifier: exportName,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PayloadComponent } from '../../config/types.js'
|
||||
import type { ImportMap } from './index.js'
|
||||
|
||||
import type { PayloadComponent } from '../../../config/types.js'
|
||||
import type { ImportMap } from '../index.js'
|
||||
import { parsePayloadComponent } from './parsePayloadComponent.js'
|
||||
|
||||
export const getFromImportMap = <TOutput>(args: {
|
||||
@@ -0,0 +1,39 @@
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Returns the path that navigates from the import map file to the base directory.
|
||||
* This can then be prepended to relative paths in the import map to get the full, absolute path.
|
||||
*/
|
||||
export function getImportMapToBaseDirPath({
|
||||
baseDir,
|
||||
importMapPath,
|
||||
}: {
|
||||
/**
|
||||
* Absolute path to the base directory
|
||||
*/
|
||||
baseDir: string
|
||||
/**
|
||||
* Absolute path to the import map file
|
||||
*/
|
||||
importMapPath: string
|
||||
}): string {
|
||||
const importMapDir = path.dirname(importMapPath)
|
||||
|
||||
// 1. Direct relative path from `importMapDir` -> `baseDir`
|
||||
let relativePath = path.relative(importMapDir, baseDir).replace(/\\/g, '/')
|
||||
|
||||
// 2. If they're the same directory, path.relative will be "", so use "./"
|
||||
if (!relativePath) {
|
||||
relativePath = './'
|
||||
} // Add ./ prefix for subdirectories of the current directory
|
||||
else if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) {
|
||||
relativePath = `./${relativePath}`
|
||||
}
|
||||
|
||||
// 3. For consistency ensure a trailing slash
|
||||
if (!relativePath.endsWith('/')) {
|
||||
relativePath += '/'
|
||||
}
|
||||
|
||||
return relativePath
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { PayloadComponent } from '../../config/types.js'
|
||||
import type { PayloadComponent } from '../../../config/types.js'
|
||||
|
||||
export function parsePayloadComponent(PayloadComponent: PayloadComponent): {
|
||||
exportName: string
|
||||
@@ -0,0 +1,38 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Returns the path to the import map file. If the import map file is not found, it throws an error.
|
||||
*/
|
||||
export function resolveImportMapFilePath({
|
||||
adminRoute = '/admin',
|
||||
importMapFile,
|
||||
rootDir,
|
||||
}: {
|
||||
adminRoute?: string
|
||||
importMapFile?: string
|
||||
rootDir: string
|
||||
}) {
|
||||
let importMapFilePath: string | undefined = undefined
|
||||
|
||||
if (importMapFile?.length) {
|
||||
if (!fs.existsSync(importMapFile)) {
|
||||
throw new Error(`Could not find the import map file at ${importMapFile}`)
|
||||
}
|
||||
importMapFilePath = importMapFile
|
||||
} else {
|
||||
const appLocation = path.resolve(rootDir, `app/(payload)${adminRoute}/`)
|
||||
const srcAppLocation = path.resolve(rootDir, `src/app/(payload)${adminRoute}/`)
|
||||
|
||||
if (fs.existsSync(appLocation)) {
|
||||
importMapFilePath = path.resolve(appLocation, 'importMap.js')
|
||||
} else if (fs.existsSync(srcAppLocation)) {
|
||||
importMapFilePath = path.resolve(srcAppLocation, 'importMap.js')
|
||||
} else {
|
||||
throw new Error(
|
||||
`Could not find Payload import map folder. Looked in ${appLocation} and ${srcAppLocation}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
return importMapFilePath
|
||||
}
|
||||
@@ -327,6 +327,11 @@ export type CollectionAdminOptions = {
|
||||
* Custom description for collection. This will also be used as JSDoc for the generated types
|
||||
*/
|
||||
description?: EntityDescription
|
||||
/**
|
||||
* Disable the Copy To Locale button in the edit document view
|
||||
* @default false
|
||||
*/
|
||||
disableCopyToLocale?: boolean
|
||||
enableRichTextLink?: boolean
|
||||
enableRichTextRelationship?: boolean
|
||||
/**
|
||||
@@ -544,6 +549,10 @@ export type SanitizedJoins = {
|
||||
[collectionSlug: string]: SanitizedJoin[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo remove the `DeepRequired` in v4.
|
||||
* We don't actually guarantee that all properties are set when sanitizing configs.
|
||||
*/
|
||||
export interface SanitizedCollectionConfig
|
||||
extends Omit<
|
||||
DeepRequired<CollectionConfig>,
|
||||
@@ -557,7 +566,6 @@ export interface SanitizedCollectionConfig
|
||||
* Rows / collapsible / tabs w/o name `fields` merged to top, UIs are excluded
|
||||
*/
|
||||
flattenedFields: FlattenedField[]
|
||||
|
||||
/**
|
||||
* Object of collections to join 'Join Fields object keyed by collection
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user