Compare commits
11 Commits
fix/resolv
...
v3.39.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef51a7cf3 | ||
|
|
0f7dc38012 | ||
|
|
c720ce3c08 | ||
|
|
3a73a67ef4 | ||
|
|
4c6fde0e89 | ||
|
|
c1c0db3b01 | ||
|
|
00667faf8d | ||
|
|
898e97ed17 | ||
|
|
8142a00da6 | ||
|
|
08a3dfbbcb | ||
|
|
fc83823e5d |
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -118,6 +118,13 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts folder-view",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Folder View",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts localization",
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
||||
@@ -132,6 +132,7 @@ The following options are available:
|
||||
| `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. |
|
||||
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
|
||||
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
|
||||
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
|
||||
@@ -84,6 +84,7 @@ The following options are available:
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| `folders` | An optional object to configure global folder settings. [More details](../folders/overview). |
|
||||
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
|
||||
105
docs/folders/overview.mdx
Normal file
105
docs/folders/overview.mdx
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Folders
|
||||
label: Folders
|
||||
order: 10
|
||||
desc: Folders allow you to group documents across collections, and are a great way to organize your content.
|
||||
keywords: folders, folder, content organization
|
||||
---
|
||||
|
||||
Folders allow you to group documents across collections, and are a great way to organize your content. Folders are built on top of relationship fields, when you enable folders on a collection, Payload adds a hidden relationship field `folders`, that relates to a folder — or no folder. Folders also have the `folder` field, allowing folders to be nested within other folders.
|
||||
|
||||
The configuration for folders is done in two places, the collection config and the Payload config. The collection config is where you enable folders, and the Payload config is where you configure the global folder settings.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** The Folders feature is currently in beta and may be subject to
|
||||
change in minor versions updates prior to being stable.
|
||||
</Banner>
|
||||
|
||||
## Folder Configuration
|
||||
|
||||
On the payload config, you can configure the following settings under the `folders` property:
|
||||
|
||||
```ts
|
||||
// Type definition
|
||||
|
||||
type RootFoldersConfiguration = {
|
||||
/**
|
||||
* An array of functions to be ran when the folder collection is initialized
|
||||
* This allows plugins to modify the collection configuration
|
||||
*/
|
||||
collectionOverrides?: (({
|
||||
collection,
|
||||
}: {
|
||||
collection: CollectionConfig
|
||||
}) => CollectionConfig | Promise<CollectionConfig>)[]
|
||||
/**
|
||||
* Ability to view hidden fields and collections related to folders
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean
|
||||
/**
|
||||
* The Folder field name
|
||||
*
|
||||
* @default "folder"
|
||||
*/
|
||||
fieldName?: string
|
||||
/**
|
||||
* Slug for the folder collection
|
||||
*
|
||||
* @default "payload-folders"
|
||||
*/
|
||||
slug?: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// Example usage
|
||||
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
folders: {
|
||||
// highlight-start
|
||||
debug: true, // optional
|
||||
collectionOverrides: [
|
||||
async ({ collection }) => {
|
||||
return collection
|
||||
},
|
||||
], // optional
|
||||
fieldName: 'folder', // optional
|
||||
slug: 'payload-folders', // optional
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Collection Configuration
|
||||
|
||||
To enable folders on a collection, you need to set the `admin.folders` property to `true` on the collection config. This will add a hidden relationship field to the collection that relates to a folder — or no folder.
|
||||
|
||||
```ts
|
||||
// Type definition
|
||||
|
||||
type CollectionFoldersConfiguration = boolean
|
||||
```
|
||||
|
||||
```ts
|
||||
// Example usage
|
||||
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
// highlight-start
|
||||
admin: {
|
||||
folders: true, // defaults to false
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
@@ -81,7 +81,7 @@ To install a Database Adapter, you can run **one** of the following commands:
|
||||
|
||||
#### 2. Copy Payload files into your Next.js app folder
|
||||
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](<https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)>) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
|
||||
@@ -142,32 +142,33 @@ import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
| ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`BoldFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`ChecklistFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
| Feature Name | Included by default | Description |
|
||||
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`BoldFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`ChecklistFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
| **`EXPERIMENTAL_TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
|
||||
|
||||
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(dirname, '../../package.json'), 'utf-8'))
|
||||
export const PACKAGE_VERSION = packageJson.version
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ProjectTemplate } from '../types.js'
|
||||
|
||||
import { error, info } from '../utils/log.js'
|
||||
import { PACKAGE_VERSION } from './constants.js'
|
||||
|
||||
export function validateTemplate(templateName: string): boolean {
|
||||
export function validateTemplate({ templateName }: { templateName: string }): boolean {
|
||||
const validTemplates = getValidTemplates()
|
||||
if (!validTemplates.map((t) => t.name).includes(templateName)) {
|
||||
error(`'${templateName}' is not a valid template.`)
|
||||
@@ -20,13 +19,13 @@ export function getValidTemplates(): ProjectTemplate[] {
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
description: 'Blank 3.0 Template',
|
||||
url: `https://github.com/payloadcms/payload/templates/blank#v${PACKAGE_VERSION}`,
|
||||
url: `https://github.com/payloadcms/payload/templates/blank#main`,
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'starter',
|
||||
description: 'Website Template',
|
||||
url: `https://github.com/payloadcms/payload/templates/website#v${PACKAGE_VERSION}`,
|
||||
url: `https://github.com/payloadcms/payload/templates/website#main`,
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import execa from 'execa'
|
||||
import fse from 'fs-extra'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
@@ -9,6 +8,7 @@ const dirname = path.dirname(filename)
|
||||
import type { NextAppDetails } from '../types.js'
|
||||
|
||||
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
|
||||
import { getLatestPackageVersion } from '../utils/getLatestPackageVersion.js'
|
||||
import { info } from '../utils/log.js'
|
||||
import { getPackageManager } from './get-package-manager.js'
|
||||
import { installPackages } from './install-packages.js'
|
||||
@@ -36,15 +36,8 @@ export async function updatePayloadInProject(
|
||||
|
||||
const packageManager = await getPackageManager({ projectDir })
|
||||
|
||||
// Fetch latest Payload version from npm
|
||||
const { exitCode: getLatestVersionExitCode, stdout: latestPayloadVersion } = await execa('npm', [
|
||||
'show',
|
||||
'payload',
|
||||
'version',
|
||||
])
|
||||
if (getLatestVersionExitCode !== 0) {
|
||||
throw new Error('Failed to fetch latest Payload version')
|
||||
}
|
||||
// Fetch latest Payload version
|
||||
const latestPayloadVersion = await getLatestPackageVersion({ packageName: 'payload' })
|
||||
|
||||
if (payloadVersion === latestPayloadVersion) {
|
||||
return { message: `Payload v${payloadVersion} is already up to date.`, success: true }
|
||||
|
||||
@@ -8,7 +8,6 @@ import path from 'path'
|
||||
import type { CliArgs } from './types.js'
|
||||
|
||||
import { configurePayloadConfig } from './lib/configure-payload-config.js'
|
||||
import { PACKAGE_VERSION } from './lib/constants.js'
|
||||
import { createProject } from './lib/create-project.js'
|
||||
import { parseExample } from './lib/examples.js'
|
||||
import { generateSecret } from './lib/generate-secret.js'
|
||||
@@ -20,6 +19,7 @@ import { parseTemplate } from './lib/parse-template.js'
|
||||
import { selectDb } from './lib/select-db.js'
|
||||
import { getValidTemplates, validateTemplate } from './lib/templates.js'
|
||||
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
|
||||
import { getLatestPackageVersion } from './utils/getLatestPackageVersion.js'
|
||||
import { debug, error, info } from './utils/log.js'
|
||||
import {
|
||||
feedbackOutro,
|
||||
@@ -78,13 +78,18 @@ export class Main {
|
||||
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
const debugFlag = this.args['--debug']
|
||||
|
||||
const LATEST_VERSION = await getLatestPackageVersion({
|
||||
debug: debugFlag,
|
||||
packageName: 'payload',
|
||||
})
|
||||
|
||||
if (this.args['--help']) {
|
||||
helpMessage()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const debugFlag = this.args['--debug']
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n')
|
||||
p.intro(chalk.bgCyan(chalk.black(' create-payload-app ')))
|
||||
@@ -200,7 +205,7 @@ export class Main {
|
||||
|
||||
const templateArg = this.args['--template']
|
||||
if (templateArg) {
|
||||
const valid = validateTemplate(templateArg)
|
||||
const valid = validateTemplate({ templateName: templateArg })
|
||||
if (!valid) {
|
||||
helpMessage()
|
||||
process.exit(1)
|
||||
@@ -230,7 +235,7 @@ export class Main {
|
||||
}
|
||||
|
||||
if (debugFlag) {
|
||||
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${PACKAGE_VERSION}`)
|
||||
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${LATEST_VERSION}`)
|
||||
}
|
||||
|
||||
if (!exampleArg) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Fetches the latest version of a package from the NPM registry.
|
||||
*
|
||||
* Used in determining the latest version of Payload to use in the generated templates.
|
||||
*/
|
||||
export async function getLatestPackageVersion({
|
||||
debug = false,
|
||||
packageName = 'payload',
|
||||
}: {
|
||||
debug?: boolean
|
||||
/**
|
||||
* Package name to fetch the latest version for based on the NPM registry URL
|
||||
*
|
||||
* Eg. for `'payload'`, it will fetch the version from `https://registry.npmjs.org/payload`
|
||||
*
|
||||
* @default 'payload'
|
||||
*/
|
||||
packageName?: string
|
||||
}) {
|
||||
try {
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}`)
|
||||
const data = await response.json()
|
||||
const latestVersion = data['dist-tags'].latest
|
||||
|
||||
if (debug) {
|
||||
console.log(`Found latest version of ${packageName}: ${latestVersion}`)
|
||||
}
|
||||
|
||||
return latestVersion
|
||||
} catch (error) {
|
||||
console.error('Error fetching Payload version:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"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.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"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.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"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.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { Link } from '@payloadcms/ui'
|
||||
import { Button } from '@payloadcms/ui'
|
||||
import { useParams, usePathname, useSearchParams } from 'next/navigation.js'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,6 @@ export const DocumentTabLink: React.FC<{
|
||||
children?: React.ReactNode
|
||||
href: string
|
||||
isActive?: boolean
|
||||
isCollection?: boolean
|
||||
newTab?: boolean
|
||||
}> = ({
|
||||
adminRoute,
|
||||
@@ -54,19 +53,17 @@ export const DocumentTabLink: React.FC<{
|
||||
isActiveFromProps
|
||||
|
||||
return (
|
||||
<li
|
||||
<Button
|
||||
aria-label={ariaLabel}
|
||||
buttonStyle="tab"
|
||||
className={[baseClass, isActive && `${baseClass}--active`].filter(Boolean).join(' ')}
|
||||
disabled={isActive}
|
||||
el={!isActive || href !== pathname ? 'link' : 'div'}
|
||||
newTab={newTab}
|
||||
size="medium"
|
||||
to={!isActive || href !== pathname ? hrefWithLocale : undefined}
|
||||
>
|
||||
<Link
|
||||
className={`${baseClass}__link`}
|
||||
href={!isActive || href !== pathname ? hrefWithLocale : ''}
|
||||
prefetch={false}
|
||||
{...(newTab && { rel: 'noopener noreferrer', target: '_blank' })}
|
||||
tabIndex={isActive ? -1 : 0}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,74 +1,24 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.doc-tab {
|
||||
@extend %h5;
|
||||
position: relative;
|
||||
|
||||
&__link {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
// Use a pseudo element for the accessability so that it doesn't take up DOM space
|
||||
// Also because the parent element has `overflow: hidden` which would clip an outline
|
||||
&:focus-visible::after {
|
||||
content: '';
|
||||
border: var(--accessibility-outline);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--style-radius-s);
|
||||
background-color: var(--theme-elevation-50);
|
||||
opacity: 0;
|
||||
}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.doc-tab__count {
|
||||
.pill-version-count {
|
||||
background-color: var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
font-weight: 600;
|
||||
&::before {
|
||||
opacity: 1;
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
.doc-tab {
|
||||
&__count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
.pill-version-count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.doc-tab {
|
||||
&__count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
.pill-version-count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,16 +30,7 @@
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: base(1.2);
|
||||
padding: base(0.2) base(0.6);
|
||||
}
|
||||
|
||||
&__count {
|
||||
line-height: base(0.8);
|
||||
min-width: base(0.8);
|
||||
text-align: center;
|
||||
background-color: var(--theme-elevation-100);
|
||||
border-radius: var(--style-radius-s);
|
||||
line-height: calc(var(--base) * 1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ export const DocumentTab: React.FC<
|
||||
baseClass={baseClass}
|
||||
href={href}
|
||||
isActive={isActive}
|
||||
isCollection={!!collectionConfig && !globalConfig}
|
||||
newTab={newTab}
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@layer payload-default {
|
||||
.pill-version-count {
|
||||
line-height: calc(var(--base) * 0.8);
|
||||
min-width: calc(var(--base) * 0.8);
|
||||
text-align: center;
|
||||
background-color: var(--theme-elevation-100);
|
||||
border-radius: var(--style-radius-s);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { baseClass } from '../../Tab/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'pill-version-count'
|
||||
|
||||
export const VersionsPill: React.FC = () => {
|
||||
const { versionCount } = useDocumentInfo()
|
||||
@@ -11,5 +13,5 @@ export const VersionsPill: React.FC = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
return <span className={`${baseClass}__count`}>{versionCount}</span>
|
||||
return <span className={baseClass}>{versionCount}</span>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
import type { NavPreferences } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { BrowseByFolderButton, Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { EntityType } from '@payloadcms/ui/shared'
|
||||
import { usePathname } from 'next/navigation.js'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
@@ -20,14 +20,35 @@ export const DefaultNavClient: React.FC<{
|
||||
|
||||
const {
|
||||
config: {
|
||||
admin: {
|
||||
routes: { browseByFolder: foldersRoute },
|
||||
},
|
||||
collections,
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
const [folderCollectionSlugs] = React.useState<string[]>(() => {
|
||||
return collections.reduce<string[]>((acc, collection) => {
|
||||
if (collection.admin.folders) {
|
||||
acc.push(collection.slug)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const folderURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: foldersRoute,
|
||||
})
|
||||
|
||||
const viewingRootFolderView = pathname.startsWith(folderURL)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{folderCollectionSlugs.length > 0 && <BrowseByFolderButton active={viewingRootFolderView} />}
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
return (
|
||||
<NavGroup isOpen={navPreferences?.groups?.[label]?.open} key={key} label={label}>
|
||||
|
||||
161
packages/next/src/views/BrowseByFolder/buildView.tsx
Normal file
161
packages/next/src/views/BrowseByFolder/buildView.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
BuildCollectionFolderViewResult,
|
||||
FolderListViewServerPropsOnly,
|
||||
ListQuery,
|
||||
} from 'payload'
|
||||
|
||||
import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import { getFolderData } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getPreferences } from '../../utilities/getPreferences.js'
|
||||
|
||||
export type BuildFolderViewArgs = {
|
||||
customCellProps?: Record<string, any>
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
enableRowSelections: boolean
|
||||
folderID?: number | string
|
||||
isInDrawer?: boolean
|
||||
overrideEntityVisibility?: boolean
|
||||
query: ListQuery
|
||||
} & AdminViewServerProps
|
||||
|
||||
export const buildBrowseByFolderView = async (
|
||||
args: BuildFolderViewArgs,
|
||||
): Promise<BuildCollectionFolderViewResult> => {
|
||||
const {
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
folderCollectionSlugs,
|
||||
folderID,
|
||||
initPageResult,
|
||||
isInDrawer,
|
||||
params,
|
||||
query: queryFromArgs,
|
||||
searchParams,
|
||||
} = args
|
||||
|
||||
const {
|
||||
locale: fullLocale,
|
||||
permissions,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
query: queryFromReq,
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
const collections = folderCollectionSlugs.filter(
|
||||
(collectionSlug) =>
|
||||
permissions?.collections?.[collectionSlug]?.read &&
|
||||
visibleEntities.collections.includes(collectionSlug),
|
||||
)
|
||||
|
||||
if (!collections.length) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const query = queryFromArgs || queryFromReq
|
||||
const selectedCollectionSlugs: string[] =
|
||||
Array.isArray(query?.relationTo) && query.relationTo.length
|
||||
? query.relationTo
|
||||
: [...folderCollectionSlugs, config.folders.slug]
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
||||
folderID,
|
||||
payload: initPageResult.req.payload,
|
||||
search: query?.search as string,
|
||||
user: initPageResult.req.user,
|
||||
})
|
||||
|
||||
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
||||
|
||||
if (
|
||||
!isInDrawer &&
|
||||
((resolvedFolderID && folderID && folderID !== resolvedFolderID) ||
|
||||
(folderID && !resolvedFolderID))
|
||||
) {
|
||||
return redirect(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: config.admin.routes.browseByFolder,
|
||||
serverURL: config.serverURL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>(
|
||||
'browse-by-folder',
|
||||
payload,
|
||||
user.id,
|
||||
user.collection,
|
||||
)
|
||||
|
||||
const serverProps: Omit<FolderListViewServerPropsOnly, 'collectionConfig' | 'listPreferences'> = {
|
||||
documents,
|
||||
i18n,
|
||||
locale: fullLocale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
subfolders,
|
||||
user,
|
||||
}
|
||||
|
||||
// const folderViewSlots = renderFolderViewSlots({
|
||||
// clientProps: {
|
||||
// },
|
||||
// description: staticDescription,
|
||||
// payload,
|
||||
// serverProps,
|
||||
// })
|
||||
|
||||
// documents cannot be created without a parent folder in this view
|
||||
const hasCreatePermissionCollectionSlugs = folderID
|
||||
? [config.folders.slug, ...folderCollectionSlugs]
|
||||
: [config.folders.slug]
|
||||
|
||||
return {
|
||||
View: (
|
||||
<FolderProvider
|
||||
breadcrumbs={breadcrumbs}
|
||||
documents={documents}
|
||||
filteredCollectionSlugs={selectedCollectionSlugs}
|
||||
folderCollectionSlugs={folderCollectionSlugs}
|
||||
folderID={folderID}
|
||||
subfolders={subfolders}
|
||||
>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
{RenderServerComponent({
|
||||
clientProps: {
|
||||
// ...folderViewSlots,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
hasCreatePermissionCollectionSlugs,
|
||||
selectedCollectionSlugs,
|
||||
viewPreference: browseByFolderPreferences?.value?.viewPreference,
|
||||
},
|
||||
// Component:config.folders?.components?.views?.list?.Component,
|
||||
Fallback: DefaultBrowseByFolderView,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
})}
|
||||
</FolderProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
20
packages/next/src/views/BrowseByFolder/index.tsx
Normal file
20
packages/next/src/views/BrowseByFolder/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type React from 'react'
|
||||
|
||||
import { notFound } from 'next/navigation.js'
|
||||
|
||||
import type { BuildFolderViewArgs } from './buildView.js'
|
||||
|
||||
import { buildBrowseByFolderView } from './buildView.js'
|
||||
|
||||
export const BrowseByFolder: React.FC<BuildFolderViewArgs> = async (args) => {
|
||||
try {
|
||||
const { View } = await buildBrowseByFolderView(args)
|
||||
return View
|
||||
} catch (error) {
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
} else {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
23
packages/next/src/views/BrowseByFolder/metadata.ts
Normal file
23
packages/next/src/views/BrowseByFolder/metadata.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateBrowseByFolderMetadata = async (
|
||||
args: Parameters<GenerateViewMetadata>[0],
|
||||
): Promise<Metadata> => {
|
||||
const { config, i18n } = args
|
||||
|
||||
const title: string = i18n.t('folder:browseByFolder')
|
||||
const description: string = ''
|
||||
const keywords: string = ''
|
||||
|
||||
return generateMetadata({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
})
|
||||
}
|
||||
207
packages/next/src/views/CollectionFolders/buildView.tsx
Normal file
207
packages/next/src/views/CollectionFolders/buildView.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
BuildCollectionFolderViewResult,
|
||||
FolderListViewServerPropsOnly,
|
||||
ListQuery,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
import { DefaultCollectionFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import { getFolderData, parseDocumentID } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getPreferences } from '../../utilities/getPreferences.js'
|
||||
|
||||
// import { renderFolderViewSlots } from './renderFolderViewSlots.js'
|
||||
|
||||
export type BuildCollectionFolderViewStateArgs = {
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
enableRowSelections: boolean
|
||||
folderID?: number | string
|
||||
isInDrawer?: boolean
|
||||
overrideEntityVisibility?: boolean
|
||||
query: ListQuery
|
||||
} & AdminViewServerProps
|
||||
|
||||
/**
|
||||
* Builds the entire view for collection-folder views on the server
|
||||
*/
|
||||
export const buildCollectionFolderView = async (
|
||||
args: BuildCollectionFolderViewStateArgs,
|
||||
): Promise<BuildCollectionFolderViewResult> => {
|
||||
const {
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
folderCollectionSlugs,
|
||||
folderID,
|
||||
initPageResult,
|
||||
isInDrawer,
|
||||
overrideEntityVisibility,
|
||||
params,
|
||||
query: queryFromArgs,
|
||||
searchParams,
|
||||
} = args
|
||||
|
||||
const {
|
||||
collectionConfig,
|
||||
collectionConfig: { slug: collectionSlug },
|
||||
locale: fullLocale,
|
||||
permissions,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
query: queryFromReq,
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
if (!permissions?.collections?.[collectionSlug]?.read) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
if (collectionConfig) {
|
||||
const query = queryFromArgs || queryFromReq
|
||||
|
||||
const collectionFolderPreferences = await getPreferences<{ viewPreference: string }>(
|
||||
`${collectionSlug}-collection-folder`,
|
||||
payload,
|
||||
user.id,
|
||||
user.collection,
|
||||
)
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
if (
|
||||
(!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) ||
|
||||
!folderCollectionSlugs.includes(collectionSlug)
|
||||
) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const whereConstraints = [
|
||||
mergeListSearchAndWhere({
|
||||
collectionConfig,
|
||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||
where: (query?.where as Where) || undefined,
|
||||
}),
|
||||
]
|
||||
|
||||
if (folderID) {
|
||||
whereConstraints.push({
|
||||
[config.folders.fieldName]: {
|
||||
equals: parseDocumentID({ id: folderID, collectionSlug, payload }),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
whereConstraints.push({
|
||||
[config.folders.fieldName]: {
|
||||
exists: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
||||
collectionSlug,
|
||||
folderID,
|
||||
payload: initPageResult.req.payload,
|
||||
search: query?.search as string,
|
||||
user: initPageResult.req.user,
|
||||
})
|
||||
|
||||
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
||||
|
||||
if (
|
||||
!isInDrawer &&
|
||||
((resolvedFolderID && folderID && folderID !== resolvedFolderID) ||
|
||||
(folderID && !resolvedFolderID))
|
||||
) {
|
||||
return redirect(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${config.folders.slug}`,
|
||||
serverURL: config.serverURL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const newDocumentURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
})
|
||||
|
||||
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create
|
||||
|
||||
const serverProps: FolderListViewServerPropsOnly = {
|
||||
collectionConfig,
|
||||
documents,
|
||||
i18n,
|
||||
locale: fullLocale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
subfolders,
|
||||
user,
|
||||
}
|
||||
|
||||
// We could support slots in the folder view in the future
|
||||
// const folderViewSlots = renderFolderViewSlots({
|
||||
// clientProps: {
|
||||
// collectionSlug,
|
||||
// hasCreatePermission,
|
||||
// newDocumentURL,
|
||||
// },
|
||||
// collectionConfig,
|
||||
// description: typeof collectionConfig.admin.description === 'function'
|
||||
// ? collectionConfig.admin.description({ t: i18n.t })
|
||||
// : collectionConfig.admin.description,
|
||||
// payload,
|
||||
// serverProps,
|
||||
// })
|
||||
|
||||
const search = query?.search as string
|
||||
|
||||
return {
|
||||
View: (
|
||||
<FolderProvider
|
||||
breadcrumbs={breadcrumbs}
|
||||
collectionSlug={collectionSlug}
|
||||
documents={documents}
|
||||
folderCollectionSlugs={folderCollectionSlugs}
|
||||
folderID={folderID}
|
||||
search={search}
|
||||
subfolders={subfolders}
|
||||
>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
{RenderServerComponent({
|
||||
clientProps: {
|
||||
// ...folderViewSlots,
|
||||
collectionSlug,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
hasCreatePermission,
|
||||
newDocumentURL,
|
||||
viewPreference: collectionFolderPreferences?.value?.viewPreference,
|
||||
},
|
||||
Component: collectionConfig?.admin?.components?.views?.list?.Component,
|
||||
Fallback: DefaultCollectionFolderView,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
})}
|
||||
</FolderProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('not-found')
|
||||
}
|
||||
20
packages/next/src/views/CollectionFolders/index.tsx
Normal file
20
packages/next/src/views/CollectionFolders/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type React from 'react'
|
||||
|
||||
import { notFound } from 'next/navigation.js'
|
||||
|
||||
import type { BuildCollectionFolderViewStateArgs } from './buildView.js'
|
||||
|
||||
import { buildCollectionFolderView } from './buildView.js'
|
||||
|
||||
export const CollectionFolderView: React.FC<BuildCollectionFolderViewStateArgs> = async (args) => {
|
||||
try {
|
||||
const { View } = await buildCollectionFolderView(args)
|
||||
return View
|
||||
} catch (error) {
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
} else {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
35
packages/next/src/views/CollectionFolders/metadata.ts
Normal file
35
packages/next/src/views/CollectionFolders/metadata.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateCollectionFolderMetadata = async (
|
||||
args: {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
} & Parameters<GenerateViewMetadata>[0],
|
||||
): Promise<Metadata> => {
|
||||
const { collectionConfig, config, i18n } = args
|
||||
|
||||
let title: string = ''
|
||||
const description: string = ''
|
||||
const keywords: string = ''
|
||||
|
||||
if (collectionConfig) {
|
||||
title = getTranslation(collectionConfig.labels.singular, i18n)
|
||||
}
|
||||
|
||||
title = `${title ? `${title} ` : title}${i18n.t('folder:folders')}`
|
||||
|
||||
return generateMetadata({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
...(collectionConfig?.admin?.meta || {}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
AfterFolderListClientProps,
|
||||
AfterFolderListTableClientProps,
|
||||
AfterFolderListTableServerPropsOnly,
|
||||
BeforeFolderListClientProps,
|
||||
BeforeFolderListServerPropsOnly,
|
||||
BeforeFolderListTableClientProps,
|
||||
BeforeFolderListTableServerPropsOnly,
|
||||
FolderListViewServerPropsOnly,
|
||||
FolderListViewSlots,
|
||||
ListViewSlotSharedClientProps,
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
StaticDescription,
|
||||
ViewDescriptionClientProps,
|
||||
ViewDescriptionServerPropsOnly,
|
||||
} from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
|
||||
type Args = {
|
||||
clientProps: ListViewSlotSharedClientProps
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
description?: StaticDescription
|
||||
payload: Payload
|
||||
serverProps: FolderListViewServerPropsOnly
|
||||
}
|
||||
|
||||
export const renderFolderViewSlots = ({
|
||||
clientProps,
|
||||
collectionConfig,
|
||||
description,
|
||||
payload,
|
||||
serverProps,
|
||||
}: Args): FolderListViewSlots => {
|
||||
const result: FolderListViewSlots = {} as FolderListViewSlots
|
||||
|
||||
if (collectionConfig.admin.components?.afterList) {
|
||||
result.AfterFolderList = RenderServerComponent({
|
||||
clientProps: clientProps satisfies AfterFolderListClientProps,
|
||||
Component: collectionConfig.admin.components.afterList,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies AfterFolderListTableServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
const listMenuItems = collectionConfig.admin.components?.listMenuItems
|
||||
if (Array.isArray(listMenuItems)) {
|
||||
result.listMenuItems = [
|
||||
RenderServerComponent({
|
||||
clientProps,
|
||||
Component: listMenuItems,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.afterListTable) {
|
||||
result.AfterFolderListTable = RenderServerComponent({
|
||||
clientProps: clientProps satisfies AfterFolderListTableClientProps,
|
||||
Component: collectionConfig.admin.components.afterListTable,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies AfterFolderListTableServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeList) {
|
||||
result.BeforeFolderList = RenderServerComponent({
|
||||
clientProps: clientProps satisfies BeforeFolderListClientProps,
|
||||
Component: collectionConfig.admin.components.beforeList,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies BeforeFolderListServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeListTable) {
|
||||
result.BeforeFolderListTable = RenderServerComponent({
|
||||
clientProps: clientProps satisfies BeforeFolderListTableClientProps,
|
||||
Component: collectionConfig.admin.components.beforeListTable,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies BeforeFolderListTableServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.Description) {
|
||||
result.Description = RenderServerComponent({
|
||||
clientProps: {
|
||||
collectionSlug: collectionConfig.slug,
|
||||
description,
|
||||
} satisfies ViewDescriptionClientProps,
|
||||
Component: collectionConfig.admin.components.Description,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies ViewDescriptionServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -32,9 +32,12 @@ import { renderDocumentSlots } from './renderDocumentSlots.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
||||
|
||||
// This function will be responsible for rendering an Edit Document view
|
||||
// it will be called on the server for Edit page views as well as
|
||||
// called on-demand from document drawers
|
||||
/**
|
||||
* This function is responsible for rendering
|
||||
* an Edit Document view on the server for both:
|
||||
* - default document edit views
|
||||
* - on-demand edit views within drawers
|
||||
*/
|
||||
export const renderDocument = async ({
|
||||
disableActions,
|
||||
documentSubViewType,
|
||||
|
||||
@@ -40,6 +40,12 @@ type RenderListViewArgs = {
|
||||
redirectAfterDuplicate?: boolean
|
||||
} & AdminViewServerProps
|
||||
|
||||
/**
|
||||
* This function is responsible for rendering
|
||||
* the list view on the server for both:
|
||||
* - default list view
|
||||
* - list view within drawers
|
||||
*/
|
||||
export const renderListView = async (
|
||||
args: RenderListViewArgs,
|
||||
): Promise<{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminViewConfig, SanitizedConfig } from 'payload'
|
||||
|
||||
import type { ViewFromConfig } from './getViewFromConfig.js'
|
||||
import type { ViewFromConfig } from './getRouteData.js'
|
||||
|
||||
import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
CollectionSlug,
|
||||
DocumentSubViewTypes,
|
||||
ImportMap,
|
||||
PayloadComponent,
|
||||
@@ -14,6 +15,8 @@ import { formatAdminURL } from 'payload/shared'
|
||||
import type { initPage } from '../../utilities/initPage/index.js'
|
||||
|
||||
import { Account } from '../Account/index.js'
|
||||
import { BrowseByFolder } from '../BrowseByFolder/index.js'
|
||||
import { CollectionFolderView } from '../CollectionFolders/index.js'
|
||||
import { CreateFirstUserView } from '../CreateFirstUser/index.js'
|
||||
import { Dashboard } from '../Dashboard/index.js'
|
||||
import { Document as DocumentView } from '../Document/index.js'
|
||||
@@ -31,6 +34,7 @@ import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
|
||||
const baseClasses = {
|
||||
account: 'account',
|
||||
folders: 'folders',
|
||||
forgot: forgotPasswordBaseClass,
|
||||
login: loginBaseClass,
|
||||
reset: resetPasswordBaseClass,
|
||||
@@ -48,6 +52,7 @@ export type ViewFromConfig = {
|
||||
|
||||
const oneSegmentViews: OneSegmentViews = {
|
||||
account: Account,
|
||||
browseByFolder: BrowseByFolder,
|
||||
createFirstUser: CreateFirstUserView,
|
||||
forgot: ForgotPasswordView,
|
||||
inactivity: LogoutInactivity,
|
||||
@@ -56,7 +61,7 @@ const oneSegmentViews: OneSegmentViews = {
|
||||
unauthorized: UnauthorizedView,
|
||||
}
|
||||
|
||||
type GetViewFromConfigArgs = {
|
||||
type GetRouteDataArgs = {
|
||||
adminRoute: string
|
||||
config: SanitizedConfig
|
||||
currentRoute: string
|
||||
@@ -67,9 +72,11 @@ type GetViewFromConfigArgs = {
|
||||
segments: string[]
|
||||
}
|
||||
|
||||
type GetViewFromConfigResult = {
|
||||
type GetRouteDataResult = {
|
||||
DefaultView: ViewFromConfig
|
||||
documentSubViewType?: DocumentSubViewTypes
|
||||
folderCollectionSlugs: CollectionSlug[]
|
||||
folderID?: string
|
||||
initPageOptions: Parameters<typeof initPage>[0]
|
||||
serverProps: ServerPropsFromView
|
||||
templateClassName: string
|
||||
@@ -77,19 +84,20 @@ type GetViewFromConfigResult = {
|
||||
viewType?: ViewTypes
|
||||
}
|
||||
|
||||
export const getViewFromConfig = ({
|
||||
export const getRouteData = ({
|
||||
adminRoute,
|
||||
config,
|
||||
currentRoute,
|
||||
importMap,
|
||||
searchParams,
|
||||
segments,
|
||||
}: GetViewFromConfigArgs): GetViewFromConfigResult => {
|
||||
}: GetRouteDataArgs): GetRouteDataResult => {
|
||||
let ViewToRender: ViewFromConfig = null
|
||||
let templateClassName: string
|
||||
let templateType: 'default' | 'minimal' | undefined
|
||||
let documentSubViewType: DocumentSubViewTypes
|
||||
let viewType: ViewTypes
|
||||
let folderID: string
|
||||
|
||||
const initPageOptions: Parameters<typeof initPage>[0] = {
|
||||
config,
|
||||
@@ -105,6 +113,13 @@ export const getViewFromConfig = ({
|
||||
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
||||
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
||||
|
||||
const folderCollectionSlugs = config.collections.reduce((acc, { slug, admin }) => {
|
||||
if (admin?.folders) {
|
||||
return [...acc, slug]
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const serverProps: ServerPropsFromView = {
|
||||
viewActions: config?.admin?.components?.actions || [],
|
||||
}
|
||||
@@ -153,6 +168,7 @@ export const getViewFromConfig = ({
|
||||
if (oneSegmentViews[viewKey]) {
|
||||
// --> /account
|
||||
// --> /create-first-user
|
||||
// --> /browse-by-folder
|
||||
// --> /forgot
|
||||
// --> /login
|
||||
// --> /logout
|
||||
@@ -170,6 +186,11 @@ export const getViewFromConfig = ({
|
||||
templateType = 'default'
|
||||
viewType = 'account'
|
||||
}
|
||||
|
||||
if (folderCollectionSlugs.length && viewKey === 'browseByFolder') {
|
||||
templateType = 'default'
|
||||
viewType = 'folders'
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -182,9 +203,19 @@ export const getViewFromConfig = ({
|
||||
templateClassName = baseClasses[segmentTwo]
|
||||
templateType = 'minimal'
|
||||
viewType = 'reset'
|
||||
}
|
||||
|
||||
if (isCollection && matchedCollection) {
|
||||
} else if (
|
||||
folderCollectionSlugs.length &&
|
||||
`/${segmentOne}` === config.admin.routes.browseByFolder
|
||||
) {
|
||||
// --> /browse-by-folder/:folderID
|
||||
ViewToRender = {
|
||||
Component: oneSegmentViews.browseByFolder,
|
||||
}
|
||||
templateClassName = baseClasses.folders
|
||||
templateType = 'default'
|
||||
viewType = 'folders'
|
||||
folderID = segmentTwo
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -229,31 +260,47 @@ export const getViewFromConfig = ({
|
||||
templateType = 'minimal'
|
||||
viewType = 'verify'
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionID
|
||||
if (
|
||||
segmentThree === config.folders.slug &&
|
||||
folderCollectionSlugs.includes(matchedCollection.slug)
|
||||
) {
|
||||
// Collection Folder Views
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug/:folderID
|
||||
ViewToRender = {
|
||||
Component: CollectionFolderView,
|
||||
}
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
templateClassName = `collection-folders`
|
||||
templateType = 'default'
|
||||
viewType = 'collection-folders'
|
||||
folderID = segmentFour
|
||||
} else {
|
||||
// Collection Edit Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionID
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
}
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
|
||||
const viewInfo = getDocumentViewInfo([segmentFour, segmentFive])
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
attachViewActions({
|
||||
collectionOrGlobal: matchedCollection,
|
||||
serverProps,
|
||||
viewKeyArg: documentSubViewType,
|
||||
})
|
||||
}
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
|
||||
const viewInfo = getDocumentViewInfo([segmentFour, segmentFive])
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
attachViewActions({
|
||||
collectionOrGlobal: matchedCollection,
|
||||
serverProps,
|
||||
viewKeyArg: documentSubViewType,
|
||||
})
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
// Custom Views
|
||||
// Global Edit Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/preview
|
||||
// --> /globals/:globalSlug/versions/:versionID
|
||||
@@ -288,6 +335,8 @@ export const getViewFromConfig = ({
|
||||
return {
|
||||
DefaultView: ViewToRender,
|
||||
documentSubViewType,
|
||||
folderCollectionSlugs,
|
||||
folderID,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
@@ -1,22 +1,23 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type {
|
||||
AdminViewClientProps,
|
||||
AdminViewServerPropsOnly,
|
||||
ImportMap,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import {
|
||||
type AdminViewClientProps,
|
||||
type AdminViewServerPropsOnly,
|
||||
type ImportMap,
|
||||
parseDocumentID,
|
||||
type SanitizedConfig,
|
||||
} from 'payload'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getViewFromConfig } from './getViewFromConfig.js'
|
||||
import { getRouteData } from './getRouteData.js'
|
||||
|
||||
export type GenerateViewMetadata = (args: {
|
||||
config: SanitizedConfig
|
||||
@@ -64,12 +65,14 @@ export const RootPage = async ({
|
||||
const {
|
||||
DefaultView,
|
||||
documentSubViewType,
|
||||
folderCollectionSlugs,
|
||||
folderID: folderIDParam,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewType,
|
||||
} = getViewFromConfig({
|
||||
} = getRouteData({
|
||||
adminRoute,
|
||||
config,
|
||||
currentRoute,
|
||||
@@ -89,6 +92,10 @@ export const RootPage = async ({
|
||||
})
|
||||
?.then((doc) => !!doc))
|
||||
|
||||
/**
|
||||
* This function is responsible for handling the case where the view is not found.
|
||||
* The current route did not match any default views or custom route views.
|
||||
*/
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
if (initPageResult?.req?.user) {
|
||||
notFound()
|
||||
@@ -132,8 +139,20 @@ export const RootPage = async ({
|
||||
importMap,
|
||||
})
|
||||
|
||||
const payload = initPageResult?.req.payload
|
||||
const folderID = parseDocumentID({
|
||||
id: folderIDParam,
|
||||
collectionSlug: payload.config.folders.slug,
|
||||
payload,
|
||||
})
|
||||
|
||||
const RenderedView = RenderServerComponent({
|
||||
clientProps: { clientConfig, documentSubViewType, viewType } satisfies AdminViewClientProps,
|
||||
clientProps: {
|
||||
clientConfig,
|
||||
documentSubViewType,
|
||||
folderCollectionSlugs,
|
||||
viewType,
|
||||
} satisfies AdminViewClientProps,
|
||||
Component: DefaultView.payloadComponent,
|
||||
Fallback: DefaultView.Component,
|
||||
importMap,
|
||||
@@ -141,6 +160,7 @@ export const RootPage = async ({
|
||||
...serverProps,
|
||||
clientConfig,
|
||||
docID: initPageResult?.docID,
|
||||
folderID,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
@@ -151,8 +171,8 @@ export const RootPage = async ({
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!templateType && <Fragment>{RenderedView}</Fragment>}
|
||||
<React.Fragment>
|
||||
{!templateType && <React.Fragment>{RenderedView}</React.Fragment>}
|
||||
{templateType === 'minimal' && (
|
||||
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
|
||||
)}
|
||||
@@ -182,6 +202,6 @@ export const RootPage = async ({
|
||||
{RenderedView}
|
||||
</DefaultTemplate>
|
||||
)}
|
||||
</Fragment>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
|
||||
import { generateAccountViewMetadata } from '../Account/metadata.js'
|
||||
import { generateBrowseByFolderMetadata } from '../BrowseByFolder/metadata.js'
|
||||
import { generateCollectionFolderMetadata } from '../CollectionFolders/metadata.js'
|
||||
import { generateCreateFirstUserViewMetadata } from '../CreateFirstUser/metadata.js'
|
||||
import { generateDashboardViewMetadata } from '../Dashboard/metadata.js'
|
||||
import { generateDocumentViewMetadata } from '../Document/metadata.js'
|
||||
@@ -18,6 +20,7 @@ import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
const oneSegmentMeta = {
|
||||
'create-first-user': generateCreateFirstUserViewMetadata,
|
||||
folders: generateBrowseByFolderMetadata,
|
||||
forgot: generateForgotPasswordViewMetadata,
|
||||
login: generateLoginViewMetadata,
|
||||
logout: generateUnauthorizedViewMetadata,
|
||||
@@ -40,12 +43,18 @@ export const generatePageMetadata = async ({
|
||||
params: paramsPromise,
|
||||
}: Args) => {
|
||||
const config = await configPromise
|
||||
|
||||
const params = await paramsPromise
|
||||
|
||||
const folderCollectionSlugs = config.collections.reduce((acc, { slug, admin }) => {
|
||||
if (admin?.folders) {
|
||||
return [...acc, slug]
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
const segments = Array.isArray(params.segments) ? params.segments : []
|
||||
|
||||
const currentRoute = `/${segments.join('/')}`
|
||||
const [segmentOne, segmentTwo] = segments
|
||||
const [segmentOne, segmentTwo, segmentThree] = segments
|
||||
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
const isCollection = segmentOne === 'collections'
|
||||
@@ -72,7 +81,14 @@ export const generatePageMetadata = async ({
|
||||
break
|
||||
}
|
||||
case 1: {
|
||||
if (oneSegmentMeta[segmentOne] && segmentOne !== 'account') {
|
||||
if (folderCollectionSlugs.length && `/${segmentOne}` === config.admin.routes.browseByFolder) {
|
||||
// --> /:folderCollectionSlug
|
||||
meta = await oneSegmentMeta.folders({ config, i18n })
|
||||
} else if (segmentOne === 'account') {
|
||||
// --> /account
|
||||
meta = await generateAccountViewMetadata({ config, i18n })
|
||||
break
|
||||
} else if (oneSegmentMeta[segmentOne]) {
|
||||
// --> /create-first-user
|
||||
// --> /forgot
|
||||
// --> /login
|
||||
@@ -81,10 +97,6 @@ export const generatePageMetadata = async ({
|
||||
// --> /unauthorized
|
||||
meta = await oneSegmentMeta[segmentOne]({ config, i18n })
|
||||
break
|
||||
} else if (segmentOne === 'account') {
|
||||
// --> /account
|
||||
meta = await generateAccountViewMetadata({ config, i18n })
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -92,8 +104,13 @@ export const generatePageMetadata = async ({
|
||||
if (`/${segmentOne}` === config.admin.routes.reset) {
|
||||
// --> /reset/:token
|
||||
meta = await generateResetPasswordViewMetadata({ config, i18n })
|
||||
}
|
||||
if (isCollection) {
|
||||
} else if (
|
||||
folderCollectionSlugs.length &&
|
||||
`/${segmentOne}` === config.admin.routes.browseByFolder
|
||||
) {
|
||||
// --> /browse-by-folder/:folderID
|
||||
meta = await generateBrowseByFolderMetadata({ config, i18n })
|
||||
} else if (isCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
meta = await generateListViewMetadata({ collectionConfig, config, i18n })
|
||||
} else if (isGlobal) {
|
||||
@@ -112,15 +129,29 @@ export const generatePageMetadata = async ({
|
||||
// --> /:collectionSlug/verify/:token
|
||||
meta = await generateVerifyViewMetadata({ config, i18n })
|
||||
} else if (isCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:version
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
meta = await generateDocumentViewMetadata({ collectionConfig, config, i18n, params })
|
||||
if (segmentThree === config.folders.slug) {
|
||||
if (folderCollectionSlugs.includes(collectionConfig.slug)) {
|
||||
// Collection Folder Views
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug/:id
|
||||
meta = await generateCollectionFolderMetadata({
|
||||
collectionConfig,
|
||||
config,
|
||||
i18n,
|
||||
params,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Collection Document Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:version
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
meta = await generateDocumentViewMetadata({ collectionConfig, config, i18n, params })
|
||||
}
|
||||
} else if (isGlobal) {
|
||||
// Custom Views
|
||||
// Global Document Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/versions/:version
|
||||
// --> /globals/:globalSlug/preview
|
||||
|
||||
@@ -18,9 +18,11 @@ export const renderPill = (data, latestVersion, currentLabel, previousLabel, pil
|
||||
return (
|
||||
<React.Fragment>
|
||||
{data?.id === latestVersion ? (
|
||||
<Pill pillStyle={pillStyle}>{currentLabel}</Pill>
|
||||
<Pill pillStyle={pillStyle} size="small">
|
||||
{currentLabel}
|
||||
</Pill>
|
||||
) : (
|
||||
<Pill>{previousLabel}</Pill>
|
||||
<Pill size="small">{previousLabel}</Pill>
|
||||
)}
|
||||
|
||||
</React.Fragment>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @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
|
||||
* @param {Object} [sortOnOptions] - Optional configuration options
|
||||
* @param {boolean} [sortOnOptions.devBundleServerPackages] - Whether to bundle server packages in development mode. @default true
|
||||
*
|
||||
* @returns {import('next').NextConfig}
|
||||
* */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -53,7 +53,7 @@ export type ListQuery = {
|
||||
search?: string
|
||||
sort?: Sort
|
||||
where?: Where
|
||||
}
|
||||
} & Record<string, unknown>
|
||||
|
||||
export type BuildTableStateArgs = {
|
||||
collectionSlug: string | string[]
|
||||
@@ -71,3 +71,7 @@ export type BuildTableStateArgs = {
|
||||
req: PayloadRequest
|
||||
tableAppearance?: 'condensed' | 'default'
|
||||
}
|
||||
|
||||
export type BuildCollectionFolderViewResult = {
|
||||
View: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -563,6 +563,7 @@ export type DocumentSlots = {
|
||||
}
|
||||
|
||||
export type {
|
||||
BuildCollectionFolderViewResult,
|
||||
BuildTableStateArgs,
|
||||
DefaultServerFunctionArgs,
|
||||
ListQuery,
|
||||
@@ -618,6 +619,26 @@ export type {
|
||||
EditViewProps,
|
||||
} from './views/document.js'
|
||||
|
||||
export type {
|
||||
AfterFolderListClientProps,
|
||||
AfterFolderListServerProps,
|
||||
AfterFolderListServerPropsOnly,
|
||||
AfterFolderListTableClientProps,
|
||||
AfterFolderListTableServerProps,
|
||||
AfterFolderListTableServerPropsOnly,
|
||||
BeforeFolderListClientProps,
|
||||
BeforeFolderListServerProps,
|
||||
BeforeFolderListServerPropsOnly,
|
||||
BeforeFolderListTableClientProps,
|
||||
BeforeFolderListTableServerProps,
|
||||
BeforeFolderListTableServerPropsOnly,
|
||||
FolderListViewClientProps,
|
||||
FolderListViewServerProps,
|
||||
FolderListViewServerPropsOnly,
|
||||
FolderListViewSlots,
|
||||
FolderListViewSlotSharedClientProps,
|
||||
} from './views/folderList.js'
|
||||
|
||||
export type {
|
||||
AdminViewClientProps,
|
||||
/**
|
||||
|
||||
60
packages/payload/src/admin/views/folderList.ts
Normal file
60
packages/payload/src/admin/views/folderList.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { ServerProps } from '../../config/types.js'
|
||||
import type { FolderOrDocument } from '../../folders/types.js'
|
||||
import type { SanitizedCollectionConfig } from '../../index.js'
|
||||
export type FolderListViewSlots = {
|
||||
AfterFolderList?: React.ReactNode
|
||||
AfterFolderListTable?: React.ReactNode
|
||||
BeforeFolderList?: React.ReactNode
|
||||
BeforeFolderListTable?: React.ReactNode
|
||||
Description?: React.ReactNode
|
||||
listMenuItems?: React.ReactNode[]
|
||||
Table: React.ReactNode
|
||||
}
|
||||
|
||||
export type FolderListViewServerPropsOnly = {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
documents: FolderOrDocument[]
|
||||
subfolders: FolderOrDocument[]
|
||||
} & ServerProps
|
||||
|
||||
export type FolderListViewServerProps = FolderListViewClientProps & FolderListViewServerPropsOnly
|
||||
|
||||
export type FolderListViewClientProps = {
|
||||
beforeActions?: React.ReactNode[]
|
||||
collectionSlug: SanitizedCollectionConfig['slug']
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
enableRowSelections?: boolean
|
||||
hasCreatePermission: boolean
|
||||
newDocumentURL: string
|
||||
viewPreference: 'grid' | 'list'
|
||||
} & FolderListViewSlots
|
||||
|
||||
export type FolderListViewSlotSharedClientProps = {
|
||||
collectionSlug: SanitizedCollectionConfig['slug']
|
||||
hasCreatePermission: boolean
|
||||
newDocumentURL: string
|
||||
}
|
||||
|
||||
// BeforeFolderList
|
||||
export type BeforeFolderListClientProps = FolderListViewSlotSharedClientProps
|
||||
export type BeforeFolderListServerPropsOnly = {} & FolderListViewServerPropsOnly
|
||||
export type BeforeFolderListServerProps = BeforeFolderListClientProps &
|
||||
BeforeFolderListServerPropsOnly
|
||||
|
||||
// BeforeFolderListTable
|
||||
export type BeforeFolderListTableClientProps = FolderListViewSlotSharedClientProps
|
||||
export type BeforeFolderListTableServerPropsOnly = {} & FolderListViewServerPropsOnly
|
||||
export type BeforeFolderListTableServerProps = BeforeFolderListTableClientProps &
|
||||
BeforeFolderListTableServerPropsOnly
|
||||
|
||||
// AfterFolderList
|
||||
export type AfterFolderListClientProps = FolderListViewSlotSharedClientProps
|
||||
export type AfterFolderListServerPropsOnly = {} & FolderListViewServerPropsOnly
|
||||
export type AfterFolderListServerProps = AfterFolderListClientProps & AfterFolderListServerPropsOnly
|
||||
|
||||
// AfterFolderListTable
|
||||
export type AfterFolderListTableClientProps = FolderListViewSlotSharedClientProps
|
||||
export type AfterFolderListTableServerPropsOnly = {} & FolderListViewServerPropsOnly
|
||||
export type AfterFolderListTableServerProps = AfterFolderListTableClientProps &
|
||||
AfterFolderListTableServerPropsOnly
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClientTranslationsObject, I18n } from '@payloadcms/translations'
|
||||
import type { ClientTranslationsObject } from '@payloadcms/translations'
|
||||
|
||||
import type { SanitizedPermissions } from '../../auth/index.js'
|
||||
import type { ImportMap } from '../../bin/generateImportMap/index.js'
|
||||
@@ -8,13 +8,12 @@ import type {
|
||||
CustomComponent,
|
||||
Locale,
|
||||
MetaConfig,
|
||||
Params,
|
||||
PayloadComponent,
|
||||
SanitizedConfig,
|
||||
ServerProps,
|
||||
} from '../../config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
||||
import type { Payload, PayloadRequest } from '../../types/index.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { LanguageOptions } from '../LanguageOptions.js'
|
||||
import type { Data, StaticDescription } from '../types.js'
|
||||
import type { DocumentSubViewTypes } from './document.js'
|
||||
@@ -32,6 +31,7 @@ export type AdminViewConfig = {
|
||||
export type AdminViewClientProps = {
|
||||
clientConfig: ClientConfig
|
||||
documentSubViewType?: DocumentSubViewTypes
|
||||
folderCollectionSlugs?: SanitizedCollectionConfig['slug'][]
|
||||
viewType: ViewTypes
|
||||
}
|
||||
|
||||
@@ -42,9 +42,14 @@ export type AdminViewServerPropsOnly = {
|
||||
* @todo remove `docID` here as it is already contained in `initPageResult`
|
||||
*/
|
||||
readonly docID?: number | string
|
||||
readonly folderID?: number | string
|
||||
readonly importMap: ImportMap
|
||||
readonly initialData?: Data
|
||||
readonly initPageResult: InitPageResult
|
||||
readonly params?: { [key: string]: string | string[] | undefined }
|
||||
readonly redirectAfterCreate?: boolean
|
||||
readonly redirectAfterDelete?: boolean
|
||||
readonly redirectAfterDuplicate?: boolean
|
||||
} & ServerProps
|
||||
|
||||
export type AdminViewServerProps = AdminViewClientProps & AdminViewServerPropsOnly
|
||||
@@ -78,8 +83,10 @@ export type InitPageResult = {
|
||||
*/
|
||||
export type ViewTypes =
|
||||
| 'account'
|
||||
| 'collection-folders'
|
||||
| 'dashboard'
|
||||
| 'document'
|
||||
| 'folders'
|
||||
| 'list'
|
||||
| 'reset'
|
||||
| 'verify'
|
||||
|
||||
@@ -38,7 +38,7 @@ export type ImportMap = {
|
||||
[path: UserImportPath]: any
|
||||
}
|
||||
|
||||
export type AddToImportMap = (payloadComponent: PayloadComponent | PayloadComponent[]) => void
|
||||
export type AddToImportMap = (payloadComponent?: PayloadComponent | PayloadComponent[]) => void
|
||||
|
||||
export async function generateImportMap(
|
||||
config: SanitizedConfig,
|
||||
|
||||
@@ -69,6 +69,7 @@ export const addDefaultsToCollectionConfig = (collection: CollectionConfig): Col
|
||||
custom: {},
|
||||
enableRichTextLink: true,
|
||||
enableRichTextRelationship: true,
|
||||
folders: false,
|
||||
useAsTitle: 'id',
|
||||
...(collection.admin || {}),
|
||||
pagination: {
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
RelationshipField,
|
||||
UploadField,
|
||||
} from '../../fields/config/types.js'
|
||||
import type { CollectionFoldersConfiguration } from '../../folders/types.js'
|
||||
import type {
|
||||
CollectionSlug,
|
||||
JsonObject,
|
||||
@@ -256,6 +257,11 @@ export type AfterForgotPasswordHook = (args: {
|
||||
context: RequestContext
|
||||
}) => any
|
||||
|
||||
export type EnableFoldersOptions = {
|
||||
// Displays the folder collection and parentFolder field in the document view
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
export type BaseListFilter = (args: {
|
||||
limit: number
|
||||
locale?: TypedLocale
|
||||
@@ -339,6 +345,11 @@ export type CollectionAdminOptions = {
|
||||
disableCopyToLocale?: boolean
|
||||
enableRichTextLink?: boolean
|
||||
enableRichTextRelationship?: boolean
|
||||
/**
|
||||
* Enables folders for this collection
|
||||
* @deprecated this property will move out of `admin` in the next patch
|
||||
*/
|
||||
folders?: CollectionFoldersConfiguration
|
||||
/**
|
||||
* Specify a navigational group for collections in the admin sidebar.
|
||||
* - Provide a string to place the entity in a custom group.
|
||||
@@ -583,8 +594,9 @@ export type SanitizedJoins = {
|
||||
export interface SanitizedCollectionConfig
|
||||
extends Omit<
|
||||
DeepRequired<CollectionConfig>,
|
||||
'auth' | 'endpoints' | 'fields' | 'slug' | 'upload' | 'versions'
|
||||
'admin' | 'auth' | 'endpoints' | 'fields' | 'slug' | 'upload' | 'versions'
|
||||
> {
|
||||
admin: CollectionAdminOptions
|
||||
auth: Auth
|
||||
endpoints: Endpoint[] | false
|
||||
fields: Field[]
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { JobsConfig } from '../queues/config/types/index.js'
|
||||
import type { Config } from './types.js'
|
||||
|
||||
import defaultAccess from '../auth/defaultAccess.js'
|
||||
import { foldersSlug, parentFolderFieldName } from '../folders/constants.js'
|
||||
|
||||
/**
|
||||
* @deprecated - remove in 4.0. This is error-prone, as mutating this object will affect any objects that use the defaults as a base.
|
||||
@@ -23,6 +24,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
||||
},
|
||||
routes: {
|
||||
account: '/account',
|
||||
browseByFolder: '/browse-by-folder',
|
||||
createFirstUser: '/create-first-user',
|
||||
forgot: '/forgot',
|
||||
inactivity: '/logout-inactivity',
|
||||
@@ -98,6 +100,7 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
||||
},
|
||||
routes: {
|
||||
account: '/account',
|
||||
browseByFolder: '/browse-by-folder',
|
||||
createFirstUser: '/create-first-user',
|
||||
forgot: '/forgot',
|
||||
inactivity: '/logout-inactivity',
|
||||
@@ -109,6 +112,13 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
||||
},
|
||||
}
|
||||
|
||||
config.folders = {
|
||||
slug: foldersSlug,
|
||||
debug: false,
|
||||
fieldName: parentFolderFieldName,
|
||||
...(config.folders || {}),
|
||||
}
|
||||
|
||||
config.bin = config.bin ?? []
|
||||
config.collections = config.collections ?? []
|
||||
config.cookiePrefix = config.cookiePrefix ?? 'payload'
|
||||
|
||||
@@ -18,6 +18,7 @@ import { sanitizeCollection } from '../collections/config/sanitize.js'
|
||||
import { migrationsCollection } from '../database/migrations/migrationsCollection.js'
|
||||
import { DuplicateCollection, InvalidConfiguration } from '../errors/index.js'
|
||||
import { defaultTimezones } from '../fields/baseFields/timezone/defaultTimezones.js'
|
||||
import { addFolderCollections } from '../folders/addFolderCollections.js'
|
||||
import { sanitizeGlobal } from '../globals/config/sanitize.js'
|
||||
import {
|
||||
baseBlockFields,
|
||||
@@ -190,6 +191,8 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
|
||||
const collectionSlugs = new Set<CollectionSlug>()
|
||||
|
||||
await addFolderCollections(config as unknown as Config)
|
||||
|
||||
const validRelationships = [
|
||||
...(config.collections.map((c) => c.slug) ?? []),
|
||||
jobsCollectionSlug,
|
||||
@@ -313,6 +316,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
for (const hook of Object.keys(hooks)) {
|
||||
const defaultAmount = hook === 'afterRead' || hook === 'beforeChange' ? 1 : 0
|
||||
if (hooks[hook]?.length > defaultAmount) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`The jobsCollectionOverrides function is returning a collection with an additional ${hook} hook defined. These hooks will not run unless the jobs.runHooks option is set to true. Setting this option to true will negatively impact performance.`,
|
||||
)
|
||||
@@ -374,7 +378,15 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
config.csrf.push(config.serverURL)
|
||||
}
|
||||
|
||||
// Get deduped list of upload adapters
|
||||
const uploadAdapters = new Set<string>()
|
||||
// interact with all collections
|
||||
for (const collection of config.collections) {
|
||||
// deduped upload adapters
|
||||
if (collection.upload?.adapter) {
|
||||
uploadAdapters.add(collection.upload.adapter)
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.upload) {
|
||||
config.upload = { adapters: [] }
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import type {
|
||||
import type { DatabaseAdapterResult } from '../database/types.js'
|
||||
import type { EmailAdapter, SendEmailOptions } from '../email/types.js'
|
||||
import type { ErrorName } from '../errors/types.js'
|
||||
import type { RootFoldersConfiguration } from '../folders/types.js'
|
||||
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type {
|
||||
Block,
|
||||
@@ -846,6 +847,11 @@ export type Config = {
|
||||
* @default '/account'
|
||||
*/
|
||||
account?: `/${string}`
|
||||
/** The route for the browse by folder view.
|
||||
*
|
||||
* @default '/browse-by-folder'
|
||||
*/
|
||||
browseByFolder: `/${string}`
|
||||
/** The route for the create first user page.
|
||||
*
|
||||
* @default '/create-first-user'
|
||||
@@ -977,6 +983,11 @@ export type Config = {
|
||||
email?: EmailAdapter | Promise<EmailAdapter>
|
||||
/** Custom REST endpoints */
|
||||
endpoints?: Endpoint[]
|
||||
/**
|
||||
* Options for folder view within the admin panel
|
||||
* @experimental this feature may change in minor versions until it is fully stable
|
||||
*/
|
||||
folders?: RootFoldersConfiguration
|
||||
/**
|
||||
* @see https://payloadcms.com/docs/configuration/globals#global-configs
|
||||
*/
|
||||
|
||||
@@ -41,11 +41,24 @@ export { getFieldPaths } from '../fields/getFieldPaths.js'
|
||||
|
||||
export * from '../fields/validations.js'
|
||||
|
||||
export type {
|
||||
FolderBreadcrumb,
|
||||
FolderDocumentItemKey,
|
||||
FolderEnabledColection,
|
||||
FolderInterface,
|
||||
FolderOrDocument,
|
||||
GetFolderDataResult,
|
||||
Subfolder,
|
||||
} from '../folders/types.js'
|
||||
export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js'
|
||||
|
||||
export { validOperators, validOperatorSet } from '../types/constants.js'
|
||||
|
||||
export { formatFilesize } from '../uploads/formatFilesize.js'
|
||||
|
||||
export { isImage } from '../uploads/isImage.js'
|
||||
export { combineWhereConstraints } from '../utilities/combineWhereConstraints.js'
|
||||
|
||||
export {
|
||||
deepCopyObject,
|
||||
deepCopyObjectComplex,
|
||||
@@ -60,6 +73,7 @@ export {
|
||||
deepMergeWithSourceArrays,
|
||||
} from '../utilities/deepMerge.js'
|
||||
|
||||
export { extractID } from '../utilities/extractID.js'
|
||||
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
|
||||
export { flattenAllFields } from '../utilities/flattenAllFields.js'
|
||||
|
||||
@@ -112,4 +126,5 @@ export { validateWhereQuery } from '../utilities/validateWhereQuery.js'
|
||||
export { wait } from '../utilities/wait.js'
|
||||
export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex.js'
|
||||
export { versionDefaults } from '../versions/defaults.js'
|
||||
|
||||
export { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
58
packages/payload/src/folders/addFolderCollections.ts
Normal file
58
packages/payload/src/folders/addFolderCollections.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Config } from '../config/types.js'
|
||||
import type { CollectionSlug } from '../index.js'
|
||||
|
||||
import { createFolderCollection } from './createFolderCollection.js'
|
||||
|
||||
export async function addFolderCollections(config: NonNullable<Config>): Promise<void> {
|
||||
if (!config.collections) {
|
||||
return
|
||||
}
|
||||
|
||||
const enabledCollectionSlugs: CollectionSlug[] = []
|
||||
const debug = Boolean(config?.folders?.debug)
|
||||
const folderFieldName = config?.folders?.fieldName as unknown as string
|
||||
const folderSlug = config?.folders?.slug as unknown as CollectionSlug
|
||||
|
||||
for (let i = 0; i < config.collections.length; i++) {
|
||||
const collection = config.collections[i]
|
||||
if (collection?.admin?.folders) {
|
||||
if (collection) {
|
||||
collection.fields.push({
|
||||
name: folderFieldName,
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
allowEdit: false,
|
||||
components: {
|
||||
Cell: '@payloadcms/ui/rsc#FolderTableCell',
|
||||
Field: '@payloadcms/ui/rsc#FolderEditField',
|
||||
},
|
||||
},
|
||||
index: true,
|
||||
label: 'Folder',
|
||||
relationTo: folderSlug,
|
||||
})
|
||||
enabledCollectionSlugs.push(collection.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledCollectionSlugs.length) {
|
||||
let folderCollection = createFolderCollection({
|
||||
slug: folderSlug,
|
||||
collectionSlugs: enabledCollectionSlugs,
|
||||
debug,
|
||||
folderFieldName,
|
||||
})
|
||||
|
||||
if (
|
||||
Array.isArray(config?.folders?.collectionOverrides) &&
|
||||
config?.folders.collectionOverrides.length
|
||||
) {
|
||||
for (const override of config.folders.collectionOverrides) {
|
||||
folderCollection = await override({ collection: folderCollection })
|
||||
}
|
||||
}
|
||||
config.collections.push(folderCollection)
|
||||
}
|
||||
}
|
||||
2
packages/payload/src/folders/constants.ts
Normal file
2
packages/payload/src/folders/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const foldersSlug = 'payload-folders'
|
||||
export const parentFolderFieldName = 'folder'
|
||||
74
packages/payload/src/folders/createFolderCollection.ts
Normal file
74
packages/payload/src/folders/createFolderCollection.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { CollectionConfig } from '../collections/config/types.js'
|
||||
|
||||
import { populateFolderDataEndpoint } from './endpoints/populateFolderData.js'
|
||||
import { deleteSubfoldersAfterDelete } from './hooks/deleteSubfoldersAfterDelete.js'
|
||||
import { dissasociateAfterDelete } from './hooks/dissasociateAfterDelete.js'
|
||||
import { reparentChildFolder } from './hooks/reparentChildFolder.js'
|
||||
|
||||
type CreateFolderCollectionArgs = {
|
||||
collectionSlugs: string[]
|
||||
debug?: boolean
|
||||
folderFieldName: string
|
||||
slug: string
|
||||
}
|
||||
export const createFolderCollection = ({
|
||||
slug,
|
||||
collectionSlugs,
|
||||
debug,
|
||||
folderFieldName,
|
||||
}: CreateFolderCollectionArgs): CollectionConfig => ({
|
||||
slug,
|
||||
admin: {
|
||||
hidden: !debug,
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
endpoints: [populateFolderDataEndpoint],
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: folderFieldName,
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
hidden: !debug,
|
||||
},
|
||||
index: true,
|
||||
relationTo: slug,
|
||||
},
|
||||
{
|
||||
name: 'documentsAndFolders',
|
||||
type: 'join',
|
||||
admin: {
|
||||
hidden: !debug,
|
||||
},
|
||||
collection: [slug, ...collectionSlugs],
|
||||
hasMany: true,
|
||||
on: folderFieldName,
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [
|
||||
reparentChildFolder({
|
||||
folderFieldName,
|
||||
}),
|
||||
],
|
||||
afterDelete: [
|
||||
dissasociateAfterDelete({
|
||||
collectionSlugs,
|
||||
folderFieldName,
|
||||
}),
|
||||
deleteSubfoldersAfterDelete({ folderFieldName, folderSlug: slug }),
|
||||
],
|
||||
},
|
||||
labels: {
|
||||
plural: 'Folders',
|
||||
singular: 'Folder',
|
||||
},
|
||||
typescript: {
|
||||
interface: 'FolderInterface',
|
||||
},
|
||||
})
|
||||
45
packages/payload/src/folders/endpoints/populateFolderData.ts
Normal file
45
packages/payload/src/folders/endpoints/populateFolderData.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import type { Endpoint } from '../../index.js'
|
||||
|
||||
import { getFolderData } from '../utils/getFolderData.js'
|
||||
|
||||
export const populateFolderDataEndpoint: Endpoint = {
|
||||
handler: async (req) => {
|
||||
if (!req?.user) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Unauthorized request.',
|
||||
},
|
||||
{
|
||||
status: httpStatus.UNAUTHORIZED,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const folderCollection = Boolean(req.payload.collections?.[req.payload.config.folders.slug])
|
||||
|
||||
if (!folderCollection) {
|
||||
return Response.json(
|
||||
{
|
||||
message: 'Folders are not configured',
|
||||
},
|
||||
{
|
||||
status: httpStatus.NOT_FOUND,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const data = await getFolderData({
|
||||
collectionSlug: req.searchParams?.get('collectionSlug') || undefined,
|
||||
folderID: req.searchParams?.get('folderID') || undefined,
|
||||
payload: req.payload,
|
||||
search: req.searchParams?.get('search') || undefined,
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
return Response.json(data)
|
||||
},
|
||||
method: 'get',
|
||||
path: '/populate-folder-data',
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { CollectionAfterDeleteHook } from '../../index.js'
|
||||
|
||||
type Args = {
|
||||
folderFieldName: string
|
||||
folderSlug: string
|
||||
}
|
||||
export const deleteSubfoldersAfterDelete = ({
|
||||
folderFieldName,
|
||||
folderSlug,
|
||||
}: Args): CollectionAfterDeleteHook => {
|
||||
return async ({ id, req }) => {
|
||||
await req.payload.delete({
|
||||
collection: folderSlug,
|
||||
req,
|
||||
where: {
|
||||
[folderFieldName]: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { CollectionAfterDeleteHook } from '../../index.js'
|
||||
|
||||
type Args = {
|
||||
collectionSlugs: string[]
|
||||
folderFieldName: string
|
||||
}
|
||||
export const dissasociateAfterDelete = ({
|
||||
collectionSlugs,
|
||||
folderFieldName,
|
||||
}: Args): CollectionAfterDeleteHook => {
|
||||
return async ({ id, req }) => {
|
||||
for (const collectionSlug of collectionSlugs) {
|
||||
await req.payload.update({
|
||||
collection: collectionSlug,
|
||||
data: {
|
||||
[folderFieldName]: null,
|
||||
},
|
||||
req,
|
||||
where: {
|
||||
[folderFieldName]: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
101
packages/payload/src/folders/hooks/reparentChildFolder.ts
Normal file
101
packages/payload/src/folders/hooks/reparentChildFolder.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { CollectionAfterChangeHook, Payload } from '../../index.js'
|
||||
|
||||
import { extractID } from '../../utilities/extractID.js'
|
||||
|
||||
type Args = {
|
||||
folderFieldName: string
|
||||
folderID: number | string
|
||||
parentIDToFind: number | string
|
||||
payload: Payload
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a child folder belongs to a parent folder by
|
||||
* recursively checking upwards through the folder hierarchy.
|
||||
*/
|
||||
async function isChildOfFolder({
|
||||
folderFieldName,
|
||||
folderID,
|
||||
parentIDToFind,
|
||||
payload,
|
||||
}: Args): Promise<boolean> {
|
||||
const parentFolder = await payload.findByID({
|
||||
id: folderID,
|
||||
collection: payload.config.folders.slug,
|
||||
})
|
||||
|
||||
const parentFolderID = parentFolder[folderFieldName]
|
||||
? extractID(parentFolder[folderFieldName])
|
||||
: undefined
|
||||
|
||||
if (!parentFolderID) {
|
||||
// made it to the root
|
||||
return false
|
||||
}
|
||||
|
||||
if (parentFolderID === parentIDToFind) {
|
||||
// found match, would be cyclic
|
||||
return true
|
||||
}
|
||||
|
||||
return isChildOfFolder({
|
||||
folderFieldName,
|
||||
folderID: parentFolderID,
|
||||
parentIDToFind,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* If a parent is moved into a child folder, we need to re-parent the child
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
→ F1
|
||||
→ F2
|
||||
→ F2A
|
||||
→ F3
|
||||
|
||||
Moving F1 → F2A becomes:
|
||||
|
||||
→ F2A
|
||||
→ F1
|
||||
→ F2
|
||||
→ F3
|
||||
```
|
||||
*/
|
||||
export const reparentChildFolder = ({
|
||||
folderFieldName,
|
||||
}: {
|
||||
folderFieldName: string
|
||||
}): CollectionAfterChangeHook => {
|
||||
return async ({ doc, previousDoc, req }) => {
|
||||
if (previousDoc[folderFieldName] !== doc[folderFieldName] && doc[folderFieldName]) {
|
||||
const newParentFolderID = extractID(doc[folderFieldName])
|
||||
const isMovingToChild = newParentFolderID
|
||||
? await isChildOfFolder({
|
||||
folderFieldName,
|
||||
folderID: newParentFolderID,
|
||||
parentIDToFind: doc.id,
|
||||
payload: req.payload,
|
||||
})
|
||||
: false
|
||||
|
||||
if (isMovingToChild) {
|
||||
// if the folder was moved into a child folder, the child folder needs
|
||||
// to be re-parented with the parent of the folder that was moved
|
||||
await req.payload.update({
|
||||
id: newParentFolderID,
|
||||
collection: req.payload.config.folders.slug,
|
||||
data: {
|
||||
[folderFieldName]: previousDoc[folderFieldName]
|
||||
? extractID(previousDoc[folderFieldName])
|
||||
: null,
|
||||
},
|
||||
req,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
packages/payload/src/folders/types.ts
Normal file
102
packages/payload/src/folders/types.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { CollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { CollectionSlug, SanitizedCollectionConfig } from '../index.js'
|
||||
import type { Document } from '../types/index.js'
|
||||
|
||||
export type FolderInterface = {
|
||||
documentsAndFolders?: {
|
||||
docs: {
|
||||
relationTo: CollectionSlug
|
||||
value: Document
|
||||
}[]
|
||||
}
|
||||
folder?: FolderInterface | (number | string | undefined)
|
||||
name: string
|
||||
} & TypeWithID
|
||||
|
||||
export type FolderBreadcrumb = {
|
||||
id: null | number | string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type Subfolder = {
|
||||
fileCount: number
|
||||
hasSubfolders: boolean
|
||||
id: number | string
|
||||
name: string
|
||||
subfolderCount: number
|
||||
}
|
||||
|
||||
export type FolderEnabledColection = {
|
||||
admin: {
|
||||
custom: {
|
||||
folderCollectionSlug: CollectionSlug
|
||||
}
|
||||
}
|
||||
slug: CollectionSlug
|
||||
} & SanitizedCollectionConfig
|
||||
|
||||
/**
|
||||
* `${relationTo}-${id}` is used as a key for the item
|
||||
*/
|
||||
export type FolderDocumentItemKey = `${string}-${number | string}`
|
||||
|
||||
/**
|
||||
* Needed for document card view for upload enabled collections
|
||||
*/
|
||||
type DocumentMediaData = {
|
||||
filename?: string
|
||||
mimeType?: string
|
||||
url?: string
|
||||
}
|
||||
/**
|
||||
* A generic structure for a folder or document item.
|
||||
*/
|
||||
export type FolderOrDocument = {
|
||||
itemKey: FolderDocumentItemKey
|
||||
relationTo: CollectionSlug
|
||||
value: {
|
||||
_folderOrDocumentTitle: string
|
||||
createdAt?: string
|
||||
folderID?: number | string
|
||||
id: number | string
|
||||
updatedAt?: string
|
||||
} & DocumentMediaData
|
||||
}
|
||||
|
||||
export type GetFolderDataResult = {
|
||||
breadcrumbs: FolderBreadcrumb[] | null
|
||||
documents: FolderOrDocument[]
|
||||
subfolders: FolderOrDocument[]
|
||||
}
|
||||
|
||||
export type RootFoldersConfiguration = {
|
||||
/**
|
||||
* An array of functions to be ran when the folder collection is initialized
|
||||
* This allows plugins to modify the collection configuration
|
||||
*/
|
||||
collectionOverrides?: (({
|
||||
collection,
|
||||
}: {
|
||||
collection: CollectionConfig
|
||||
}) => CollectionConfig | Promise<CollectionConfig>)[]
|
||||
/**
|
||||
* Ability to view hidden fields and collections related to folders
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean
|
||||
/**
|
||||
* The Folder field name
|
||||
*
|
||||
* @default "folder"
|
||||
*/
|
||||
fieldName?: string
|
||||
/**
|
||||
* Slug for the folder collection
|
||||
*
|
||||
* @default "payload-folders"
|
||||
*/
|
||||
slug?: string
|
||||
}
|
||||
|
||||
export type CollectionFoldersConfiguration = boolean
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { CollectionSlug, Document } from '../../index.js'
|
||||
import type { FolderOrDocument } from '../types.js'
|
||||
|
||||
type Args = {
|
||||
folderFieldName: string
|
||||
isUpload: boolean
|
||||
relationTo: CollectionSlug
|
||||
useAsTitle?: string
|
||||
value: Document
|
||||
}
|
||||
export function formatFolderOrDocumentItem({
|
||||
folderFieldName,
|
||||
isUpload,
|
||||
relationTo,
|
||||
useAsTitle,
|
||||
value,
|
||||
}: Args): FolderOrDocument {
|
||||
const itemValue: FolderOrDocument['value'] = {
|
||||
id: value?.id,
|
||||
_folderOrDocumentTitle: (useAsTitle && value?.[useAsTitle]) || value['id'],
|
||||
createdAt: value?.createdAt,
|
||||
folderID: value?.[folderFieldName],
|
||||
updatedAt: value?.updatedAt,
|
||||
}
|
||||
|
||||
if (isUpload) {
|
||||
itemValue.filename = value.filename
|
||||
itemValue.mimeType = value.mimeType
|
||||
itemValue.url = value.url
|
||||
}
|
||||
|
||||
return {
|
||||
itemKey: `${relationTo}-${value.id}`,
|
||||
relationTo,
|
||||
value: itemValue,
|
||||
}
|
||||
}
|
||||
63
packages/payload/src/folders/utils/getFolderBreadcrumbs.ts
Normal file
63
packages/payload/src/folders/utils/getFolderBreadcrumbs.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { User } from '../../index.js'
|
||||
import type { Document, Payload } from '../../types/index.js'
|
||||
import type { FolderBreadcrumb } from '../types.js'
|
||||
|
||||
type GetFolderBreadcrumbsArgs = {
|
||||
breadcrumbs?: FolderBreadcrumb[]
|
||||
folderID?: number | string
|
||||
payload: Payload
|
||||
user?: User
|
||||
}
|
||||
/**
|
||||
* Builds breadcrumbs up from child folder
|
||||
* all the way up to root folder
|
||||
*/
|
||||
export const getFolderBreadcrumbs = async ({
|
||||
breadcrumbs = [],
|
||||
folderID,
|
||||
payload,
|
||||
user,
|
||||
}: GetFolderBreadcrumbsArgs): Promise<FolderBreadcrumb[] | null> => {
|
||||
const folderFieldName: string = payload.config.folders.fieldName
|
||||
if (folderID) {
|
||||
const folderQuery = await payload.find({
|
||||
collection: payload.config.folders.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
overrideAccess: false,
|
||||
select: {
|
||||
name: true,
|
||||
[folderFieldName]: true,
|
||||
},
|
||||
user,
|
||||
where: {
|
||||
id: {
|
||||
equals: folderID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const folder = folderQuery.docs[0] as Document
|
||||
|
||||
if (folder) {
|
||||
breadcrumbs.push({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
})
|
||||
if (folder[folderFieldName]) {
|
||||
return getFolderBreadcrumbs({
|
||||
breadcrumbs,
|
||||
folderID:
|
||||
typeof folder[folderFieldName] === 'number' ||
|
||||
typeof folder[folderFieldName] === 'string'
|
||||
? folder[folderFieldName]
|
||||
: folder[folderFieldName].id,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return breadcrumbs.reverse()
|
||||
}
|
||||
104
packages/payload/src/folders/utils/getFolderData.ts
Normal file
104
packages/payload/src/folders/utils/getFolderData.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { CollectionSlug, User } from '../../index.js'
|
||||
import type { Payload } from '../../types/index.js'
|
||||
import type { GetFolderDataResult } from '../types.js'
|
||||
|
||||
import { parseDocumentID } from '../../index.js'
|
||||
import { getFolderBreadcrumbs } from './getFolderBreadcrumbs.js'
|
||||
import { queryDocumentsAndFoldersFromJoin } from './getFoldersAndDocumentsFromJoin.js'
|
||||
import { getOrphanedDocs } from './getOrphanedDocs.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Specify to query documents from a specific collection
|
||||
* @default undefined
|
||||
* @example 'posts'
|
||||
*/
|
||||
collectionSlug?: CollectionSlug
|
||||
/**
|
||||
* The ID of the folder to query documents from
|
||||
* @default undefined
|
||||
*/
|
||||
folderID?: number | string
|
||||
/**
|
||||
* The locale to use for the document query
|
||||
* @default undefined
|
||||
*/
|
||||
payload: Payload
|
||||
/**
|
||||
* Search term to filter documents by - only applicable IF `collectionSlug` exists and NO `folderID` is provided
|
||||
*/
|
||||
search?: string
|
||||
/**
|
||||
* The user making the request
|
||||
* @default undefined
|
||||
*/
|
||||
user?: User
|
||||
}
|
||||
/**
|
||||
* Query for documents, subfolders and breadcrumbs for a given folder
|
||||
*/
|
||||
export const getFolderData = async ({
|
||||
collectionSlug,
|
||||
folderID: _folderID,
|
||||
payload,
|
||||
search,
|
||||
user,
|
||||
}: Args): Promise<GetFolderDataResult> => {
|
||||
const parentFolderID = parseDocumentID({
|
||||
id: _folderID,
|
||||
collectionSlug: payload.config.folders.slug,
|
||||
payload,
|
||||
})
|
||||
|
||||
const breadcrumbsPromise = getFolderBreadcrumbs({
|
||||
folderID: parentFolderID,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
|
||||
if (parentFolderID) {
|
||||
// subfolders and documents are queried together
|
||||
const documentAndSubfolderPromise = queryDocumentsAndFoldersFromJoin({
|
||||
collectionSlug,
|
||||
parentFolderID,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
const [breadcrumbs, documentsAndSubfolders] = await Promise.all([
|
||||
breadcrumbsPromise,
|
||||
documentAndSubfolderPromise,
|
||||
])
|
||||
|
||||
return {
|
||||
breadcrumbs,
|
||||
documents: documentsAndSubfolders.documents,
|
||||
subfolders: documentsAndSubfolders.subfolders,
|
||||
}
|
||||
} else {
|
||||
// subfolders and documents are queried separately
|
||||
const subfoldersPromise = getOrphanedDocs({
|
||||
collectionSlug: payload.config.folders.slug,
|
||||
payload,
|
||||
search,
|
||||
user,
|
||||
})
|
||||
const documentsPromise = collectionSlug
|
||||
? getOrphanedDocs({
|
||||
collectionSlug,
|
||||
payload,
|
||||
search,
|
||||
user,
|
||||
})
|
||||
: Promise.resolve([])
|
||||
const [breadcrumbs, subfolders, documents] = await Promise.all([
|
||||
breadcrumbsPromise,
|
||||
subfoldersPromise,
|
||||
documentsPromise,
|
||||
])
|
||||
return {
|
||||
breadcrumbs,
|
||||
documents,
|
||||
subfolders,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { User } from '../../auth/types.js'
|
||||
import type { PaginatedDocs } from '../../database/types.js'
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type { Document, Payload } from '../../types/index.js'
|
||||
import type { FolderOrDocument } from '../types.js'
|
||||
|
||||
import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js'
|
||||
|
||||
type QueryDocumentsAndFoldersResults = {
|
||||
documents: FolderOrDocument[]
|
||||
subfolders: FolderOrDocument[]
|
||||
}
|
||||
type QueryDocumentsAndFoldersArgs = {
|
||||
collectionSlug?: CollectionSlug
|
||||
parentFolderID: number | string
|
||||
payload: Payload
|
||||
user?: User
|
||||
}
|
||||
export async function queryDocumentsAndFoldersFromJoin({
|
||||
collectionSlug,
|
||||
parentFolderID,
|
||||
payload,
|
||||
user,
|
||||
}: QueryDocumentsAndFoldersArgs): Promise<QueryDocumentsAndFoldersResults> {
|
||||
const folderCollectionSlugs: string[] = payload.config.collections.reduce<string[]>(
|
||||
(acc, collection) => {
|
||||
if (collection?.admin?.folders) {
|
||||
acc.push(collection.slug)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const subfolderDoc = (await payload.find({
|
||||
collection: payload.config.folders.slug,
|
||||
joins: {
|
||||
documentsAndFolders: {
|
||||
limit: 100_000,
|
||||
sort: 'name',
|
||||
where: {
|
||||
relationTo: {
|
||||
in: [
|
||||
payload.config.folders.slug,
|
||||
...(collectionSlug ? [collectionSlug] : folderCollectionSlugs),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
limit: 1,
|
||||
overrideAccess: false,
|
||||
user,
|
||||
where: {
|
||||
id: {
|
||||
equals: parentFolderID,
|
||||
},
|
||||
},
|
||||
})) as PaginatedDocs<Document>
|
||||
|
||||
const childrenDocs = subfolderDoc?.docs[0]?.documentsAndFolders?.docs || []
|
||||
|
||||
const results: QueryDocumentsAndFoldersResults = childrenDocs.reduce(
|
||||
(acc: QueryDocumentsAndFoldersResults, doc: Document) => {
|
||||
const { relationTo, value } = doc
|
||||
const item = formatFolderOrDocumentItem({
|
||||
folderFieldName: payload.config.folders.fieldName,
|
||||
isUpload: Boolean(payload.collections[relationTo].config.upload),
|
||||
relationTo,
|
||||
useAsTitle: payload.collections[relationTo].config.admin?.useAsTitle,
|
||||
value,
|
||||
})
|
||||
|
||||
if (relationTo === payload.config.folders.slug) {
|
||||
acc.subfolders.push(item)
|
||||
} else {
|
||||
acc.documents.push(item)
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{
|
||||
documents: [],
|
||||
subfolders: [],
|
||||
},
|
||||
)
|
||||
|
||||
return results
|
||||
}
|
||||
61
packages/payload/src/folders/utils/getOrphanedDocs.ts
Normal file
61
packages/payload/src/folders/utils/getOrphanedDocs.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { CollectionSlug, Payload, User, Where } from '../../index.js'
|
||||
import type { FolderOrDocument } from '../types.js'
|
||||
|
||||
import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js'
|
||||
|
||||
type Args = {
|
||||
collectionSlug: CollectionSlug
|
||||
payload: Payload
|
||||
search?: string
|
||||
user?: User
|
||||
}
|
||||
export async function getOrphanedDocs({
|
||||
collectionSlug,
|
||||
payload,
|
||||
search,
|
||||
user,
|
||||
}: Args): Promise<FolderOrDocument[]> {
|
||||
let whereConstraints: Where = {
|
||||
or: [
|
||||
{
|
||||
[payload.config.folders.fieldName]: {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
[payload.config.folders.fieldName]: {
|
||||
equals: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
if (collectionSlug && search && payload.collections[collectionSlug].config.admin?.useAsTitle) {
|
||||
whereConstraints = {
|
||||
[payload.collections[collectionSlug].config.admin.useAsTitle]: {
|
||||
like: search,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const orphanedFolders = await payload.find({
|
||||
collection: collectionSlug,
|
||||
limit: 0,
|
||||
overrideAccess: false,
|
||||
sort: payload.collections[collectionSlug].config.admin.useAsTitle,
|
||||
user,
|
||||
where: whereConstraints,
|
||||
})
|
||||
|
||||
return (
|
||||
orphanedFolders?.docs.map((doc) =>
|
||||
formatFolderOrDocumentItem({
|
||||
folderFieldName: payload.config.folders.fieldName,
|
||||
isUpload: Boolean(payload.collections[collectionSlug].config.upload),
|
||||
relationTo: collectionSlug,
|
||||
useAsTitle: payload.collections[collectionSlug].config.admin.useAsTitle,
|
||||
value: doc,
|
||||
}),
|
||||
) || []
|
||||
)
|
||||
}
|
||||
@@ -1371,6 +1371,7 @@ export type {
|
||||
UploadFieldValidation,
|
||||
UsernameFieldValidation,
|
||||
} from './fields/validations.js'
|
||||
export { getFolderData } from './folders/utils/getFolderData.js'
|
||||
export {
|
||||
type ClientGlobalConfig,
|
||||
createClientGlobalConfig,
|
||||
@@ -1378,7 +1379,6 @@ export {
|
||||
type ServerOnlyGlobalAdminProperties,
|
||||
type ServerOnlyGlobalProperties,
|
||||
} from './globals/config/client.js'
|
||||
|
||||
export type {
|
||||
AfterChangeHook as GlobalAfterChangeHook,
|
||||
AfterReadHook as GlobalAfterReadHook,
|
||||
@@ -1390,7 +1390,6 @@ export type {
|
||||
GlobalConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from './globals/config/types.js'
|
||||
|
||||
export { docAccessOperation as docAccessOperationGlobal } from './globals/operations/docAccess.js'
|
||||
|
||||
export { findOneOperation } from './globals/operations/findOne.js'
|
||||
@@ -1437,11 +1436,11 @@ export type {
|
||||
WorkflowTypes,
|
||||
} from './queues/config/types/workflowTypes.js'
|
||||
export { importHandlerPath } from './queues/operations/runJobs/runJob/importHandlerPath.js'
|
||||
|
||||
export { getLocalI18n } from './translations/getLocalI18n.js'
|
||||
export * from './types/index.js'
|
||||
export { getFileByPath } from './uploads/getFileByPath.js'
|
||||
export type * from './uploads/types.js'
|
||||
|
||||
export { addDataAndFileToRequest } from './utilities/addDataAndFileToRequest.js'
|
||||
|
||||
export { addLocalesToRequestFromData, sanitizeLocales } from './utilities/addLocalesToRequest.js'
|
||||
@@ -1499,6 +1498,7 @@ export { logError } from './utilities/logError.js'
|
||||
export { defaultLoggerOptions } from './utilities/logger.js'
|
||||
export { mapAsync } from './utilities/mapAsync.js'
|
||||
export { mergeHeaders } from './utilities/mergeHeaders.js'
|
||||
export { parseDocumentID } from './utilities/parseDocumentID.js'
|
||||
export { sanitizeFallbackLocale } from './utilities/sanitizeFallbackLocale.js'
|
||||
export { sanitizeJoinParams } from './utilities/sanitizeJoinParams.js'
|
||||
export { sanitizePopulateParam } from './utilities/sanitizePopulateParam.js'
|
||||
@@ -1515,6 +1515,7 @@ export { appendVersionToQueryKey } from './versions/drafts/appendVersionToQueryK
|
||||
export { getQueryDraftsSort } from './versions/drafts/getQueryDraftsSort.js'
|
||||
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
|
||||
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
|
||||
|
||||
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
|
||||
export { saveVersion } from './versions/saveVersion.js'
|
||||
export type { SchedulePublishTaskInput } from './versions/schedule/types.js'
|
||||
|
||||
19
packages/payload/src/utilities/combineWhereConstraints.ts
Normal file
19
packages/payload/src/utilities/combineWhereConstraints.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Where } from '../types/index.js'
|
||||
|
||||
export function combineWhereConstraints(
|
||||
constraints: Array<undefined | Where>,
|
||||
as: 'and' | 'or' = 'and',
|
||||
): Where {
|
||||
if (constraints.length === 0) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
[as]: constraints.filter((constraint): constraint is Where => {
|
||||
if (constraint && typeof constraint === 'object' && Object.keys(constraint).length > 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}),
|
||||
}
|
||||
}
|
||||
9
packages/payload/src/utilities/extractID.ts
Normal file
9
packages/payload/src/utilities/extractID.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const extractID = <IDType extends number | string>(
|
||||
objectOrID: { id: IDType } | IDType,
|
||||
): IDType => {
|
||||
if (typeof objectOrID === 'string' || typeof objectOrID === 'number') {
|
||||
return objectOrID
|
||||
}
|
||||
|
||||
return objectOrID.id
|
||||
}
|
||||
15
packages/payload/src/utilities/parseDocumentID.ts
Normal file
15
packages/payload/src/utilities/parseDocumentID.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { CollectionSlug, Payload } from '../index.js'
|
||||
|
||||
import { isNumber } from './isNumber.js'
|
||||
|
||||
type ParseDocumentIDArgs = {
|
||||
collectionSlug: CollectionSlug
|
||||
id?: number | string
|
||||
payload: Payload
|
||||
}
|
||||
|
||||
export function parseDocumentID({ id, collectionSlug, payload }: ParseDocumentIDArgs) {
|
||||
const idType = payload.collections[collectionSlug]?.customIDType ?? payload.db.defaultIDType
|
||||
|
||||
return id ? (idType === 'number' && isNumber(id) ? parseFloat(String(id)) : id) : undefined
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-multi-tenant",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Multi Tenant plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -21,7 +21,7 @@ export const WatchTenantCollection = () => {
|
||||
const [useAsTitleName] = React.useState(
|
||||
() => (getEntityConfig({ collectionSlug }) as ClientCollectionConfig).admin.useAsTitle,
|
||||
)
|
||||
const titleField = useFormFields(([fields]) => fields[useAsTitleName])
|
||||
const titleField = useFormFields(([fields]) => (useAsTitleName ? fields[useAsTitleName] : {}))
|
||||
|
||||
const { updateTenants } = useTenantSelection()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Sentry plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -370,6 +370,7 @@
|
||||
"@types/uuid": "10.0.0",
|
||||
"acorn": "8.12.1",
|
||||
"bson-objectid": "2.0.4",
|
||||
"csstype": "3.1.3",
|
||||
"dequal": "2.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
"jsox": "1.2.121",
|
||||
|
||||
@@ -20,6 +20,7 @@ export { StrikethroughFeatureClient } from '../../features/format/strikethrough/
|
||||
export { SubscriptFeatureClient } from '../../features/format/subscript/feature.client.js'
|
||||
export { SuperscriptFeatureClient } from '../../features/format/superscript/feature.client.js'
|
||||
export { UnderlineFeatureClient } from '../../features/format/underline/feature.client.js'
|
||||
export { TextStateFeatureClient } from '../../features/textState/feature.client.js'
|
||||
export { HeadingFeatureClient } from '../../features/heading/client/index.js'
|
||||
export { HorizontalRuleFeatureClient } from '../../features/horizontalRule/client/index.js'
|
||||
export { IndentFeatureClient } from '../../features/indent/client/index.js'
|
||||
|
||||
@@ -88,12 +88,12 @@ const nativeMax = Math.max,
|
||||
* @category Function
|
||||
* @param {Function} func The function to debounce.
|
||||
* @param {number} [wait=0] The number of milliseconds to delay.
|
||||
* @param {Object} [options={}] The options object.
|
||||
* @param {boolean} [options.leading=false]
|
||||
* @param {Object} [sortOnOptions={}] The options object.
|
||||
* @param {boolean} [sortOnOptions.leading=false]
|
||||
* Specify invoking on the leading edge of the timeout.
|
||||
* @param {number} [options.maxWait]
|
||||
* @param {number} [sortOnOptions.maxWait]
|
||||
* The maximum time `func` is allowed to be delayed before it's invoked.
|
||||
* @param {boolean} [options.trailing=true]
|
||||
* @param {boolean} [sortOnOptions.trailing=true]
|
||||
* Specify invoking on the trailing edge of the timeout.
|
||||
* @returns {Function} Returns the new debounced function.
|
||||
* @example
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import type { StateValues } from './feature.server.js'
|
||||
|
||||
const tailwindColors = {
|
||||
amber: {
|
||||
'50': 'oklch(0.987 0.022 95.277)',
|
||||
'100': 'oklch(0.962 0.059 95.617)',
|
||||
'200': 'oklch(0.924 0.12 95.746)',
|
||||
'300': 'oklch(0.879 0.169 91.605)',
|
||||
'400': 'oklch(0.828 0.189 84.429)',
|
||||
'500': 'oklch(0.769 0.188 70.08)',
|
||||
'600': 'oklch(0.666 0.179 58.318)',
|
||||
'700': 'oklch(0.555 0.163 48.998)',
|
||||
'800': 'oklch(0.473 0.137 46.201)',
|
||||
'900': 'oklch(0.414 0.112 45.904)',
|
||||
'950': 'oklch(0.279 0.077 45.635)',
|
||||
},
|
||||
black: '#000',
|
||||
blue: {
|
||||
'50': 'oklch(0.97 0.014 254.604)',
|
||||
'100': 'oklch(0.932 0.032 255.585)',
|
||||
'200': 'oklch(0.882 0.059 254.128)',
|
||||
'300': 'oklch(0.809 0.105 251.813)',
|
||||
'400': 'oklch(0.707 0.165 254.624)',
|
||||
'500': 'oklch(0.623 0.214 259.815)',
|
||||
'600': 'oklch(0.546 0.245 262.881)',
|
||||
'700': 'oklch(0.488 0.243 264.376)',
|
||||
'800': 'oklch(0.424 0.199 265.638)',
|
||||
'900': 'oklch(0.379 0.146 265.522)',
|
||||
'950': 'oklch(0.282 0.091 267.935)',
|
||||
},
|
||||
current: 'currentColor',
|
||||
cyan: {
|
||||
'50': 'oklch(0.984 0.019 200.873)',
|
||||
'100': 'oklch(0.956 0.045 203.388)',
|
||||
'200': 'oklch(0.917 0.08 205.041)',
|
||||
'300': 'oklch(0.865 0.127 207.078)',
|
||||
'400': 'oklch(0.789 0.154 211.53)',
|
||||
'500': 'oklch(0.715 0.143 215.221)',
|
||||
'600': 'oklch(0.609 0.126 221.723)',
|
||||
'700': 'oklch(0.52 0.105 223.128)',
|
||||
'800': 'oklch(0.45 0.085 224.283)',
|
||||
'900': 'oklch(0.398 0.07 227.392)',
|
||||
'950': 'oklch(0.302 0.056 229.695)',
|
||||
},
|
||||
emerald: {
|
||||
'50': 'oklch(0.979 0.021 166.113)',
|
||||
'100': 'oklch(0.95 0.052 163.051)',
|
||||
'200': 'oklch(0.905 0.093 164.15)',
|
||||
'300': 'oklch(0.845 0.143 164.978)',
|
||||
'400': 'oklch(0.765 0.177 163.223)',
|
||||
'500': 'oklch(0.696 0.17 162.48)',
|
||||
'600': 'oklch(0.596 0.145 163.225)',
|
||||
'700': 'oklch(0.508 0.118 165.612)',
|
||||
'800': 'oklch(0.432 0.095 166.913)',
|
||||
'900': 'oklch(0.378 0.077 168.94)',
|
||||
'950': 'oklch(0.262 0.051 172.552)',
|
||||
},
|
||||
fuchsia: {
|
||||
'50': 'oklch(0.977 0.017 320.058)',
|
||||
'100': 'oklch(0.952 0.037 318.852)',
|
||||
'200': 'oklch(0.903 0.076 319.62)',
|
||||
'300': 'oklch(0.833 0.145 321.434)',
|
||||
'400': 'oklch(0.74 0.238 322.16)',
|
||||
'500': 'oklch(0.667 0.295 322.15)',
|
||||
'600': 'oklch(0.591 0.293 322.896)',
|
||||
'700': 'oklch(0.518 0.253 323.949)',
|
||||
'800': 'oklch(0.452 0.211 324.591)',
|
||||
'900': 'oklch(0.401 0.17 325.612)',
|
||||
'950': 'oklch(0.293 0.136 325.661)',
|
||||
},
|
||||
gray: {
|
||||
'50': 'oklch(0.985 0.002 247.839)',
|
||||
'100': 'oklch(0.967 0.003 264.542)',
|
||||
'200': 'oklch(0.928 0.006 264.531)',
|
||||
'300': 'oklch(0.872 0.01 258.338)',
|
||||
'400': 'oklch(0.707 0.022 261.325)',
|
||||
'500': 'oklch(0.551 0.027 264.364)',
|
||||
'600': 'oklch(0.446 0.03 256.802)',
|
||||
'700': 'oklch(0.373 0.034 259.733)',
|
||||
'800': 'oklch(0.278 0.033 256.848)',
|
||||
'900': 'oklch(0.21 0.034 264.665)',
|
||||
'950': 'oklch(0.13 0.028 261.692)',
|
||||
},
|
||||
green: {
|
||||
'50': 'oklch(0.982 0.018 155.826)',
|
||||
'100': 'oklch(0.962 0.044 156.743)',
|
||||
'200': 'oklch(0.925 0.084 155.995)',
|
||||
'300': 'oklch(0.871 0.15 154.449)',
|
||||
'400': 'oklch(0.792 0.209 151.711)',
|
||||
'500': 'oklch(0.723 0.219 149.579)',
|
||||
'600': 'oklch(0.627 0.194 149.214)',
|
||||
'700': 'oklch(0.527 0.154 150.069)',
|
||||
'800': 'oklch(0.448 0.119 151.328)',
|
||||
'900': 'oklch(0.393 0.095 152.535)',
|
||||
'950': 'oklch(0.266 0.065 152.934)',
|
||||
},
|
||||
indigo: {
|
||||
'50': 'oklch(0.962 0.018 272.314)',
|
||||
'100': 'oklch(0.93 0.034 272.788)',
|
||||
'200': 'oklch(0.87 0.065 274.039)',
|
||||
'300': 'oklch(0.785 0.115 274.713)',
|
||||
'400': 'oklch(0.673 0.182 276.935)',
|
||||
'500': 'oklch(0.585 0.233 277.117)',
|
||||
'600': 'oklch(0.511 0.262 276.966)',
|
||||
'700': 'oklch(0.457 0.24 277.023)',
|
||||
'800': 'oklch(0.398 0.195 277.366)',
|
||||
'900': 'oklch(0.359 0.144 278.697)',
|
||||
'950': 'oklch(0.257 0.09 281.288)',
|
||||
},
|
||||
inherit: 'inherit',
|
||||
lime: {
|
||||
'50': 'oklch(0.986 0.031 120.757)',
|
||||
'100': 'oklch(0.967 0.067 122.328)',
|
||||
'200': 'oklch(0.938 0.127 124.321)',
|
||||
'300': 'oklch(0.897 0.196 126.665)',
|
||||
'400': 'oklch(0.841 0.238 128.85)',
|
||||
'500': 'oklch(0.768 0.233 130.85)',
|
||||
'600': 'oklch(0.648 0.2 131.684)',
|
||||
'700': 'oklch(0.532 0.157 131.589)',
|
||||
'800': 'oklch(0.453 0.124 130.933)',
|
||||
'900': 'oklch(0.405 0.101 131.063)',
|
||||
'950': 'oklch(0.274 0.072 132.109)',
|
||||
},
|
||||
neutral: {
|
||||
'50': 'oklch(0.985 0 0)',
|
||||
'100': 'oklch(0.97 0 0)',
|
||||
'200': 'oklch(0.922 0 0)',
|
||||
'300': 'oklch(0.87 0 0)',
|
||||
'400': 'oklch(0.708 0 0)',
|
||||
'500': 'oklch(0.556 0 0)',
|
||||
'600': 'oklch(0.439 0 0)',
|
||||
'700': 'oklch(0.371 0 0)',
|
||||
'800': 'oklch(0.269 0 0)',
|
||||
'900': 'oklch(0.205 0 0)',
|
||||
'950': 'oklch(0.145 0 0)',
|
||||
},
|
||||
orange: {
|
||||
'50': 'oklch(0.98 0.016 73.684)',
|
||||
'100': 'oklch(0.954 0.038 75.164)',
|
||||
'200': 'oklch(0.901 0.076 70.697)',
|
||||
'300': 'oklch(0.837 0.128 66.29)',
|
||||
'400': 'oklch(0.75 0.183 55.934)',
|
||||
'500': 'oklch(0.705 0.213 47.604)',
|
||||
'600': 'oklch(0.646 0.222 41.116)',
|
||||
'700': 'oklch(0.553 0.195 38.402)',
|
||||
'800': 'oklch(0.47 0.157 37.304)',
|
||||
'900': 'oklch(0.408 0.123 38.172)',
|
||||
'950': 'oklch(0.266 0.079 36.259)',
|
||||
},
|
||||
pink: {
|
||||
'50': 'oklch(0.971 0.014 343.198)',
|
||||
'100': 'oklch(0.948 0.028 342.258)',
|
||||
'200': 'oklch(0.899 0.061 343.231)',
|
||||
'300': 'oklch(0.823 0.12 346.018)',
|
||||
'400': 'oklch(0.718 0.202 349.761)',
|
||||
'500': 'oklch(0.656 0.241 354.308)',
|
||||
'600': 'oklch(0.592 0.249 0.584)',
|
||||
'700': 'oklch(0.525 0.223 3.958)',
|
||||
'800': 'oklch(0.459 0.187 3.815)',
|
||||
'900': 'oklch(0.408 0.153 2.432)',
|
||||
'950': 'oklch(0.284 0.109 3.907)',
|
||||
},
|
||||
purple: {
|
||||
'50': 'oklch(0.977 0.014 308.299)',
|
||||
'100': 'oklch(0.946 0.033 307.174)',
|
||||
'200': 'oklch(0.902 0.063 306.703)',
|
||||
'300': 'oklch(0.827 0.119 306.383)',
|
||||
'400': 'oklch(0.714 0.203 305.504)',
|
||||
'500': 'oklch(0.627 0.265 303.9)',
|
||||
'600': 'oklch(0.558 0.288 302.321)',
|
||||
'700': 'oklch(0.496 0.265 301.924)',
|
||||
'800': 'oklch(0.438 0.218 303.724)',
|
||||
'900': 'oklch(0.381 0.176 304.987)',
|
||||
'950': 'oklch(0.291 0.149 302.717)',
|
||||
},
|
||||
red: {
|
||||
'50': 'oklch(0.971 0.013 17.38)',
|
||||
'100': 'oklch(0.936 0.032 17.717)',
|
||||
'200': 'oklch(0.885 0.062 18.334)',
|
||||
'300': 'oklch(0.808 0.114 19.571)',
|
||||
'400': 'oklch(0.704 0.191 22.216)',
|
||||
'500': 'oklch(0.637 0.237 25.331)',
|
||||
'600': 'oklch(0.577 0.245 27.325)',
|
||||
'700': 'oklch(0.505 0.213 27.518)',
|
||||
'800': 'oklch(0.444 0.177 26.899)',
|
||||
'900': 'oklch(0.396 0.141 25.723)',
|
||||
'950': 'oklch(0.258 0.092 26.042)',
|
||||
},
|
||||
rose: {
|
||||
'50': 'oklch(0.969 0.015 12.422)',
|
||||
'100': 'oklch(0.941 0.03 12.58)',
|
||||
'200': 'oklch(0.892 0.058 10.001)',
|
||||
'300': 'oklch(0.81 0.117 11.638)',
|
||||
'400': 'oklch(0.712 0.194 13.428)',
|
||||
'500': 'oklch(0.645 0.246 16.439)',
|
||||
'600': 'oklch(0.586 0.253 17.585)',
|
||||
'700': 'oklch(0.514 0.222 16.935)',
|
||||
'800': 'oklch(0.455 0.188 13.697)',
|
||||
'900': 'oklch(0.41 0.159 10.272)',
|
||||
'950': 'oklch(0.271 0.105 12.094)',
|
||||
},
|
||||
sky: {
|
||||
'50': 'oklch(0.977 0.013 236.62)',
|
||||
'100': 'oklch(0.951 0.026 236.824)',
|
||||
'200': 'oklch(0.901 0.058 230.902)',
|
||||
'300': 'oklch(0.828 0.111 230.318)',
|
||||
'400': 'oklch(0.746 0.16 232.661)',
|
||||
'500': 'oklch(0.685 0.169 237.323)',
|
||||
'600': 'oklch(0.588 0.158 241.966)',
|
||||
'700': 'oklch(0.5 0.134 242.749)',
|
||||
'800': 'oklch(0.443 0.11 240.79)',
|
||||
'900': 'oklch(0.391 0.09 240.876)',
|
||||
'950': 'oklch(0.293 0.066 243.157)',
|
||||
},
|
||||
slate: {
|
||||
'50': 'oklch(0.984 0.003 247.858)',
|
||||
'100': 'oklch(0.968 0.007 247.896)',
|
||||
'200': 'oklch(0.929 0.013 255.508)',
|
||||
'300': 'oklch(0.869 0.022 252.894)',
|
||||
'400': 'oklch(0.704 0.04 256.788)',
|
||||
'500': 'oklch(0.554 0.046 257.417)',
|
||||
'600': 'oklch(0.446 0.043 257.281)',
|
||||
'700': 'oklch(0.372 0.044 257.287)',
|
||||
'800': 'oklch(0.279 0.041 260.031)',
|
||||
'900': 'oklch(0.208 0.042 265.755)',
|
||||
'950': 'oklch(0.129 0.042 264.695)',
|
||||
},
|
||||
stone: {
|
||||
'50': 'oklch(0.985 0.001 106.423)',
|
||||
'100': 'oklch(0.97 0.001 106.424)',
|
||||
'200': 'oklch(0.923 0.003 48.717)',
|
||||
'300': 'oklch(0.869 0.005 56.366)',
|
||||
'400': 'oklch(0.709 0.01 56.259)',
|
||||
'500': 'oklch(0.553 0.013 58.071)',
|
||||
'600': 'oklch(0.444 0.011 73.639)',
|
||||
'700': 'oklch(0.374 0.01 67.558)',
|
||||
'800': 'oklch(0.268 0.007 34.298)',
|
||||
'900': 'oklch(0.216 0.006 56.043)',
|
||||
'950': 'oklch(0.147 0.004 49.25)',
|
||||
},
|
||||
teal: {
|
||||
'50': 'oklch(0.984 0.014 180.72)',
|
||||
'100': 'oklch(0.953 0.051 180.801)',
|
||||
'200': 'oklch(0.91 0.096 180.426)',
|
||||
'300': 'oklch(0.855 0.138 181.071)',
|
||||
'400': 'oklch(0.777 0.152 181.912)',
|
||||
'500': 'oklch(0.704 0.14 182.503)',
|
||||
'600': 'oklch(0.6 0.118 184.704)',
|
||||
'700': 'oklch(0.511 0.096 186.391)',
|
||||
'800': 'oklch(0.437 0.078 188.216)',
|
||||
'900': 'oklch(0.386 0.063 188.416)',
|
||||
'950': 'oklch(0.277 0.046 192.524)',
|
||||
},
|
||||
transparent: 'transparent',
|
||||
violet: {
|
||||
'50': 'oklch(0.969 0.016 293.756)',
|
||||
'100': 'oklch(0.943 0.029 294.588)',
|
||||
'200': 'oklch(0.894 0.057 293.283)',
|
||||
'300': 'oklch(0.811 0.111 293.571)',
|
||||
'400': 'oklch(0.702 0.183 293.541)',
|
||||
'500': 'oklch(0.606 0.25 292.717)',
|
||||
'600': 'oklch(0.541 0.281 293.009)',
|
||||
'700': 'oklch(0.491 0.27 292.581)',
|
||||
'800': 'oklch(0.432 0.232 292.759)',
|
||||
'900': 'oklch(0.38 0.189 293.745)',
|
||||
'950': 'oklch(0.283 0.141 291.089)',
|
||||
},
|
||||
white: '#fff',
|
||||
yellow: {
|
||||
'50': 'oklch(0.987 0.026 102.212)',
|
||||
'100': 'oklch(0.973 0.071 103.193)',
|
||||
'200': 'oklch(0.945 0.129 101.54)',
|
||||
'300': 'oklch(0.905 0.182 98.111)',
|
||||
'400': 'oklch(0.852 0.199 91.936)',
|
||||
'500': 'oklch(0.795 0.184 86.047)',
|
||||
'600': 'oklch(0.681 0.162 75.834)',
|
||||
'700': 'oklch(0.554 0.135 66.442)',
|
||||
'800': 'oklch(0.476 0.114 61.907)',
|
||||
'900': 'oklch(0.421 0.095 57.708)',
|
||||
'950': 'oklch(0.286 0.066 53.813)',
|
||||
},
|
||||
zinc: {
|
||||
'50': 'oklch(0.985 0 0)',
|
||||
'100': 'oklch(0.967 0.001 286.375)',
|
||||
'200': 'oklch(0.92 0.004 286.32)',
|
||||
'300': 'oklch(0.871 0.006 286.286)',
|
||||
'400': 'oklch(0.705 0.015 286.067)',
|
||||
'500': 'oklch(0.552 0.016 285.938)',
|
||||
'600': 'oklch(0.442 0.017 285.786)',
|
||||
'700': 'oklch(0.37 0.013 285.805)',
|
||||
'800': 'oklch(0.274 0.006 286.033)',
|
||||
'900': 'oklch(0.21 0.006 285.885)',
|
||||
'950': 'oklch(0.141 0.005 285.823)',
|
||||
},
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
/* eslint-disable perfectionist/sort-objects */
|
||||
export const defaultColors = {
|
||||
text: {
|
||||
'text-red': { css: { 'color': `light-dark(${tailwindColors.red[600]}, ${tailwindColors.red[400]})`, }, label: 'Red' },
|
||||
'text-orange': { css: { 'color': `light-dark(${tailwindColors.orange[600]}, ${tailwindColors.orange[400]})`, }, label: 'Orange' },
|
||||
'text-yellow': { css: { 'color': `light-dark(${tailwindColors.yellow[700]}, ${tailwindColors.yellow[300]})`, }, label: 'Yellow' },
|
||||
'text-green': { css: { 'color': `light-dark(${tailwindColors.green[700]}, ${tailwindColors.green[400]})`, }, label: 'Green' },
|
||||
'text-blue': { css: { 'color': `light-dark(${tailwindColors.blue[600]}, ${tailwindColors.blue[400]})`, }, label: 'Blue' },
|
||||
'text-purple': { css: { 'color': `light-dark(${tailwindColors.purple[600]}, ${tailwindColors.purple[400]})`, }, label: 'Purple' },
|
||||
'text-pink': { css: { 'color': `light-dark(${tailwindColors.pink[600]}, ${tailwindColors.pink[400]})`, }, label: 'Pink' },
|
||||
} satisfies StateValues,
|
||||
background: {
|
||||
'bg-red': { css: { 'background-color': `light-dark(${tailwindColors.red[400]}, ${tailwindColors.red[600]})`, }, label: 'Red' },
|
||||
'bg-orange': { css: { 'background-color': `light-dark(${tailwindColors.orange[400]}, ${tailwindColors.orange[600]})`, }, label: 'Orange' },
|
||||
'bg-yellow': { css: { 'background-color': `light-dark(${tailwindColors.yellow[300]}, ${tailwindColors.yellow[700]})`, }, label: 'Yellow' },
|
||||
'bg-green': { css: { 'background-color': `light-dark(${tailwindColors.green[400]}, ${tailwindColors.green[700]})`, }, label: 'Green' },
|
||||
'bg-blue': { css: { 'background-color': `light-dark(${tailwindColors.blue[400]}, ${tailwindColors.blue[600]})`, }, label: 'Blue' },
|
||||
'bg-purple': { css: { 'background-color': `light-dark(${tailwindColors.purple[400]}, ${tailwindColors.purple[600]})`, }, label: 'Purple' },
|
||||
'bg-pink': { css: { 'background-color': `light-dark(${tailwindColors.pink[400]}, ${tailwindColors.pink[600]})`, }, label: 'Pink' },
|
||||
} satisfies StateValues
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import type { ToolbarDropdownGroup, ToolbarGroup } from '../toolbars/types.js'
|
||||
import type { TextStateFeatureProps } from './feature.server.js'
|
||||
|
||||
import { TextStateIcon } from '../../lexical/ui/icons/TextState/index.js'
|
||||
import { createClientFeature } from '../../utilities/createClientFeature.js'
|
||||
import { registerTextStates, setTextState, StatePlugin } from './textState.js'
|
||||
|
||||
const toolbarGroups = (props: TextStateFeatureProps): ToolbarGroup[] => {
|
||||
const items: ToolbarDropdownGroup['items'] = []
|
||||
|
||||
for (const stateKey in props.state) {
|
||||
const key = props.state[stateKey]!
|
||||
for (const stateValue in key) {
|
||||
const meta = key[stateValue]!
|
||||
items.push({
|
||||
ChildComponent: () => <TextStateIcon css={meta.css} />,
|
||||
key: stateValue,
|
||||
label: meta.label,
|
||||
onSelect: ({ editor }) => {
|
||||
setTextState(editor, stateKey, stateValue)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clearStyle: ToolbarDropdownGroup['items'] = [
|
||||
{
|
||||
ChildComponent: () => <TextStateIcon />,
|
||||
key: `clear-style`,
|
||||
label: 'Default style',
|
||||
onSelect: ({ editor }) => {
|
||||
for (const stateKey in props.state) {
|
||||
setTextState(editor, stateKey, undefined)
|
||||
}
|
||||
},
|
||||
order: 1,
|
||||
},
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'dropdown',
|
||||
ChildComponent: () => <TextStateIcon css={{ color: 'var(--theme-elevation-600)' }} />,
|
||||
items: [...clearStyle, ...items],
|
||||
key: 'textState',
|
||||
order: 30,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const TextStateFeatureClient = createClientFeature<TextStateFeatureProps>(({ props }) => {
|
||||
registerTextStates(props.state)
|
||||
return {
|
||||
plugins: [
|
||||
{
|
||||
Component: StatePlugin,
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups(props),
|
||||
},
|
||||
toolbarInline: {
|
||||
groups: toolbarGroups(props),
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { PropertiesHyphenFallback } from 'csstype'
|
||||
import type { Prettify } from 'ts-essentials'
|
||||
|
||||
import { createServerFeature } from '../../utilities/createServerFeature.js'
|
||||
|
||||
// extracted from https://github.com/facebook/lexical/pull/7294
|
||||
export type StyleObject = Prettify<{
|
||||
[K in keyof PropertiesHyphenFallback]?:
|
||||
| Extract<PropertiesHyphenFallback[K], string>
|
||||
// This is simplified to not deal with arrays or numbers.
|
||||
// This is an example after all!
|
||||
| undefined
|
||||
}>
|
||||
|
||||
export type StateValues = { [stateValue: string]: { css: StyleObject; label: string } }
|
||||
|
||||
export type TextStateFeatureProps = {
|
||||
/**
|
||||
* The keys of the top-level object (stateKeys) represent the attributes that the textNode can have (e.g., color).
|
||||
* The values of the top-level object (stateValues) represent the values that the attribute can have (e.g., red, blue, etc.).
|
||||
* Within the stateValue, you can define inline styles and labels.
|
||||
*
|
||||
* @note Because this is a common use case, we provide a defaultColors object with colors that
|
||||
* look good in both dark and light mode, which you can use or adapt to your liking.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* import { defaultColors } from '@payloadcms/richtext-lexical'
|
||||
*
|
||||
* state: {
|
||||
* color: {
|
||||
* ...defaultColors.background,
|
||||
* ...defaultColors.text,
|
||||
* // fancy gradients!
|
||||
* galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } },
|
||||
* sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } },
|
||||
* },
|
||||
* // You can have both colored and underlined text at the same time.
|
||||
* // If you don't want that, you should group them within the same key.
|
||||
* // (just like I did with defaultColors and my fancy gradients)
|
||||
* underline: {
|
||||
* 'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } },
|
||||
* // You'll probably want to use the CSS light-dark() utility.
|
||||
* 'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } },
|
||||
* },
|
||||
* }
|
||||
*
|
||||
*/
|
||||
state: { [stateKey: string]: StateValues }
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to store key-value attributes within TextNodes and define inline styles for each combination.
|
||||
* Inline styles are not part of the editorState, reducing the JSON size and allowing you to easily migrate or adapt styles later.
|
||||
*
|
||||
* This feature can be used, among other things, to add colors to text.
|
||||
*
|
||||
* For more information and examples, see the JSdocs for the "state" property that this feature receives as a parameter.
|
||||
*
|
||||
* @experimental There may be breaking changes to this API
|
||||
*/
|
||||
export const TextStateFeature = createServerFeature<
|
||||
TextStateFeatureProps,
|
||||
TextStateFeatureProps,
|
||||
TextStateFeatureProps
|
||||
>({
|
||||
feature: ({ props }) => {
|
||||
return {
|
||||
ClientFeature: '@payloadcms/richtext-lexical/client#TextStateFeatureClient',
|
||||
clientFeatureProps: {
|
||||
state: props.state,
|
||||
},
|
||||
}
|
||||
},
|
||||
key: 'textState',
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { LexicalEditor, StateConfig } from 'lexical'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $forEachSelectedTextNode } from '@lexical/selection'
|
||||
import { $getNodeByKey, $getState, $setState, createState, TextNode } from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { type StateValues, type TextStateFeatureProps } from './feature.server.js'
|
||||
|
||||
const stateMap = new Map<
|
||||
string,
|
||||
{
|
||||
stateConfig: StateConfig<string, string | undefined>
|
||||
stateValues: StateValues
|
||||
}
|
||||
>()
|
||||
|
||||
export function registerTextStates(state: TextStateFeatureProps['state']) {
|
||||
for (const stateKey in state) {
|
||||
const stateValues = state[stateKey]!
|
||||
const stateConfig = createState(stateKey, {
|
||||
parse: (value) =>
|
||||
typeof value === 'string' && Object.keys(stateValues).includes(value) ? value : undefined,
|
||||
})
|
||||
stateMap.set(stateKey, { stateConfig, stateValues })
|
||||
}
|
||||
}
|
||||
|
||||
export function setTextState(editor: LexicalEditor, stateKey: string, value: string | undefined) {
|
||||
editor.update(() => {
|
||||
$forEachSelectedTextNode((textNode) => {
|
||||
const stateMapEntry = stateMap.get(stateKey)
|
||||
if (!stateMapEntry) {
|
||||
throw new Error(`State config for ${stateKey} not found`)
|
||||
}
|
||||
$setState(textNode, stateMapEntry.stateConfig, value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function StatePlugin() {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerMutationListener(TextNode, (mutatedNodes) => {
|
||||
editor.getEditorState().read(() => {
|
||||
for (const [nodeKey, mutation] of mutatedNodes) {
|
||||
if (mutation === 'destroyed') {
|
||||
continue
|
||||
}
|
||||
const node = $getNodeByKey(nodeKey)
|
||||
const dom = editor.getElementByKey(nodeKey)
|
||||
if (!node || !dom) {
|
||||
continue
|
||||
}
|
||||
// stateKey could be color for example
|
||||
stateMap.forEach((stateEntry, _stateKey) => {
|
||||
// stateValue could be bg-red for example
|
||||
const stateValue = $getState(node, stateEntry.stateConfig)
|
||||
if (!stateValue) {
|
||||
delete dom.dataset[_stateKey]
|
||||
dom.style.cssText = ''
|
||||
return
|
||||
}
|
||||
dom.dataset[_stateKey] = stateValue
|
||||
const css = stateEntry.stateValues[stateValue]?.css
|
||||
if (!css) {
|
||||
return
|
||||
}
|
||||
Object.entries(css).forEach(([key, value]) => {
|
||||
dom.style.setProperty(key, value)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -925,21 +925,21 @@ export { HeadingFeature, type HeadingFeatureProps } from './features/heading/ser
|
||||
export { HorizontalRuleFeature } from './features/horizontalRule/server/index.js'
|
||||
|
||||
export { IndentFeature } from './features/indent/server/index.js'
|
||||
|
||||
export { AutoLinkNode } from './features/link/nodes/AutoLinkNode.js'
|
||||
export { LinkNode } from './features/link/nodes/LinkNode.js'
|
||||
|
||||
export type { LinkFields } from './features/link/nodes/types.js'
|
||||
export { LinkFeature, type LinkFeatureServerProps } from './features/link/server/index.js'
|
||||
export { ChecklistFeature } from './features/lists/checklist/server/index.js'
|
||||
export { OrderedListFeature } from './features/lists/orderedList/server/index.js'
|
||||
|
||||
export { UnorderedListFeature } from './features/lists/unorderedList/server/index.js'
|
||||
|
||||
export type {
|
||||
SlateNode,
|
||||
SlateNodeConverter,
|
||||
} from './features/migrations/slateToLexical/converter/types.js'
|
||||
export { ParagraphFeature } from './features/paragraph/server/index.js'
|
||||
|
||||
export {
|
||||
RelationshipFeature,
|
||||
type RelationshipFeatureProps,
|
||||
@@ -949,6 +949,9 @@ export {
|
||||
type RelationshipData,
|
||||
RelationshipServerNode,
|
||||
} from './features/relationship/server/nodes/RelationshipNode.js'
|
||||
export { defaultColors } from './features/textState/defaultColors.js'
|
||||
export { TextStateFeature } from './features/textState/feature.server.js'
|
||||
|
||||
export { FixedToolbarFeature } from './features/toolbars/fixed/server/index.js'
|
||||
|
||||
export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js'
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { StyleObject } from '../../../../features/textState/feature.server.js'
|
||||
|
||||
function kebabToCamelCase(str: string): string {
|
||||
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
export const TextStateIcon: React.FC<{
|
||||
css?: StyleObject
|
||||
}> = ({ css }) => {
|
||||
const convertedCss = css
|
||||
? Object.fromEntries(Object.entries(css).map(([key, value]) => [kebabToCamelCase(key), value]))
|
||||
: {}
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
...convertedCss,
|
||||
alignItems: 'center',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
fontSize: '16px',
|
||||
height: '20px',
|
||||
justifyContent: 'center',
|
||||
width: '20px',
|
||||
}}
|
||||
>
|
||||
A
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.38.0",
|
||||
"version": "3.39.1",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user