Compare commits
31 Commits
chore/beta
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5427ad3b8 | ||
|
|
763a34f19b | ||
|
|
be0462db56 | ||
|
|
6e55a2e52d | ||
|
|
4e127054ca | ||
|
|
27510bb963 | ||
|
|
de45e6094b | ||
|
|
74159de1ec | ||
|
|
ba92d864bb | ||
|
|
0fb14cfebe | ||
|
|
2ada6fc58d | ||
|
|
cb3355b30f | ||
|
|
10c6ffafc3 | ||
|
|
6512d5ce69 | ||
|
|
57fcc9148e | ||
|
|
36f4f23463 | ||
|
|
7b7dc71845 | ||
|
|
ba513d5a97 | ||
|
|
a26d03190e | ||
|
|
9f525621c8 | ||
|
|
7309d474ee | ||
|
|
45e86832c2 | ||
|
|
1bd91b23ca | ||
|
|
ac34380eb8 | ||
|
|
17707852e0 | ||
|
|
8b95218577 | ||
|
|
a79d23c631 | ||
|
|
52c81ad525 | ||
|
|
8ec836737e | ||
|
|
e4a90294ea | ||
|
|
7c8d562f03 |
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -111,6 +111,13 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js field-error-states",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Field Error States",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
||||
@@ -22,6 +22,7 @@ Collections and Globals both support the same options for configuring drafts. Yo
|
||||
| Draft Option | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](/docs/versions/autosave). |
|
||||
| `validate` | Set `validate` to `true` to validate draft documents when saved. Default is `false`. |
|
||||
|
||||
## Database changes
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export default withBundleAnalyzer(
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
}),
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -9,6 +9,7 @@
|
||||
"build:app": "next build",
|
||||
"build:app:analyze": "cross-env ANALYZE=true next build",
|
||||
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\"",
|
||||
"build:core:force": "pnpm clean:build && turbo build --filter \"!@payloadcms/plugin-*\" --no-cache --force",
|
||||
"build:create-payload-app": "turbo build --filter create-payload-app",
|
||||
"build:db-mongodb": "turbo build --filter db-mongodb",
|
||||
"build:db-postgres": "turbo build --filter db-postgres",
|
||||
@@ -42,21 +43,21 @@
|
||||
"build:translations": "turbo build --filter translations",
|
||||
"build:ui": "turbo build --filter ui",
|
||||
"clean": "turbo clean",
|
||||
"clean:all": "find . \\( -type d \\( -name node_modules -o -name dist -o -name .cache -o -name .next -o -name .turbo \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} +",
|
||||
"clean:build": "find . \\( -type d \\( -name dist -o -name .cache -o -name .next -o -name .turbo \\) -o -type f -name tsconfig.tsbuildinfo \\) -not -path '*/node_modules/*' -exec rm -rf {} +",
|
||||
"clean:cache": "rimraf node_modules/.cache && rimraf packages/payload/node_modules/.cache && rimraf .next",
|
||||
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media' '**/dist' '**/.cache' '**/.next' '**/.turbo' '**/tsconfig.tsbuildinfo' '**/payload*.tgz'",
|
||||
"clean:build": "node ./scripts/delete-recursively.js 'media' '**/dist' '**/.cache' '**/.next' '**/.turbo' '**/tsconfig.tsbuildinfo' '**/payload*.tgz'",
|
||||
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation node ./test/dev.js",
|
||||
"dev:generate-graphql-schema": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateGraphQLSchema.ts",
|
||||
"dev:generate-types": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateTypes.ts",
|
||||
"dev:postgres": "cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DATABASE=postgres node ./test/dev.js",
|
||||
"devsafe": "rimraf .next && pnpm dev",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
|
||||
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
|
||||
"fix": "eslint \"packages/**/*.ts\" --fix",
|
||||
"lint": "eslint \"packages/**/*.ts\"",
|
||||
"lint-staged": "lint-staged",
|
||||
"obliterate-playwright-cache": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
"prepare": "husky install",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release:alpha": "tsx ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
@@ -123,6 +124,7 @@
|
||||
"husky": "^8.0.3",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"json-schema-to-typescript": "11.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^9.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
1
packages/graphql/src/exports/types.ts
Normal file
1
packages/graphql/src/exports/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { GraphQLJSON, GraphQLJSONObject } from '../packages/graphql-type-json/index.js'
|
||||
@@ -1 +1,2 @@
|
||||
export { generateSchema } from '../bin/generateSchema.js'
|
||||
export { buildObjectType } from '../schema/buildObjectType.js'
|
||||
|
||||
@@ -81,7 +81,7 @@ type Args = {
|
||||
parentName: string
|
||||
}
|
||||
|
||||
function buildObjectType({
|
||||
export function buildObjectType({
|
||||
name,
|
||||
baseFields = {},
|
||||
config,
|
||||
@@ -492,13 +492,13 @@ function buildObjectType({
|
||||
// is run here again, with the provided depth.
|
||||
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
|
||||
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
|
||||
if (editor?.populationPromises) {
|
||||
if (editor?.graphQLPopulationPromises) {
|
||||
const fieldPromises = []
|
||||
const populationPromises = []
|
||||
const populateDepth =
|
||||
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
|
||||
|
||||
editor?.populationPromises({
|
||||
editor?.graphQLPopulationPromises({
|
||||
context,
|
||||
depth: populateDepth,
|
||||
draft: args.draft,
|
||||
@@ -698,5 +698,3 @@ function buildObjectType({
|
||||
|
||||
return newlyCreatedBlockType
|
||||
}
|
||||
|
||||
export default buildObjectType
|
||||
|
||||
@@ -37,7 +37,7 @@ import restoreVersionResolver from '../resolvers/collections/restoreVersion.js'
|
||||
import { updateResolver } from '../resolvers/collections/update.js'
|
||||
import formatName from '../utilities/formatName.js'
|
||||
import { buildMutationInputType, getCollectionIDType } from './buildMutationInputType.js'
|
||||
import buildObjectType from './buildObjectType.js'
|
||||
import { buildObjectType } from './buildObjectType.js'
|
||||
import buildPaginatedListType from './buildPaginatedListType.js'
|
||||
import { buildPolicyType } from './buildPoliciesType.js'
|
||||
import buildWhereInputType from './buildWhereInputType.js'
|
||||
|
||||
@@ -17,7 +17,7 @@ import restoreVersionResolver from '../resolvers/globals/restoreVersion.js'
|
||||
import updateResolver from '../resolvers/globals/update.js'
|
||||
import formatName from '../utilities/formatName.js'
|
||||
import { buildMutationInputType } from './buildMutationInputType.js'
|
||||
import buildObjectType from './buildObjectType.js'
|
||||
import { buildObjectType } from './buildObjectType.js'
|
||||
import buildPaginatedListType from './buildPaginatedListType.js'
|
||||
import { buildPolicyType } from './buildPoliciesType.js'
|
||||
import buildWhereInputType from './buildWhereInputType.js'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -54,8 +54,8 @@
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"qs": "6.11.2",
|
||||
"react-diff-viewer-continued": "3.2.6",
|
||||
"react-toastify": "10.0.5",
|
||||
"sass": "1.77.4",
|
||||
"sonner": "^1.5.0",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { parseCookies } from 'payload/auth'
|
||||
import { createClientConfig } from 'payload/config'
|
||||
import React from 'react'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import './styles.scss';
|
||||
@import './toastify.scss';
|
||||
@import './toasts.scss';
|
||||
@import './colors.scss';
|
||||
|
||||
:root {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
@import 'vars';
|
||||
|
||||
.Toastify {
|
||||
.Toastify__toast-container {
|
||||
left: base(5);
|
||||
transform: none;
|
||||
right: base(5);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.Toastify__toast {
|
||||
padding: base(0.5);
|
||||
border-radius: $style-radius-m;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.Toastify__close-button {
|
||||
align-self: center;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.Toastify__toast--success {
|
||||
color: var(--color-success-900);
|
||||
background: var(--color-success-500);
|
||||
|
||||
.Toastify__progress-bar {
|
||||
background-color: var(--color-success-900);
|
||||
}
|
||||
}
|
||||
|
||||
.Toastify__close-button--success {
|
||||
color: var(--color-success-900);
|
||||
}
|
||||
|
||||
.Toastify__toast--error {
|
||||
background: var(--theme-error-500);
|
||||
color: #fff;
|
||||
|
||||
.Toastify__progress-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.Toastify__close-button--light {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
.Toastify__toast-container {
|
||||
left: $baseline;
|
||||
right: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
packages/next/src/scss/toasts.scss
Normal file
111
packages/next/src/scss/toasts.scss
Normal file
@@ -0,0 +1,111 @@
|
||||
.payload-toast-container {
|
||||
.payload-toast-close-button {
|
||||
left: unset;
|
||||
right: 0.5rem;
|
||||
top: 1.55rem;
|
||||
color: var(--theme-elevation-400);
|
||||
background: unset;
|
||||
border: none;
|
||||
display: flex;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
[dir='RTL'] & {
|
||||
right: unset;
|
||||
left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.payload-toast-item {
|
||||
padding: 1rem 2.5rem 1rem 1rem;
|
||||
color: var(--theme-text);
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border-radius: 0.15rem;
|
||||
border: 1px solid var(--theme-border-color);
|
||||
background: var(--theme-input-bg);
|
||||
box-shadow:
|
||||
0px 10px 4px -8px rgba(0, 2, 4, 0.02),
|
||||
0px 2px 3px 0px rgba(0, 2, 4, 0.05);
|
||||
|
||||
.toast-content {
|
||||
transition: opacity 100ms cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||
}
|
||||
|
||||
&[data-front='false'] {
|
||||
.toast-content {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-expanded='true'] {
|
||||
.toast-content {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
svg {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-warning {
|
||||
border-color: var(--theme-warning-200);
|
||||
background-color: var(--theme-warning-100);
|
||||
}
|
||||
|
||||
&.toast-error {
|
||||
border-color: var(--theme-error-300);
|
||||
background-color: var(--theme-error-150);
|
||||
}
|
||||
|
||||
&.toast-success {
|
||||
border-color: var(--theme-success-200);
|
||||
background-color: var(--theme-success-100);
|
||||
}
|
||||
|
||||
&.toast-info {
|
||||
border-color: var(--theme-elevation-250);
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
[data-theme='light'] & {
|
||||
&.toast-warning {
|
||||
border-color: var(--theme-warning-550);
|
||||
background-color: var(--theme-warning-100);
|
||||
}
|
||||
|
||||
&.toast-error {
|
||||
border-color: var(--theme-error-200);
|
||||
background-color: var(--theme-error-50);
|
||||
}
|
||||
|
||||
&.toast-success {
|
||||
border-color: var(--theme-success-550);
|
||||
background-color: var(--theme-success-50);
|
||||
}
|
||||
|
||||
&.toast-info {
|
||||
border-color: var(--theme-border-color);
|
||||
background-color: var(--theme-elevation-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,16 @@ export const reload = async (config: SanitizedConfig, payload: Payload): Promise
|
||||
|
||||
// TODO: support HMR for other props in the future (see payload/src/index init()) hat may change on Payload singleton
|
||||
|
||||
// Generate types
|
||||
if (config.typescript.autoGenerate !== false) {
|
||||
// We cannot run it directly here, as generate-types imports json-schema-to-typescript, which breaks on turbopack.
|
||||
// see: https://github.com/vercel/next.js/issues/66723
|
||||
void payload.bin({
|
||||
args: ['generate:types'],
|
||||
log: false,
|
||||
})
|
||||
}
|
||||
|
||||
await payload.db.init()
|
||||
if (payload.db.connect) {
|
||||
await payload.db.connect({ hotReload: true })
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import { useSearchParams } from 'next/navigation.js'
|
||||
import qs from 'qs'
|
||||
import * as React from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import { LocaleSelector } from './LocaleSelector/index.js'
|
||||
|
||||
@@ -151,8 +151,10 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
hasSavePermission &&
|
||||
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
|
||||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
|
||||
const validateDraftData =
|
||||
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
|
||||
|
||||
if (shouldAutosave && !id && collectionSlug) {
|
||||
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
|
||||
const doc = await payload.create({
|
||||
collection: collectionSlug,
|
||||
data: {},
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
|
||||
@@ -71,7 +71,7 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
toast.success(t('authentication:successfullyUnlocked'), { autoClose: 3000 })
|
||||
toast.success(t('authentication:successfullyUnlocked'))
|
||||
} else {
|
||||
toast.error(t('authentication:failedToUnlock'))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import { email } from 'payload/fields/validations'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export const ForgotPasswordForm: React.FC = () => {
|
||||
const config = useConfig()
|
||||
|
||||
@@ -97,7 +97,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
const sort =
|
||||
query?.sort && typeof query.sort === 'string'
|
||||
? query.sort
|
||||
: listPreferences?.sort || undefined
|
||||
: listPreferences?.sort || collectionConfig.defaultSort || undefined
|
||||
|
||||
const data = await payload.find({
|
||||
collection: collectionSlug,
|
||||
|
||||
@@ -10,16 +10,20 @@ export const DeviceContainer: React.FC<{
|
||||
const { children } = props
|
||||
|
||||
const deviceFrameRef = React.useRef<HTMLDivElement>(null)
|
||||
const outerFrameRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const { breakpoint, setMeasuredDeviceSize, size, zoom } = useLivePreviewContext()
|
||||
const { breakpoint, setMeasuredDeviceSize, size: desiredSize, zoom } = useLivePreviewContext()
|
||||
|
||||
// Keep an accurate measurement of the actual device size as it is truly rendered
|
||||
// This is helpful when `sizes` are non-number units like percentages, etc.
|
||||
const { size: measuredDeviceSize } = useResize(deviceFrameRef)
|
||||
const { size: measuredDeviceSize } = useResize(deviceFrameRef.current)
|
||||
const { size: outerFrameSize } = useResize(outerFrameRef.current)
|
||||
|
||||
let deviceIsLargerThanFrame: boolean = false
|
||||
|
||||
// Sync the measured device size with the context so that other components can use it
|
||||
// This happens from the bottom up so that as this component mounts and unmounts,
|
||||
// Its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe
|
||||
// its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe
|
||||
useEffect(() => {
|
||||
if (measuredDeviceSize) {
|
||||
setMeasuredDeviceSize(measuredDeviceSize)
|
||||
@@ -34,13 +38,34 @@ export const DeviceContainer: React.FC<{
|
||||
|
||||
if (
|
||||
typeof zoom === 'number' &&
|
||||
typeof size.width === 'number' &&
|
||||
typeof size.height === 'number'
|
||||
typeof desiredSize.width === 'number' &&
|
||||
typeof desiredSize.height === 'number' &&
|
||||
typeof measuredDeviceSize.width === 'number' &&
|
||||
typeof measuredDeviceSize.height === 'number'
|
||||
) {
|
||||
const scaledWidth = size.width / zoom
|
||||
const difference = scaledWidth - size.width
|
||||
x = `${difference / 2}px`
|
||||
margin = '0 auto'
|
||||
const scaledDesiredWidth = desiredSize.width / zoom
|
||||
const scaledDeviceWidth = measuredDeviceSize.width * zoom
|
||||
const scaledDeviceDifferencePixels = scaledDesiredWidth - desiredSize.width
|
||||
deviceIsLargerThanFrame = scaledDeviceWidth > outerFrameSize.width
|
||||
|
||||
if (deviceIsLargerThanFrame) {
|
||||
if (zoom > 1) {
|
||||
const differenceFromDeviceToFrame = measuredDeviceSize.width - outerFrameSize.width
|
||||
if (differenceFromDeviceToFrame < 0) x = `${differenceFromDeviceToFrame / 2}px`
|
||||
else x = '0'
|
||||
} else {
|
||||
x = '0'
|
||||
}
|
||||
} else {
|
||||
if (zoom >= 1) {
|
||||
x = `${scaledDeviceDifferencePixels / 2}px`
|
||||
} else {
|
||||
const differenceFromDeviceToFrame = outerFrameSize.width - scaledDeviceWidth
|
||||
x = `${differenceFromDeviceToFrame / 2}px`
|
||||
margin = '0'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,21 +73,29 @@ export const DeviceContainer: React.FC<{
|
||||
let height = zoom ? `${100 / zoom}%` : '100%'
|
||||
|
||||
if (breakpoint !== 'responsive') {
|
||||
width = `${size?.width / (typeof zoom === 'number' ? zoom : 1)}px`
|
||||
height = `${size?.height / (typeof zoom === 'number' ? zoom : 1)}px`
|
||||
width = `${desiredSize?.width / (typeof zoom === 'number' ? zoom : 1)}px`
|
||||
height = `${desiredSize?.height / (typeof zoom === 'number' ? zoom : 1)}px`
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={deviceFrameRef}
|
||||
ref={outerFrameRef}
|
||||
style={{
|
||||
height,
|
||||
margin,
|
||||
transform: `translate3d(${x}, 0, 0)`,
|
||||
width,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
ref={deviceFrameRef}
|
||||
style={{
|
||||
height,
|
||||
margin,
|
||||
transform: `translate3d(${x}, 0, 0)`,
|
||||
width,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
type Args = {
|
||||
token: string
|
||||
@@ -49,7 +49,7 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
|
||||
history.push(`${admin}`)
|
||||
} else {
|
||||
history.push(`${admin}/login`)
|
||||
toast.success(i18n.t('general:updatedSuccessfully'), { autoClose: 3000 })
|
||||
toast.success(i18n.t('general:updatedSuccessfully'))
|
||||
}
|
||||
},
|
||||
[fetchFullUser, history, admin, i18n],
|
||||
|
||||
@@ -9,7 +9,7 @@ import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
|
||||
import { requests } from '@payloadcms/ui/utilities/api'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { Fragment, useCallback, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ export const withPayload = (nextConfig = {}) => {
|
||||
'libsql',
|
||||
],
|
||||
},
|
||||
turbo: {
|
||||
...(nextConfig?.experimental?.turbo || {}),
|
||||
resolveAlias: {
|
||||
...(nextConfig?.experimental?.turbo?.resolveAlias || {}),
|
||||
'payload-mock-package': 'payload-mock-package',
|
||||
},
|
||||
},
|
||||
},
|
||||
headers: async () => {
|
||||
const headersFromConfig = 'headers' in nextConfig ? await nextConfig.headers() : []
|
||||
|
||||
@@ -4,8 +4,6 @@ import { register } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
import { bin } from './dist/bin/index.js'
|
||||
|
||||
// Allow disabling SWC for debugging
|
||||
if (process.env.DISABLE_SWC !== 'true') {
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
@@ -15,4 +13,9 @@ if (process.env.DISABLE_SWC !== 'true') {
|
||||
register('./dist/bin/loader/index.js', url)
|
||||
}
|
||||
|
||||
bin()
|
||||
const start = async () => {
|
||||
const { bin } = await import('./dist/bin/index.js')
|
||||
bin()
|
||||
}
|
||||
|
||||
void start()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { GenericLanguages, I18n, I18nClient } from '@payloadcms/translation
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
import type React from 'react'
|
||||
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
|
||||
import type { Field, FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { PayloadRequestWithData, RequestContext } from '../types/index.js'
|
||||
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
|
||||
|
||||
@@ -15,6 +17,173 @@ export type RichTextFieldProps<
|
||||
path?: string
|
||||
}
|
||||
|
||||
export type AfterReadRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = {
|
||||
currentDepth?: number
|
||||
|
||||
depth?: number
|
||||
|
||||
draft?: boolean
|
||||
|
||||
fallbackLocale?: string
|
||||
|
||||
fieldPromises?: Promise<void>[]
|
||||
|
||||
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
|
||||
findMany?: boolean
|
||||
|
||||
flattenLocales?: boolean
|
||||
|
||||
locale?: string
|
||||
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
operation?: 'create' | 'delete' | 'read' | 'update'
|
||||
|
||||
overrideAccess?: boolean
|
||||
|
||||
populationPromises?: Promise<void>[]
|
||||
showHiddenFields?: boolean
|
||||
triggerAccessControl?: boolean
|
||||
triggerHooks?: boolean
|
||||
}
|
||||
|
||||
export type AfterChangeRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = {
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
operation: 'create' | 'update'
|
||||
/** The document before changes were applied. */
|
||||
previousDoc?: TData
|
||||
/** The sibling data of the document before changes being applied. */
|
||||
previousSiblingDoc?: TData
|
||||
/** The previous value of the field, before changes */
|
||||
previousValue?: TValue
|
||||
}
|
||||
export type BeforeValidateRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = {
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess?: boolean
|
||||
/** The sibling data of the document before changes being applied. */
|
||||
previousSiblingDoc?: TData
|
||||
/** The previous value of the field, before changes */
|
||||
previousValue?: TValue
|
||||
}
|
||||
|
||||
export type BeforeChangeRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = {
|
||||
/**
|
||||
* The original data with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
|
||||
*/
|
||||
docWithLocales?: Record<string, unknown>
|
||||
|
||||
duplicate?: boolean
|
||||
|
||||
errors?: { field: string; message: string }[]
|
||||
/** Only available in `beforeChange` field hooks */
|
||||
mergeLocaleActions?: (() => Promise<void>)[]
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
operation?: 'create' | 'delete' | 'read' | 'update'
|
||||
/** The sibling data of the document before changes being applied. */
|
||||
previousSiblingDoc?: TData
|
||||
/** The previous value of the field, before changes */
|
||||
previousValue?: TValue
|
||||
/**
|
||||
* The original siblingData with locales (not modified by any hooks).
|
||||
*/
|
||||
siblingDocWithLocales?: Record<string, unknown>
|
||||
|
||||
skipValidation?: boolean
|
||||
}
|
||||
|
||||
export type BaseRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = {
|
||||
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
|
||||
collection: SanitizedCollectionConfig | null
|
||||
context: RequestContext
|
||||
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
|
||||
data?: Partial<TData>
|
||||
/** The field which the hook is running against. */
|
||||
field: FieldAffectingData
|
||||
/** The global which the field belongs to. If the field belongs to a collection, this will be null. */
|
||||
global: SanitizedGlobalConfig | null
|
||||
|
||||
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
||||
originalDoc?: TData
|
||||
/**
|
||||
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
|
||||
*/
|
||||
path: (number | string)[]
|
||||
|
||||
/** The Express request object. It is mocked for Local API operations. */
|
||||
req: PayloadRequestWithData
|
||||
/**
|
||||
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
|
||||
*/
|
||||
schemaPath: string[]
|
||||
/** The sibling data passed to a field that the hook is running against. */
|
||||
siblingData: Partial<TSiblingData>
|
||||
/** The value of the field. */
|
||||
value?: TValue
|
||||
}
|
||||
|
||||
export type AfterReadRichTextHook<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = (
|
||||
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||
AfterReadRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||
) => Promise<TValue> | TValue
|
||||
|
||||
export type AfterChangeRichTextHook<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = (
|
||||
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||
AfterChangeRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||
) => Promise<TValue> | TValue
|
||||
|
||||
export type BeforeChangeRichTextHook<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = (
|
||||
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||
BeforeChangeRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||
) => Promise<TValue> | TValue
|
||||
|
||||
export type BeforeValidateRichTextHook<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
TSiblingData = any,
|
||||
> = (
|
||||
args: BaseRichTextHookArgs<TData, TValue, TSiblingData> &
|
||||
BeforeValidateRichTextHookArgs<TData, TValue, TSiblingData>,
|
||||
) => Promise<TValue> | TValue
|
||||
|
||||
export type RichTextHooks = {
|
||||
afterChange?: AfterChangeRichTextHook[]
|
||||
afterRead?: AfterReadRichTextHook[]
|
||||
beforeChange?: BeforeChangeRichTextHook[]
|
||||
beforeValidate?: BeforeValidateRichTextHook[]
|
||||
}
|
||||
|
||||
type RichTextAdapterBase<
|
||||
Value extends object = object,
|
||||
AdapterProps = any,
|
||||
@@ -32,7 +201,28 @@ type RichTextAdapterBase<
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaPath: string
|
||||
}) => Map<string, Field[]>
|
||||
hooks?: FieldBase['hooks']
|
||||
/**
|
||||
* Like an afterRead hook, but runs only for the GraphQL resolver. For populating data, this should be used, as afterRead hooks do not have a depth in graphQL.
|
||||
*
|
||||
* To populate stuff / resolve field hooks, mutate the incoming populationPromises or fieldPromises array. They will then be awaited in the correct order within payload itself.
|
||||
* @param data
|
||||
*/
|
||||
graphQLPopulationPromises?: (data: {
|
||||
context: RequestContext
|
||||
currentDepth?: number
|
||||
depth: number
|
||||
draft: boolean
|
||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||
fieldPromises: Promise<void>[]
|
||||
findMany: boolean
|
||||
flattenLocales: boolean
|
||||
overrideAccess?: boolean
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequestWithData
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => void
|
||||
hooks?: RichTextHooks
|
||||
i18n?: Partial<GenericLanguages>
|
||||
outputSchema?: ({
|
||||
collectionIDFieldTypes,
|
||||
@@ -50,27 +240,6 @@ type RichTextAdapterBase<
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>
|
||||
isRequired: boolean
|
||||
}) => JSONSchema4
|
||||
/**
|
||||
* Like an afterRead hook, but runs for both afterRead AND in the GraphQL resolver. For populating data, this should be used.
|
||||
*
|
||||
* To populate stuff / resolve field hooks, mutate the incoming populationPromises or fieldPromises array. They will then be awaited in the correct order within payload itself.
|
||||
* @param data
|
||||
*/
|
||||
populationPromises?: (data: {
|
||||
context: RequestContext
|
||||
currentDepth?: number
|
||||
depth: number
|
||||
draft: boolean
|
||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||
fieldPromises: Promise<void>[]
|
||||
findMany: boolean
|
||||
flattenLocales: boolean
|
||||
overrideAccess?: boolean
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequestWithData
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => void
|
||||
validate: Validate<
|
||||
Value,
|
||||
Value,
|
||||
|
||||
@@ -129,8 +129,7 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
email.sendEmail({
|
||||
await email.sendEmail({
|
||||
from: `"${email.defaultFromName}" <${email.defaultFromAddress}>`,
|
||||
html,
|
||||
subject,
|
||||
|
||||
@@ -64,8 +64,7 @@ export async function sendVerificationEmail(args: Args): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
email.sendEmail({
|
||||
await email.sendEmail({
|
||||
from: `"${email.defaultFromName}" <${email.defaultFromAddress}>`,
|
||||
html,
|
||||
subject,
|
||||
|
||||
@@ -6,11 +6,16 @@ import type { SanitizedConfig } from '../config/types.js'
|
||||
import { configToJSONSchema } from '../utilities/configToJSONSchema.js'
|
||||
import Logger from '../utilities/logger.js'
|
||||
|
||||
export async function generateTypes(config: SanitizedConfig): Promise<void> {
|
||||
export async function generateTypes(
|
||||
config: SanitizedConfig,
|
||||
options?: { log: boolean },
|
||||
): Promise<void> {
|
||||
const logger = Logger()
|
||||
const outputFile = process.env.PAYLOAD_TS_OUTPUT_PATH || config.typescript.outputFile
|
||||
|
||||
logger.info('Compiling TS types for Collections and Globals...')
|
||||
const shouldLog = options?.log ?? true
|
||||
|
||||
if (shouldLog) logger.info('Compiling TS types for Collections and Globals...')
|
||||
|
||||
const jsonSchema = configToJSONSchema(config, config.db.defaultIDType)
|
||||
|
||||
@@ -36,6 +41,18 @@ export async function generateTypes(config: SanitizedConfig): Promise<void> {
|
||||
compiled += `\n\n${declare}`
|
||||
}
|
||||
}
|
||||
|
||||
// Diff the compiled types against the existing types file
|
||||
try {
|
||||
const existingTypes = fs.readFileSync(outputFile, 'utf-8')
|
||||
|
||||
if (compiled === existingTypes) {
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
// swallow err
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputFile, compiled)
|
||||
logger.info(`Types written to ${outputFile}`)
|
||||
if (shouldLog) logger.info(`Types written to ${outputFile}`)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getBaseUploadFields } from '../../uploads/getBaseFields.js'
|
||||
import { formatLabels } from '../../utilities/formatLabels.js'
|
||||
import { isPlainObject } from '../../utilities/isPlainObject.js'
|
||||
import baseVersionFields from '../../versions/baseFields.js'
|
||||
import { versionDefaults } from '../../versions/defaults.js'
|
||||
import { authDefaults, defaults } from './defaults.js'
|
||||
|
||||
export const sanitizeCollection = async (
|
||||
@@ -84,15 +85,20 @@ export const sanitizeCollection = async (
|
||||
if (sanitized.versions.drafts === true) {
|
||||
sanitized.versions.drafts = {
|
||||
autosave: false,
|
||||
validate: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitized.versions.drafts.autosave === true) {
|
||||
sanitized.versions.drafts.autosave = {
|
||||
interval: 2000,
|
||||
interval: versionDefaults.autosaveInterval,
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitized.versions.drafts.validate === undefined) {
|
||||
sanitized.versions.drafts.validate = false
|
||||
}
|
||||
|
||||
sanitized.fields = mergeBaseFields(sanitized.fields, baseVersionFields)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ const collectionSchema = joi.object().keys({
|
||||
interval: joi.number(),
|
||||
}),
|
||||
),
|
||||
validate: joi.boolean(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
|
||||
@@ -165,14 +165,6 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
|
||||
Promise.resolve(),
|
||||
)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Write files to local storage
|
||||
// /////////////////////////////////////
|
||||
|
||||
// if (!collectionConfig.upload.disableLocalStorage) {
|
||||
// await uploadFiles(payload, filesToUpload, req.t)
|
||||
// }
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeChange - Collection
|
||||
// /////////////////////////////////////
|
||||
@@ -203,7 +195,10 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
|
||||
global: null,
|
||||
operation: 'create',
|
||||
req,
|
||||
skipValidation: shouldSaveDraft,
|
||||
skipValidation:
|
||||
shouldSaveDraft &&
|
||||
collectionConfig.versions.drafts &&
|
||||
!collectionConfig.versions.drafts.validate,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
@@ -268,8 +263,7 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (collectionConfig.auth && collectionConfig.auth.verify) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
sendVerificationEmail({
|
||||
await sendVerificationEmail({
|
||||
collection: { config: collectionConfig },
|
||||
config: payload.config,
|
||||
disableEmail: disableVerificationEmail,
|
||||
|
||||
@@ -205,7 +205,10 @@ export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['col
|
||||
global: null,
|
||||
operation,
|
||||
req,
|
||||
skipValidation: shouldSaveDraft,
|
||||
skipValidation:
|
||||
shouldSaveDraft &&
|
||||
collectionConfig.versions.drafts &&
|
||||
!collectionConfig.versions.drafts.validate,
|
||||
})
|
||||
|
||||
// set req.locale back to the original locale
|
||||
|
||||
@@ -270,7 +270,10 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['collec
|
||||
operation: 'update',
|
||||
req,
|
||||
skipValidation:
|
||||
Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
|
||||
shouldSaveDraft &&
|
||||
collectionConfig.versions.drafts &&
|
||||
!collectionConfig.versions.drafts.validate &&
|
||||
data._status !== 'published',
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -242,7 +242,11 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
|
||||
global: null,
|
||||
operation: 'update',
|
||||
req,
|
||||
skipValidation: Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
|
||||
skipValidation:
|
||||
shouldSaveDraft &&
|
||||
collectionConfig.versions.drafts &&
|
||||
!collectionConfig.versions.drafts.validate &&
|
||||
data._status !== 'published',
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -49,6 +49,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
||||
serverURL: '',
|
||||
telemetry: true,
|
||||
typescript: {
|
||||
autoGenerate: true,
|
||||
outputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/payload-types.ts`,
|
||||
},
|
||||
upload: {},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { findUpSync, pathExistsSync } from 'find-up'
|
||||
import fs from 'fs'
|
||||
import { getTsconfig } from 'get-tsconfig'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
@@ -12,37 +12,30 @@ const getTSConfigPaths = (): {
|
||||
outPath?: string
|
||||
rootPath?: string
|
||||
srcPath?: string
|
||||
tsConfigPath?: string
|
||||
} => {
|
||||
const tsConfigPath = findUpSync('tsconfig.json')
|
||||
|
||||
if (!tsConfigPath) {
|
||||
return {
|
||||
rootPath: process.cwd(),
|
||||
}
|
||||
}
|
||||
const tsConfigResult = getTsconfig()
|
||||
const tsConfig = tsConfigResult.config
|
||||
const tsConfigDir = path.dirname(tsConfigResult.path)
|
||||
|
||||
try {
|
||||
// Read the file as a string and remove trailing commas
|
||||
const rawTsConfig = fs
|
||||
.readFileSync(tsConfigPath, 'utf-8')
|
||||
.replace(/,\s*\]/g, ']')
|
||||
.replace(/,\s*\}/g, '}')
|
||||
|
||||
const tsConfig = JSON.parse(rawTsConfig)
|
||||
|
||||
const rootPath = process.cwd()
|
||||
const rootConfigDir = path.resolve(tsConfigDir, tsConfig.compilerOptions.baseUrl || '')
|
||||
const srcPath = tsConfig.compilerOptions?.rootDir || path.resolve(process.cwd(), 'src')
|
||||
const outPath = tsConfig.compilerOptions?.outDir || path.resolve(process.cwd(), 'dist')
|
||||
const tsConfigDir = path.dirname(tsConfigPath)
|
||||
let configPath = tsConfig.compilerOptions?.paths?.['@payload-config']?.[0]
|
||||
let configPath = path.resolve(
|
||||
rootConfigDir,
|
||||
tsConfig.compilerOptions?.paths?.['@payload-config']?.[0],
|
||||
)
|
||||
|
||||
if (configPath) {
|
||||
configPath = path.resolve(tsConfigDir, configPath)
|
||||
configPath = path.resolve(rootConfigDir, configPath)
|
||||
}
|
||||
return {
|
||||
configPath,
|
||||
outPath,
|
||||
rootPath,
|
||||
rootPath: rootConfigDir,
|
||||
srcPath,
|
||||
tsConfigPath: tsConfigResult.path,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing tsconfig.json: ${error}`) // Do not throw the error, as we can still continue with the other config path finding methods
|
||||
@@ -70,6 +63,11 @@ export const findConfig = (): string => {
|
||||
|
||||
const { configPath, outPath, rootPath, srcPath } = getTSConfigPaths()
|
||||
|
||||
// if configPath is absolute file, not folder, return it
|
||||
if (path.extname(configPath) === '.js' || path.extname(configPath) === '.ts') {
|
||||
return configPath
|
||||
}
|
||||
|
||||
const searchPaths =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? [configPath, outPath, srcPath, rootPath]
|
||||
|
||||
@@ -189,6 +189,7 @@ export default joi.object({
|
||||
sharp: joi.any(),
|
||||
telemetry: joi.boolean(),
|
||||
typescript: joi.object({
|
||||
autoGenerate: joi.boolean(),
|
||||
declare: joi.alternatives().try(joi.boolean(), joi.object({ ignoreTSError: joi.boolean() })),
|
||||
outputFile: joi.string(),
|
||||
}),
|
||||
|
||||
@@ -719,6 +719,12 @@ export type Config = {
|
||||
telemetry?: boolean
|
||||
/** Control how typescript interfaces are generated from your collections. */
|
||||
typescript?: {
|
||||
/**
|
||||
* Automatically generate types during development
|
||||
* @default true
|
||||
*/
|
||||
autoGenerate?: boolean
|
||||
|
||||
/** Disable declare block in generated types file */
|
||||
declare?:
|
||||
| {
|
||||
@@ -732,6 +738,7 @@ export type Config = {
|
||||
ignoreTSError?: boolean
|
||||
}
|
||||
| false
|
||||
|
||||
/** Filename to write the generated types to */
|
||||
outputFile?: string
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { buildVersionCollectionFields } from '../versions/buildCollectionFields.js'
|
||||
export { buildVersionGlobalFields } from '../versions/buildGlobalFields.js'
|
||||
export { versionDefaults } from '../versions/defaults.js'
|
||||
export { deleteCollectionVersions } from '../versions/deleteCollectionVersions.js'
|
||||
export { enforceMaxVersions } from '../versions/enforceMaxVersions.js'
|
||||
export { getLatestCollectionVersion } from '../versions/getLatestCollectionVersion.js'
|
||||
|
||||
@@ -172,25 +172,6 @@ export const sanitizeFields = async ({
|
||||
if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
|
||||
config.i18n.translations = deepMerge(config.i18n.translations, field.editor.i18n)
|
||||
}
|
||||
|
||||
// Add editor adapter hooks to field hooks
|
||||
if (!field.hooks) field.hooks = {}
|
||||
|
||||
const mergeHooks = (hookName: keyof typeof field.editor.hooks) => {
|
||||
if (typeof field.editor === 'function') return
|
||||
|
||||
if (field.editor?.hooks?.[hookName]?.length) {
|
||||
field.hooks[hookName] = field.hooks[hookName]
|
||||
? field.hooks[hookName].concat(field.editor.hooks[hookName])
|
||||
: [...field.editor.hooks[hookName]]
|
||||
}
|
||||
}
|
||||
|
||||
mergeHooks('afterRead')
|
||||
mergeHooks('afterChange')
|
||||
mergeHooks('beforeChange')
|
||||
mergeHooks('beforeValidate')
|
||||
mergeHooks('beforeDuplicate')
|
||||
}
|
||||
if (richTextSanitizationPromises) {
|
||||
richTextSanitizationPromises.push(sanitizeRichText)
|
||||
|
||||
@@ -499,8 +499,8 @@ export const richText = baseField.keys({
|
||||
CellComponent: componentSchema.optional(),
|
||||
FieldComponent: componentSchema.optional(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
graphQLPopulationPromises: joi.func().optional(),
|
||||
outputSchema: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
validate: joi.func().required(),
|
||||
})
|
||||
.unknown(),
|
||||
|
||||
@@ -41,16 +41,28 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
|
||||
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
||||
originalDoc?: TData
|
||||
overrideAccess?: boolean
|
||||
/**
|
||||
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
|
||||
*/
|
||||
path: (number | string)[]
|
||||
/** The document before changes were applied, only in `afterChange` hooks. */
|
||||
previousDoc?: TData
|
||||
/** The sibling data of the document before changes being applied, only in `beforeChange` and `afterChange` hook. */
|
||||
/** The sibling data of the document before changes being applied, only in `beforeChange`, `beforeValidate`, `beforeDuplicate` and `afterChange` field hooks. */
|
||||
previousSiblingDoc?: TData
|
||||
/** The previous value of the field, before changes, only in `beforeChange`, `afterChange` and `beforeValidate` hooks. */
|
||||
/** The previous value of the field, before changes, only in `beforeChange`, `afterChange`, `beforeDuplicate` and `beforeValidate` field hooks. */
|
||||
previousValue?: TValue
|
||||
/** The Express request object. It is mocked for Local API operations. */
|
||||
req: PayloadRequestWithData
|
||||
/**
|
||||
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
|
||||
*/
|
||||
schemaPath: string[]
|
||||
/** The sibling data passed to a field that the hook is running against. */
|
||||
siblingData: Partial<TSiblingData>
|
||||
/**
|
||||
* The original siblingData with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
|
||||
*/
|
||||
siblingDocWithLocales?: Record<string, unknown>
|
||||
/** The value of the field. */
|
||||
value?: TValue
|
||||
}
|
||||
|
||||
39
packages/payload/src/fields/getFieldPaths.ts
Normal file
39
packages/payload/src/fields/getFieldPaths.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Field, TabAsField } from './config/types.js'
|
||||
|
||||
import { tabHasName } from './config/types.js'
|
||||
|
||||
export function getFieldPaths({
|
||||
field,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
}: {
|
||||
field: Field | TabAsField
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
}): {
|
||||
path: (number | string)[]
|
||||
schemaPath: string[]
|
||||
} {
|
||||
if (field.type === 'tabs' || field.type === 'row' || field.type === 'collapsible') {
|
||||
return {
|
||||
path: parentPath,
|
||||
schemaPath: parentSchemaPath,
|
||||
}
|
||||
} else if (field.type === 'tab') {
|
||||
if (tabHasName(field)) {
|
||||
return {
|
||||
path: [...parentPath, field.name],
|
||||
schemaPath: [...parentSchemaPath, field.name],
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
path: parentPath,
|
||||
schemaPath: parentSchemaPath,
|
||||
}
|
||||
}
|
||||
}
|
||||
const path = parentPath?.length ? [...parentPath, field.name] : [field.name]
|
||||
const schemaPath = parentSchemaPath?.length ? [...parentSchemaPath, field.name] : [field.name]
|
||||
|
||||
return { path, schemaPath }
|
||||
}
|
||||
@@ -8,7 +8,13 @@ import { traverseFields } from './traverseFields.js'
|
||||
type Args<T> = {
|
||||
collection: SanitizedCollectionConfig | null
|
||||
context: RequestContext
|
||||
/**
|
||||
* The data before hooks
|
||||
*/
|
||||
data: Record<string, unknown> | T
|
||||
/**
|
||||
* The data after hooks
|
||||
*/
|
||||
doc: Record<string, unknown> | T
|
||||
global: SanitizedGlobalConfig | null
|
||||
operation: 'create' | 'update'
|
||||
@@ -24,7 +30,6 @@ export const afterChange = async <T extends Record<string, unknown>>({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
doc: incomingDoc,
|
||||
global,
|
||||
operation,
|
||||
@@ -41,9 +46,11 @@ export const afterChange = async <T extends Record<string, unknown>>({
|
||||
fields: collection?.fields || global?.fields,
|
||||
global,
|
||||
operation,
|
||||
path: [],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc,
|
||||
req,
|
||||
schemaPath: [],
|
||||
siblingData: data,
|
||||
siblingDoc: doc,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
@@ -15,6 +18,14 @@ type Args = {
|
||||
field: Field | TabAsField
|
||||
global: SanitizedGlobalConfig | null
|
||||
operation: 'create' | 'update'
|
||||
/**
|
||||
* The parent's path
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
previousDoc: Record<string, unknown>
|
||||
previousSiblingDoc: Record<string, unknown>
|
||||
req: PayloadRequestWithData
|
||||
@@ -33,12 +44,20 @@ export const promise = async ({
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): Promise<void> => {
|
||||
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
if (field.hooks?.afterChange) {
|
||||
@@ -53,12 +72,14 @@ export const promise = async ({
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
@@ -79,9 +100,11 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: (siblingData?.[field.name] as Record<string, unknown>) || {},
|
||||
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
||||
})
|
||||
@@ -104,9 +127,11 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: { ...row } || {},
|
||||
}),
|
||||
@@ -135,10 +160,12 @@ export const promise = async ({
|
||||
fields: block.fields,
|
||||
global,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc:
|
||||
previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: { ...row } || {},
|
||||
}),
|
||||
@@ -161,9 +188,11 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
@@ -190,9 +219,11 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: tabPreviousSiblingDoc,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
@@ -209,15 +240,57 @@ export const promise = async ({
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.afterChange?.length) {
|
||||
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ type Args = {
|
||||
fields: (Field | TabAsField)[]
|
||||
global: SanitizedGlobalConfig | null
|
||||
operation: 'create' | 'update'
|
||||
path: (number | string)[]
|
||||
previousDoc: Record<string, unknown>
|
||||
previousSiblingDoc: Record<string, unknown>
|
||||
req: PayloadRequestWithData
|
||||
schemaPath: string[]
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
}
|
||||
@@ -28,9 +30,11 @@ export const traverseFields = async ({
|
||||
fields,
|
||||
global,
|
||||
operation,
|
||||
path,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): Promise<void> => {
|
||||
@@ -46,6 +50,8 @@ export const traverseFields = async ({
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
req,
|
||||
|
||||
@@ -25,7 +25,7 @@ type Args = {
|
||||
/**
|
||||
* This function is responsible for the following actions, in order:
|
||||
* - Remove hidden fields from response
|
||||
* - Flatten locales into requested locale
|
||||
* - Flatten locales into requested locale. If the input doc contains all locales, the output doc after this function will only contain the requested locale.
|
||||
* - Sanitize outgoing data (point field, etc.)
|
||||
* - Execute field hooks
|
||||
* - Execute read access control
|
||||
@@ -77,8 +77,10 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [],
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: [],
|
||||
showHiddenFields,
|
||||
siblingDoc: doc,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { RichTextAdapter } from '../../../admin/types.js'
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||
@@ -8,6 +8,7 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import getValueWithDefault from '../../getDefaultValue.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
@@ -29,6 +30,14 @@ type Args = {
|
||||
global: SanitizedGlobalConfig | null
|
||||
locale: null | string
|
||||
overrideAccess: boolean
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequestWithData
|
||||
showHiddenFields: boolean
|
||||
@@ -60,6 +69,8 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -67,6 +78,12 @@ export const promise = async ({
|
||||
triggerAccessControl = true,
|
||||
triggerHooks = true,
|
||||
}: Args): Promise<void> => {
|
||||
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
if (
|
||||
fieldAffectsData(field) &&
|
||||
field.hidden &&
|
||||
@@ -151,29 +168,7 @@ export const promise = async ({
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
// This is run here AND in the GraphQL Resolver
|
||||
if (editor?.populationPromises) {
|
||||
const populateDepth =
|
||||
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
|
||||
|
||||
editor.populationPromises({
|
||||
context,
|
||||
currentDepth,
|
||||
depth: populateDepth,
|
||||
draft,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
})
|
||||
}
|
||||
|
||||
// Rich Text fields should use afterRead hooks to do population. The previous editor.populationPromises have been renamed to editor.graphQLPopulationPromises
|
||||
break
|
||||
}
|
||||
|
||||
@@ -212,10 +207,14 @@ export const promise = async ({
|
||||
context,
|
||||
data: doc,
|
||||
field,
|
||||
findMany,
|
||||
global,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
value,
|
||||
})
|
||||
@@ -238,7 +237,9 @@ export const promise = async ({
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
@@ -322,8 +323,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: groupDoc,
|
||||
triggerAccessControl,
|
||||
@@ -337,7 +340,7 @@ export const promise = async ({
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row) => {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
@@ -353,8 +356,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: row || {},
|
||||
triggerAccessControl,
|
||||
@@ -364,7 +369,7 @@ export const promise = async ({
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row) => {
|
||||
localeRows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
@@ -380,8 +385,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: row || {},
|
||||
triggerAccessControl,
|
||||
@@ -400,7 +407,7 @@ export const promise = async ({
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row) => {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
||||
|
||||
if (block) {
|
||||
@@ -419,8 +426,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: row || {},
|
||||
triggerAccessControl,
|
||||
@@ -431,7 +440,7 @@ export const promise = async ({
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row) => {
|
||||
localeRows.forEach((row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
|
||||
|
||||
if (block) {
|
||||
@@ -450,8 +459,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: row || {},
|
||||
triggerAccessControl,
|
||||
@@ -485,8 +496,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl,
|
||||
@@ -518,8 +531,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: tabDoc,
|
||||
triggerAccessControl,
|
||||
@@ -545,8 +560,10 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl,
|
||||
@@ -555,6 +572,101 @@ export const promise = async ({
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.afterRead?.length) {
|
||||
await editor.hooks.afterRead.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const shouldRunHookOnAllLocales =
|
||||
field.localized &&
|
||||
(locale === 'all' || !flattenLocales) &&
|
||||
typeof siblingDoc[field.name] === 'object'
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
value,
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name][locale] = hookedValue
|
||||
}
|
||||
})(),
|
||||
)
|
||||
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ type Args = {
|
||||
global: SanitizedGlobalConfig | null
|
||||
locale: null | string
|
||||
overrideAccess: boolean
|
||||
path: (number | string)[]
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequestWithData
|
||||
schemaPath: string[]
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
triggerAccessControl?: boolean
|
||||
@@ -46,8 +48,10 @@ export const traverseFields = ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl = true,
|
||||
@@ -70,6 +74,8 @@ export const traverseFields = ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
|
||||
@@ -27,7 +27,7 @@ type Args<T> = {
|
||||
* - Validate data
|
||||
* - Transform data for storage
|
||||
* - beforeDuplicate hooks (if duplicate)
|
||||
* - Unflatten locales
|
||||
* - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales.
|
||||
*/
|
||||
export const beforeChange = async <T extends Record<string, unknown>>({
|
||||
id,
|
||||
@@ -59,8 +59,9 @@ export const beforeChange = async <T extends Record<string, unknown>>({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: '',
|
||||
path: [],
|
||||
req,
|
||||
schemaPath: [],
|
||||
siblingData: data,
|
||||
siblingDoc: doc,
|
||||
siblingDocWithLocales: docWithLocales,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import merge from 'deepmerge'
|
||||
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { Operation, PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||
import type { Field, FieldHookArgs, TabAsField, ValidateOptions } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { beforeDuplicate } from './beforeDuplicate.js'
|
||||
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
@@ -23,7 +26,14 @@ type Args = {
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void>)[]
|
||||
operation: Operation
|
||||
path: string
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequestWithData
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
@@ -52,7 +62,8 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
@@ -67,6 +78,12 @@ export const promise = async ({
|
||||
const defaultLocale = localization ? localization?.defaultLocale : 'en'
|
||||
const operationLocale = req.locale || defaultLocale
|
||||
|
||||
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// skip validation if the field is localized and the incoming data is null
|
||||
if (field.localized && operationLocale !== defaultLocale) {
|
||||
@@ -88,10 +105,13 @@ export const promise = async ({
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
@@ -127,7 +147,7 @@ export const promise = async ({
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
errors.push({
|
||||
field: `${path}${field.name}`,
|
||||
field: fieldPath.join('.'),
|
||||
message: validationResult,
|
||||
})
|
||||
}
|
||||
@@ -139,8 +159,13 @@ export const promise = async ({
|
||||
data,
|
||||
field,
|
||||
global: undefined,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
value: siblingData[field.name],
|
||||
}
|
||||
|
||||
@@ -225,8 +250,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: `${path}${field.name}.`,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData[field.name] as Record<string, unknown>,
|
||||
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
||||
siblingDocWithLocales: siblingDocWithLocales[field.name] as Record<string, unknown>,
|
||||
@@ -256,8 +282,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: `${path}${field.name}.${i}.`,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row,
|
||||
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
||||
siblingDocWithLocales: getExistingRowDoc(row, siblingDocWithLocales[field.name]),
|
||||
@@ -299,8 +326,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: `${path}${field.name}.${i}.`,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
siblingDocWithLocales: rowSiblingDocWithLocales,
|
||||
@@ -331,8 +359,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
@@ -343,13 +372,11 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabPath = path
|
||||
let tabSiblingData = siblingData
|
||||
let tabSiblingDoc = siblingDoc
|
||||
let tabSiblingDocWithLocales = siblingDocWithLocales
|
||||
|
||||
if (tabHasName(field)) {
|
||||
tabPath = `${path}${field.name}.`
|
||||
if (typeof siblingData[field.name] !== 'object') siblingData[field.name] = {}
|
||||
if (typeof siblingDoc[field.name] !== 'object') siblingDoc[field.name] = {}
|
||||
if (typeof siblingDocWithLocales[field.name] !== 'object')
|
||||
@@ -373,8 +400,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: tabPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
siblingDocWithLocales: tabSiblingDocWithLocales,
|
||||
@@ -398,8 +426,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
@@ -409,6 +438,52 @@ export const promise = async ({
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.beforeChange?.length) {
|
||||
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
docWithLocales,
|
||||
duplicate,
|
||||
errors,
|
||||
field,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ type Args = {
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void>)[]
|
||||
operation: Operation
|
||||
path: string
|
||||
path: (number | string)[]
|
||||
req: PayloadRequestWithData
|
||||
schemaPath: string[]
|
||||
siblingData: Record<string, unknown>
|
||||
/**
|
||||
* The original siblingData (not modified by any hooks)
|
||||
@@ -44,7 +45,7 @@ type Args = {
|
||||
* - Execute field hooks
|
||||
* - Validate data
|
||||
* - Transform data for storage
|
||||
* - Unflatten locales
|
||||
* - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales.
|
||||
*/
|
||||
export const traverseFields = async ({
|
||||
id,
|
||||
@@ -61,6 +62,7 @@ export const traverseFields = async ({
|
||||
operation,
|
||||
path,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
@@ -83,7 +85,8 @@ export const traverseFields = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
|
||||
@@ -49,7 +49,9 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: [],
|
||||
req,
|
||||
schemaPath: [],
|
||||
siblingData: data,
|
||||
siblingDoc: doc,
|
||||
})
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { PayloadRequestWithData, RequestContext } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
|
||||
import getValueWithDefault from '../../getDefaultValue.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js'
|
||||
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
@@ -23,6 +26,8 @@ type Args<T> = {
|
||||
id?: number | string
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequestWithData
|
||||
siblingData: Record<string, unknown>
|
||||
/**
|
||||
@@ -48,10 +53,18 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'id') {
|
||||
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {
|
||||
@@ -229,8 +242,11 @@ export const promise = async <T>({
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingData[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
@@ -288,7 +304,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: groupData,
|
||||
siblingDoc: groupDoc,
|
||||
})
|
||||
@@ -301,7 +319,7 @@ export const promise = async <T>({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row) => {
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
@@ -313,7 +331,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row,
|
||||
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
||||
}),
|
||||
@@ -329,7 +349,7 @@ export const promise = async <T>({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row) => {
|
||||
rows.forEach((row, i) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row, siblingDoc[field.name])
|
||||
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
@@ -348,7 +368,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
}),
|
||||
@@ -373,7 +395,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
@@ -405,7 +429,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
@@ -424,7 +450,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
@@ -432,6 +460,46 @@ export const promise = async <T>({
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.beforeValidate?.length) {
|
||||
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingData[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ type Args<T> = {
|
||||
id?: number | string
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
path: (number | string)[]
|
||||
req: PayloadRequestWithData
|
||||
schemaPath: string[]
|
||||
siblingData: Record<string, unknown>
|
||||
/**
|
||||
* The original siblingData (not modified by any hooks)
|
||||
@@ -36,7 +38,9 @@ export const traverseFields = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
@@ -53,6 +57,8 @@ export const traverseFields = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import mergeBaseFields from '../../fields/mergeBaseFields.js'
|
||||
import { toWords } from '../../utilities/formatLabels.js'
|
||||
import baseVersionFields from '../../versions/baseFields.js'
|
||||
import { versionDefaults } from '../../versions/defaults.js'
|
||||
|
||||
export const sanitizeGlobals = async (
|
||||
config: Config,
|
||||
@@ -47,15 +48,20 @@ export const sanitizeGlobals = async (
|
||||
if (global.versions.drafts === true) {
|
||||
global.versions.drafts = {
|
||||
autosave: false,
|
||||
validate: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (global.versions.drafts.autosave === true) {
|
||||
global.versions.drafts.autosave = {
|
||||
interval: 2000,
|
||||
interval: versionDefaults.autosaveInterval,
|
||||
}
|
||||
}
|
||||
|
||||
if (global.versions.drafts.validate === undefined) {
|
||||
global.versions.drafts.validate = false
|
||||
}
|
||||
|
||||
global.fields = mergeBaseFields(global.fields, baseVersionFields)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ const globalSchema = joi
|
||||
interval: joi.number(),
|
||||
}),
|
||||
),
|
||||
validate: joi.boolean(),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
|
||||
@@ -167,7 +167,8 @@ export const updateOperation = async <TSlug extends keyof GeneratedTypes['global
|
||||
global: globalConfig,
|
||||
operation: 'update',
|
||||
req,
|
||||
skipValidation: shouldSaveDraft,
|
||||
skipValidation:
|
||||
shouldSaveDraft && globalConfig.versions.drafts && !globalConfig.versions.drafts.validate,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql'
|
||||
import type { OperationArgs, Request as graphQLRequest } from 'graphql-http'
|
||||
import type pino from 'pino'
|
||||
|
||||
import { spawn } from 'child_process'
|
||||
import crypto from 'crypto'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import type { AuthArgs } from './auth/operations/auth.js'
|
||||
import type { Result as ForgotPasswordResult } from './auth/operations/forgotPassword.js'
|
||||
@@ -56,6 +59,9 @@ import flattenFields from './utilities/flattenTopLevelFields.js'
|
||||
import Logger from './utilities/logger.js'
|
||||
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
/**
|
||||
* @description Payload
|
||||
*/
|
||||
@@ -301,6 +307,31 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
[slug: string]: any // TODO: Type this
|
||||
} = {}
|
||||
|
||||
async bin({
|
||||
args,
|
||||
cwd,
|
||||
log,
|
||||
}: {
|
||||
args: string[]
|
||||
cwd?: string
|
||||
log?: boolean
|
||||
}): Promise<{ code: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawned = spawn('node', [path.resolve(dirname, '../bin.js'), ...args], {
|
||||
cwd,
|
||||
stdio: log || log === undefined ? 'inherit' : 'ignore',
|
||||
})
|
||||
|
||||
spawned.on('exit', (code) => {
|
||||
resolve({ code })
|
||||
})
|
||||
|
||||
spawned.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete one or more documents
|
||||
* @param options
|
||||
@@ -363,6 +394,16 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
}
|
||||
})
|
||||
|
||||
// Generate types on startup
|
||||
if (process.env.NODE_ENV !== 'production' && this.config.typescript.autoGenerate !== false) {
|
||||
// We cannot run it directly here, as generate-types imports json-schema-to-typescript, which breaks on turbopack.
|
||||
// see: https://github.com/vercel/next.js/issues/66723
|
||||
void this.bin({
|
||||
args: ['generate:types'],
|
||||
log: false,
|
||||
})
|
||||
}
|
||||
|
||||
this.db = this.config.db.init({ payload: this })
|
||||
this.db.payload = this
|
||||
|
||||
|
||||
@@ -133,21 +133,23 @@ export const generateFileData = async <T>({
|
||||
|
||||
if (fileIsAnimated) sharpOptions.animated = true
|
||||
|
||||
if (fileHasAdjustments && sharp) {
|
||||
if (sharp && (fileIsAnimated || fileHasAdjustments)) {
|
||||
if (file.tempFilePath) {
|
||||
sharpFile = sharp(file.tempFilePath, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||
} else {
|
||||
sharpFile = sharp(file.data, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||
}
|
||||
|
||||
if (resizeOptions) {
|
||||
sharpFile = sharpFile.resize(resizeOptions)
|
||||
}
|
||||
if (formatOptions) {
|
||||
sharpFile = sharpFile.toFormat(formatOptions.format, formatOptions.options)
|
||||
}
|
||||
if (trimOptions) {
|
||||
sharpFile = sharpFile.trim(trimOptions)
|
||||
if (fileHasAdjustments) {
|
||||
if (resizeOptions) {
|
||||
sharpFile = sharpFile.resize(resizeOptions)
|
||||
}
|
||||
if (formatOptions) {
|
||||
sharpFile = sharpFile.toFormat(formatOptions.format, formatOptions.options)
|
||||
}
|
||||
if (trimOptions) {
|
||||
sharpFile = sharpFile.trim(trimOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +203,6 @@ export const generateFileData = async <T>({
|
||||
let fileForResize = file
|
||||
|
||||
if (cropData && sharp) {
|
||||
const metadata = await sharpFile.metadata()
|
||||
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file, sharp })
|
||||
|
||||
filesToSave.push({
|
||||
@@ -215,7 +216,11 @@ export const generateFileData = async <T>({
|
||||
size: info.size,
|
||||
}
|
||||
fileData.width = info.width
|
||||
fileData.height = fileIsAnimated ? info.height / metadata.pages : info.height
|
||||
fileData.height = info.height
|
||||
if (fileIsAnimated) {
|
||||
const metadata = await sharpFile.metadata()
|
||||
fileData.height = metadata.pages ? info.height / metadata.pages : info.height
|
||||
}
|
||||
fileData.filesize = info.size
|
||||
|
||||
if (file.tempFilePath) {
|
||||
|
||||
@@ -364,7 +364,7 @@ export async function resizeAndTransformImageSizes({
|
||||
name: imageResizeConfig.name,
|
||||
filename: imageNameWithDimensions,
|
||||
filesize: size,
|
||||
height: fileIsAnimated ? height / metadata.pages : height,
|
||||
height: fileIsAnimated && metadata.pages ? height / metadata.pages : height,
|
||||
mimeType: mimeInfo?.mime || mimeType,
|
||||
sizesToSave: [{ buffer: bufferData, path: imagePath }],
|
||||
width,
|
||||
|
||||
3
packages/payload/src/versions/defaults.ts
Normal file
3
packages/payload/src/versions/defaults.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const versionDefaults = {
|
||||
autosaveInterval: 2000,
|
||||
}
|
||||
@@ -4,10 +4,12 @@ export type Autosave = {
|
||||
|
||||
export type IncomingDrafts = {
|
||||
autosave?: Autosave | boolean
|
||||
validate?: boolean
|
||||
}
|
||||
|
||||
export type SanitizedDrafts = {
|
||||
autosave: Autosave | false
|
||||
validate: boolean
|
||||
}
|
||||
|
||||
export type IncomingCollectionVersions = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
/**
|
||||
* Simple object check.
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isObject(item: unknown): boolean {
|
||||
return item && typeof item === 'object' && !Array.isArray(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects.
|
||||
* @param target
|
||||
* @param ...sources
|
||||
*/
|
||||
export default function deepMerge<T, R>(target: T, source: R): T {
|
||||
const output = { ...target }
|
||||
if (isObject(target) && isObject(source)) {
|
||||
Object.keys(source).forEach((key) => {
|
||||
if (isObject(source[key])) {
|
||||
if (!(key in target)) {
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
} else {
|
||||
output[key] = deepMerge(target[key], source[key])
|
||||
}
|
||||
} else {
|
||||
Object.assign(output, { [key]: source[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
@@ -1,80 +1,85 @@
|
||||
import type { Config } from 'payload/config'
|
||||
import type { CollectionConfig, Field } from 'payload/types'
|
||||
|
||||
import type { RedirectsPluginConfig } from './types.js'
|
||||
|
||||
import deepMerge from './deepMerge.js'
|
||||
|
||||
export const redirectsPlugin =
|
||||
(pluginConfig: RedirectsPluginConfig) =>
|
||||
(incomingConfig: Config): Config => ({
|
||||
...incomingConfig,
|
||||
collections: [
|
||||
...(incomingConfig?.collections || []),
|
||||
deepMerge(
|
||||
{
|
||||
slug: 'redirects',
|
||||
access: {
|
||||
read: (): boolean => true,
|
||||
},
|
||||
admin: {
|
||||
defaultColumns: ['from', 'to.type', 'createdAt'],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'from',
|
||||
type: 'text',
|
||||
index: true,
|
||||
label: 'From URL',
|
||||
required: true,
|
||||
(incomingConfig: Config): Config => {
|
||||
const defaultFields: Field[] = [
|
||||
{
|
||||
name: 'from',
|
||||
type: 'text',
|
||||
index: true,
|
||||
label: 'From URL',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'to',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radio',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
},
|
||||
{
|
||||
name: 'to',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radio',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
},
|
||||
defaultValue: 'reference',
|
||||
label: 'To URL Type',
|
||||
options: [
|
||||
{
|
||||
label: 'Internal link',
|
||||
value: 'reference',
|
||||
},
|
||||
{
|
||||
label: 'Custom URL',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'reference',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
label: 'Document to redirect to',
|
||||
relationTo: pluginConfig?.collections || [],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
},
|
||||
label: 'Custom URL',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
label: false,
|
||||
defaultValue: 'reference',
|
||||
label: 'To URL Type',
|
||||
options: [
|
||||
{
|
||||
label: 'Internal link',
|
||||
value: 'reference',
|
||||
},
|
||||
{
|
||||
label: 'Custom URL',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'reference',
|
||||
type: 'relationship',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
pluginConfig?.overrides || {},
|
||||
),
|
||||
],
|
||||
})
|
||||
label: 'Document to redirect to',
|
||||
relationTo: pluginConfig?.collections || [],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
},
|
||||
label: 'Custom URL',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const redirectsCollection: CollectionConfig = {
|
||||
...(pluginConfig?.overrides || {}),
|
||||
slug: pluginConfig?.overrides?.slug || 'redirects',
|
||||
access: {
|
||||
read: () => true,
|
||||
...(pluginConfig?.overrides?.access || {}),
|
||||
},
|
||||
admin: {
|
||||
defaultColumns: ['from', 'to.type', 'createdAt'],
|
||||
...(pluginConfig?.overrides?.admin || {}),
|
||||
},
|
||||
fields:
|
||||
pluginConfig?.overrides?.fields && typeof pluginConfig?.overrides?.fields === 'function'
|
||||
? pluginConfig?.overrides.fields({ defaultFields })
|
||||
: defaultFields,
|
||||
}
|
||||
|
||||
return {
|
||||
...incomingConfig,
|
||||
collections: [...(incomingConfig?.collections || []), redirectsCollection],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { CollectionConfig, Field } from 'payload/types'
|
||||
|
||||
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
|
||||
|
||||
export type RedirectsPluginConfig = {
|
||||
collections?: string[]
|
||||
overrides?: Partial<CollectionConfig>
|
||||
overrides?: Partial<Omit<CollectionConfig, 'fields'>> & { fields: FieldsOverride }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-relationship-object-ids",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.0.0-beta.42",
|
||||
"version": "3.0.0-beta.46",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -41,8 +41,8 @@
|
||||
"translateNewKeys": "tsx scripts/translateNewKeys.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faceless-ui/modal": "2.1.0-rc.0",
|
||||
"@faceless-ui/scroll-info": "1.4.0-rc.0",
|
||||
"@faceless-ui/modal": "3.0.0-beta.0",
|
||||
"@faceless-ui/scroll-info": "2.0.0-beta.0",
|
||||
"@lexical/headless": "0.16.0",
|
||||
"@lexical/link": "0.16.0",
|
||||
"@lexical/list": "0.16.0",
|
||||
@@ -75,8 +75,8 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@faceless-ui/modal": "2.1.0-rc.0",
|
||||
"@faceless-ui/scroll-info": "1.4.0-rc.0",
|
||||
"@faceless-ui/modal": "3.0.0-beta.0",
|
||||
"@faceless-ui/scroll-info": "2.0.0-beta.0",
|
||||
"@lexical/headless": "0.16.0",
|
||||
"@lexical/link": "0.16.0",
|
||||
"@lexical/list": "0.16.0",
|
||||
|
||||
@@ -51,7 +51,6 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
formData,
|
||||
formSchema,
|
||||
nodeKey,
|
||||
path,
|
||||
reducedBlock: { labels },
|
||||
schemaPath,
|
||||
} = props
|
||||
@@ -111,17 +110,21 @@ export const BlockContent: React.FC<Props> = (props) => {
|
||||
// does not have, even if it's undefined.
|
||||
// Currently, this happens if a block has another sub-blocks field. Inside formData, that sub-blocks field has an undefined blockName property.
|
||||
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
|
||||
function removeUndefinedAndNullRecursively(obj: object) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (obj[key] && typeof obj[key] === 'object') {
|
||||
removeUndefinedAndNullRecursively(obj[key])
|
||||
} else if (obj[key] === undefined || obj[key] === null) {
|
||||
function removeUndefinedAndNullAndEmptyArraysRecursively(obj: object) {
|
||||
for (const key in obj) {
|
||||
const value = obj[key]
|
||||
if (Array.isArray(value) && !value?.length) {
|
||||
delete obj[key]
|
||||
} else if (value && typeof value === 'object') {
|
||||
removeUndefinedAndNullAndEmptyArraysRecursively(value)
|
||||
} else if (value === undefined || value === null) {
|
||||
delete obj[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
removeUndefinedAndNullRecursively(newFormData)
|
||||
removeUndefinedAndNullRecursively(formData)
|
||||
removeUndefinedAndNullAndEmptyArraysRecursively(newFormData)
|
||||
|
||||
removeUndefinedAndNullAndEmptyArraysRecursively(formData)
|
||||
|
||||
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
||||
// which would trigger the "Leave without saving" dialog unnecessarily
|
||||
|
||||
@@ -29,10 +29,8 @@ import type { BlocksFeatureClientProps } from '../feature.client.js'
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { BlockContent } from './BlockContent.js'
|
||||
import './index.scss'
|
||||
import { removeEmptyArrayValues } from './removeEmptyArrayValues.js'
|
||||
|
||||
type Props = {
|
||||
blockFieldWrapperName: string
|
||||
children?: React.ReactNode
|
||||
|
||||
formData: BlockFields
|
||||
@@ -44,7 +42,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const { blockFieldWrapperName, formData, nodeKey } = props
|
||||
const { formData, nodeKey } = props
|
||||
const config = useConfig()
|
||||
const submitted = useFormSubmitted()
|
||||
const { id } = useDocumentInfo()
|
||||
@@ -81,7 +79,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
if (state) {
|
||||
setInitialState({
|
||||
...removeEmptyArrayValues({ fields: state }),
|
||||
...state,
|
||||
blockName: {
|
||||
initialValue: '',
|
||||
passesCondition: true,
|
||||
@@ -175,6 +173,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
</Collapsible>
|
||||
)
|
||||
}, [
|
||||
classNames,
|
||||
fieldMap,
|
||||
parentLexicalRichTextField,
|
||||
nodeKey,
|
||||
@@ -182,7 +181,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
submitted,
|
||||
initialState,
|
||||
reducedBlock,
|
||||
blockFieldWrapperName,
|
||||
onChange,
|
||||
schemaFieldsPath,
|
||||
path,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { LexicalCommand, LexicalEditor } from 'lexical'
|
||||
|
||||
import * as facelessUIImport from '@faceless-ui/modal'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
|
||||
import { formatDrawerSlug } from '@payloadcms/ui/elements/Drawer'
|
||||
import { BlocksDrawer } from '@payloadcms/ui/fields/Blocks/BlocksDrawer'
|
||||
@@ -54,8 +54,6 @@ const insertBlock = ({
|
||||
}
|
||||
|
||||
export const BlocksDrawerComponent: React.FC = () => {
|
||||
const { useModal } = facelessUIImport
|
||||
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { editorConfig, uuid } = useEditorConfigContext()
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import type { BlocksFeatureClientProps } from './feature.client.js'
|
||||
|
||||
import { createNode } from '../typeUtilities.js'
|
||||
import { BlocksFeatureClientComponent } from './feature.client.js'
|
||||
import { blockPopulationPromiseHOC } from './graphQLPopulationPromise.js'
|
||||
import { i18n } from './i18n.js'
|
||||
import { BlockNode } from './nodes/BlocksNode.js'
|
||||
import { blockPopulationPromiseHOC } from './populationPromise.js'
|
||||
import { blockValidationHOC } from './validate.js'
|
||||
|
||||
export type BlocksFeatureProps = {
|
||||
@@ -114,71 +114,17 @@ export const BlocksFeature: FeatureProviderProviderServer<
|
||||
i18n,
|
||||
nodes: [
|
||||
createNode({
|
||||
/* // TODO: Implement these hooks once docWithLocales / originalSiblingDoc => node matching has been figured out
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
async ({ context, findMany, node, operation, overrideAccess, req }) => {
|
||||
const blockType = node.fields.blockType
|
||||
getSubFields: ({ node, req }) => {
|
||||
const blockType = node.fields.blockType
|
||||
|
||||
const block = deepCopyObject(
|
||||
props.blocks.find((block) => block.slug === blockType),
|
||||
)
|
||||
|
||||
|
||||
await beforeChangeTraverseFields({
|
||||
id: null,
|
||||
collection: null,
|
||||
context,
|
||||
data: node.fields,
|
||||
doc: node.fields,
|
||||
fields: sanitizedBlock.fields,
|
||||
global: null,
|
||||
mergeLocaleActions: [],
|
||||
operation:
|
||||
operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
overrideAccess,
|
||||
path: '',
|
||||
req,
|
||||
siblingData: node.fields,
|
||||
siblingDoc: node.fields,
|
||||
})
|
||||
|
||||
|
||||
return node
|
||||
},
|
||||
],
|
||||
beforeValidate: [
|
||||
async ({ context, findMany, node, operation, overrideAccess, req }) => {
|
||||
const blockType = node.fields.blockType
|
||||
|
||||
const block = deepCopyObject(
|
||||
props.blocks.find((block) => block.slug === blockType),
|
||||
)
|
||||
|
||||
|
||||
|
||||
await beforeValidateTraverseFields({
|
||||
id: null,
|
||||
collection: null,
|
||||
context,
|
||||
data: node.fields,
|
||||
doc: node.fields,
|
||||
fields: sanitizedBlock.fields,
|
||||
global: null,
|
||||
operation:
|
||||
operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
overrideAccess,
|
||||
req,
|
||||
siblingData: node.fields,
|
||||
siblingDoc: node.fields,
|
||||
})
|
||||
|
||||
return node
|
||||
},
|
||||
],
|
||||
},*/
|
||||
const block = props.blocks.find((block) => block.slug === blockType)
|
||||
return block?.fields
|
||||
},
|
||||
getSubFieldsData: ({ node }) => {
|
||||
return node?.fields
|
||||
},
|
||||
graphQLPopulationPromises: [blockPopulationPromiseHOC(props)],
|
||||
node: BlockNode,
|
||||
populationPromises: [blockPopulationPromiseHOC(props)],
|
||||
validations: [blockValidationHOC(props)],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PopulationPromise } from '../types.js'
|
||||
import type { BlocksFeatureProps } from './feature.server.js'
|
||||
import type { SerializedBlockNode } from './nodes/BlocksNode.js'
|
||||
|
||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
||||
import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
|
||||
|
||||
export const blockPopulationPromiseHOC = (
|
||||
props: BlocksFeatureProps,
|
||||
@@ -21,7 +21,6 @@ export const blockPopulationPromiseHOC = (
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) => {
|
||||
const blockFieldData = node.fields
|
||||
|
||||
@@ -31,22 +30,21 @@ export const blockPopulationPromiseHOC = (
|
||||
return
|
||||
}
|
||||
|
||||
recurseNestedFields({
|
||||
recursivelyPopulateFieldsForGraphQL({
|
||||
context,
|
||||
currentDepth,
|
||||
data: blockFieldData,
|
||||
depth,
|
||||
draft,
|
||||
editorPopulationPromises,
|
||||
fieldPromises,
|
||||
fields: block.fields,
|
||||
findMany,
|
||||
flattenLocales: false, // Disable localization handling which does not work properly yet. Once we fully support hooks, this can be enabled (pass through flattenLocales again)
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
// The afterReadPromise gets its data from looking for field.name inside the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field.
|
||||
draft,
|
||||
siblingDoc: blockFieldData,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { User } from 'payload/auth'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Field, RadioField, TextField } from 'payload/types'
|
||||
import type { FieldAffectingData, RadioField, TextField } from 'payload/types'
|
||||
|
||||
import { validateUrl } from '../../../lexical/utils/url.js'
|
||||
|
||||
@@ -9,7 +9,7 @@ export const getBaseFields = (
|
||||
enabledCollections: false | string[],
|
||||
disabledCollections: false | string[],
|
||||
maxDepth?: number,
|
||||
): Field[] => {
|
||||
): FieldAffectingData[] => {
|
||||
let enabledRelations: string[]
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ export const getBaseFields = (
|
||||
.map(({ slug }) => slug)
|
||||
}
|
||||
|
||||
const baseFields: Field[] = [
|
||||
const baseFields: FieldAffectingData[] = [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Config, SanitizedConfig } from 'payload/config'
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Field, FieldAffectingData } from 'payload/types'
|
||||
|
||||
import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
@@ -11,12 +11,12 @@ import type { ClientProps } from './feature.client.js'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
||||
import { createNode } from '../typeUtilities.js'
|
||||
import { LinkFeatureClientComponent } from './feature.client.js'
|
||||
import { linkPopulationPromiseHOC } from './graphQLPopulationPromise.js'
|
||||
import { i18n } from './i18n.js'
|
||||
import { LinkMarkdownTransformer } from './markdownTransformer.js'
|
||||
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
|
||||
import { LinkNode } from './nodes/LinkNode.js'
|
||||
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
|
||||
import { linkPopulationPromiseHOC } from './populationPromise.js'
|
||||
import { linkValidation } from './validate.js'
|
||||
|
||||
export type ExclusiveLinkCollectionsProps =
|
||||
@@ -46,7 +46,12 @@ export type LinkFeatureServerProps = ExclusiveLinkCollectionsProps & {
|
||||
* A function or array defining additional fields for the link feature. These will be
|
||||
* displayed in the link editor drawer.
|
||||
*/
|
||||
fields?: ((args: { config: SanitizedConfig; defaultFields: Field[] }) => Field[]) | Field[]
|
||||
fields?:
|
||||
| ((args: {
|
||||
config: SanitizedConfig
|
||||
defaultFields: FieldAffectingData[]
|
||||
}) => (Field | FieldAffectingData)[])
|
||||
| Field[]
|
||||
/**
|
||||
* Sets a maximum population depth for the internal doc default field of link, regardless of the remaining depth when the field is reached.
|
||||
* This behaves exactly like the maxDepth properties of relationship and upload fields.
|
||||
@@ -82,6 +87,13 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
||||
})
|
||||
props.fields = sanitizedFields
|
||||
|
||||
// the text field is not included in the node data.
|
||||
// Thus, for tasks like validation, we do not want to pass it a text field in the schema which will never have data.
|
||||
// Otherwise, it will cause a validation error (field is required).
|
||||
const sanitizedFieldsWithoutText = deepCopyObject(sanitizedFields).filter(
|
||||
(field) => field.name !== 'text',
|
||||
)
|
||||
|
||||
return {
|
||||
ClientComponent: LinkFeatureClientComponent,
|
||||
clientFeatureProps: {
|
||||
@@ -143,16 +155,9 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
||||
nodeTypes: [AutoLinkNode.getType()],
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
({ node }) => {
|
||||
return node
|
||||
},
|
||||
],
|
||||
},
|
||||
node: AutoLinkNode,
|
||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||
validations: [linkValidation(props)],
|
||||
// Since AutoLinkNodes are just internal links, they need no hooks or graphQL population promises
|
||||
validations: [linkValidation(props, sanitizedFieldsWithoutText)],
|
||||
}),
|
||||
createNode({
|
||||
converters: {
|
||||
@@ -181,9 +186,15 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
||||
nodeTypes: [LinkNode.getType()],
|
||||
},
|
||||
},
|
||||
getSubFields: ({ node, req }) => {
|
||||
return sanitizedFieldsWithoutText
|
||||
},
|
||||
getSubFieldsData: ({ node }) => {
|
||||
return node?.fields
|
||||
},
|
||||
graphQLPopulationPromises: [linkPopulationPromiseHOC(props)],
|
||||
node: LinkNode,
|
||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||
validations: [linkValidation(props)],
|
||||
validations: [linkValidation(props, sanitizedFieldsWithoutText)],
|
||||
}),
|
||||
],
|
||||
serverFeatureProps: props,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PopulationPromise } from '../types.js'
|
||||
import type { LinkFeatureServerProps } from './feature.server.js'
|
||||
import type { SerializedLinkNode } from './nodes/types.js'
|
||||
|
||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields.js'
|
||||
import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'
|
||||
|
||||
export const linkPopulationPromiseHOC = (
|
||||
props: LinkFeatureServerProps,
|
||||
@@ -30,7 +30,7 @@ export const linkPopulationPromiseHOC = (
|
||||
* Should populate all fields, including the doc field (for internal links), as it's treated like a normal field
|
||||
*/
|
||||
if (Array.isArray(props.fields)) {
|
||||
recurseNestedFields({
|
||||
recursivelyPopulateFieldsForGraphQL({
|
||||
context,
|
||||
currentDepth,
|
||||
data: node.fields,
|
||||
@@ -40,7 +40,7 @@ export const linkPopulationPromiseHOC = (
|
||||
fieldPromises,
|
||||
fields: props.fields,
|
||||
findMany,
|
||||
flattenLocales: false, // Disable localization handling which does not work properly yet. Once we fully support hooks, this can be enabled (pass through flattenLocales again)
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
@@ -11,7 +11,7 @@ import { LinkNode } from './LinkNode.js'
|
||||
|
||||
export class AutoLinkNode extends LinkNode {
|
||||
static clone(node: AutoLinkNode): AutoLinkNode {
|
||||
return new AutoLinkNode({ fields: node.__fields, key: node.__key })
|
||||
return new AutoLinkNode({ id: undefined, fields: node.__fields, key: node.__key })
|
||||
}
|
||||
|
||||
static getType(): string {
|
||||
@@ -61,7 +61,7 @@ export class AutoLinkNode extends LinkNode {
|
||||
}
|
||||
|
||||
export function $createAutoLinkNode({ fields }: { fields: LinkFields }): AutoLinkNode {
|
||||
return $applyNodeReplacement(new AutoLinkNode({ fields }))
|
||||
return $applyNodeReplacement(new AutoLinkNode({ id: undefined, fields }))
|
||||
}
|
||||
export function $isAutoLinkNode(node: LexicalNode | null | undefined): node is AutoLinkNode {
|
||||
return node instanceof AutoLinkNode
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from 'lexical'
|
||||
|
||||
import { addClassNamesToElement, isHTMLAnchorElement } from '@lexical/utils'
|
||||
import ObjectID from 'bson-objectid'
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
$createTextNode,
|
||||
@@ -29,8 +30,10 @@ const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'sms:', '
|
||||
/** @noInheritDoc */
|
||||
export class LinkNode extends ElementNode {
|
||||
__fields: LinkFields
|
||||
__id: string
|
||||
|
||||
constructor({
|
||||
id,
|
||||
fields = {
|
||||
doc: null,
|
||||
linkType: 'custom',
|
||||
@@ -40,14 +43,17 @@ export class LinkNode extends ElementNode {
|
||||
key,
|
||||
}: {
|
||||
fields: LinkFields
|
||||
id: string
|
||||
key?: NodeKey
|
||||
}) {
|
||||
super(key)
|
||||
this.__fields = fields
|
||||
this.__id = id
|
||||
}
|
||||
|
||||
static clone(node: LinkNode): LinkNode {
|
||||
return new LinkNode({
|
||||
id: node.__id,
|
||||
fields: node.__fields,
|
||||
key: node.__key,
|
||||
})
|
||||
@@ -76,7 +82,13 @@ export class LinkNode extends ElementNode {
|
||||
serializedNode.version = 2
|
||||
}
|
||||
|
||||
if (serializedNode.version === 2 && !serializedNode.id) {
|
||||
serializedNode.id = new ObjectID.default().toHexString()
|
||||
serializedNode.version = 3
|
||||
}
|
||||
|
||||
const node = $createLinkNode({
|
||||
id: serializedNode.id,
|
||||
fields: serializedNode.fields,
|
||||
})
|
||||
node.setFormat(serializedNode.format)
|
||||
@@ -115,12 +127,17 @@ export class LinkNode extends ElementNode {
|
||||
}
|
||||
|
||||
exportJSON(): SerializedLinkNode {
|
||||
return {
|
||||
const returnObject: SerializedLinkNode = {
|
||||
...super.exportJSON(),
|
||||
type: this.getType(),
|
||||
fields: this.getFields(),
|
||||
version: 2,
|
||||
version: 3,
|
||||
}
|
||||
const id = this.getID()
|
||||
if (id) {
|
||||
returnObject.id = id
|
||||
}
|
||||
return returnObject
|
||||
}
|
||||
|
||||
extractWithChild(
|
||||
@@ -146,6 +163,10 @@ export class LinkNode extends ElementNode {
|
||||
return this.getLatest().__fields
|
||||
}
|
||||
|
||||
getID(): string {
|
||||
return this.getLatest().__id
|
||||
}
|
||||
|
||||
insertNewAfter(selection: RangeSelection, restoreSelection = true): ElementNodeType | null {
|
||||
const element = this.getParentOrThrow().insertNewAfter(selection, restoreSelection)
|
||||
if ($isElementNode(element)) {
|
||||
@@ -216,6 +237,7 @@ function $convertAnchorElement(domNode: Node): DOMConversionOutput {
|
||||
const content = domNode.textContent
|
||||
if (content !== null && content !== '') {
|
||||
node = $createLinkNode({
|
||||
id: new ObjectID.default().toHexString(),
|
||||
fields: {
|
||||
doc: null,
|
||||
linkType: 'custom',
|
||||
@@ -228,8 +250,13 @@ function $convertAnchorElement(domNode: Node): DOMConversionOutput {
|
||||
return { node }
|
||||
}
|
||||
|
||||
export function $createLinkNode({ fields }: { fields: LinkFields }): LinkNode {
|
||||
return $applyNodeReplacement(new LinkNode({ fields }))
|
||||
export function $createLinkNode({ id, fields }: { fields: LinkFields; id?: string }): LinkNode {
|
||||
return $applyNodeReplacement(
|
||||
new LinkNode({
|
||||
id: id ?? new ObjectID.default().toHexString(),
|
||||
fields,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function $isLinkNode(node: LexicalNode | null | undefined): node is LinkNode {
|
||||
@@ -349,8 +376,6 @@ export function $toggleLink(payload: LinkPayload): void {
|
||||
})
|
||||
}
|
||||
}
|
||||
/** @deprecated renamed to {@link $toggleLink} by @lexical/eslint-plugin rules-of-lexical */
|
||||
export const toggleLink = $toggleLink
|
||||
|
||||
function $getLinkAncestor(node: LexicalNode): LinkNode | null {
|
||||
return $getAncestor(node, (ancestor) => $isLinkNode(ancestor)) as LinkNode
|
||||
|
||||
@@ -21,7 +21,8 @@ export type LinkFields = {
|
||||
export type SerializedLinkNode = Spread<
|
||||
{
|
||||
fields: LinkFields
|
||||
id?: string // optional if AutoLinkNode
|
||||
},
|
||||
SerializedElementNode
|
||||
>
|
||||
export type SerializedAutoLinkNode = SerializedLinkNode
|
||||
export type SerializedAutoLinkNode = Omit<SerializedLinkNode, 'id'>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { LexicalNode } from 'lexical'
|
||||
import type { FormState } from 'payload/types'
|
||||
import type { Data } from 'payload/types'
|
||||
|
||||
import * as facelessUIImport from '@faceless-ui/modal'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
|
||||
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
@@ -36,8 +36,6 @@ import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode.js'
|
||||
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands.js'
|
||||
|
||||
export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.ReactNode {
|
||||
const { useModal } = facelessUIImport
|
||||
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const editorRef = useRef<HTMLDivElement | null>(null)
|
||||
@@ -50,7 +48,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const [stateData, setStateData] = useState<{} | (LinkFields & { text: string })>({})
|
||||
const [stateData, setStateData] = useState<{} | (LinkFields & { id?: string; text: string })>({})
|
||||
|
||||
const { closeModal, isModalOpen, toggleModal } = useModal()
|
||||
const editDepth = useEditDepth()
|
||||
@@ -116,6 +114,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
|
||||
newTab: undefined,
|
||||
url: '',
|
||||
...focusLinkParent.getFields(),
|
||||
id: focusLinkParent.getID(),
|
||||
text: focusLinkParent.getTextContent(),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Field } from 'payload/types'
|
||||
import type { Field, FieldAffectingData } from 'payload/types'
|
||||
|
||||
import { getBaseFields } from '../../drawer/baseFields.js'
|
||||
|
||||
@@ -8,14 +8,14 @@ import { getBaseFields } from '../../drawer/baseFields.js'
|
||||
*/
|
||||
export function transformExtraFields(
|
||||
customFieldSchema:
|
||||
| ((args: { config: SanitizedConfig; defaultFields: Field[] }) => Field[])
|
||||
| ((args: { config: SanitizedConfig; defaultFields: FieldAffectingData[] }) => Field[])
|
||||
| Field[],
|
||||
config: SanitizedConfig,
|
||||
enabledCollections?: false | string[],
|
||||
disabledCollections?: false | string[],
|
||||
maxDepth?: number,
|
||||
): Field[] {
|
||||
const baseFields: Field[] = getBaseFields(
|
||||
const baseFields: FieldAffectingData[] = getBaseFields(
|
||||
config,
|
||||
enabledCollections,
|
||||
disabledCollections,
|
||||
@@ -29,7 +29,7 @@ export function transformExtraFields(
|
||||
} else if (Array.isArray(customFieldSchema)) {
|
||||
fields = customFieldSchema
|
||||
} else {
|
||||
fields = baseFields
|
||||
fields = baseFields as Field[]
|
||||
}
|
||||
|
||||
return fields
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { LinkFields } from '../../nodes/types.js'
|
||||
import type { LinkPayload } from '../floatingLinkEditor/types.js'
|
||||
|
||||
import { validateUrl } from '../../../../lexical/utils/url.js'
|
||||
import { LinkNode, TOGGLE_LINK_COMMAND, toggleLink } from '../../nodes/LinkNode.js'
|
||||
import { $toggleLink, LinkNode, TOGGLE_LINK_COMMAND } from '../../nodes/LinkNode.js'
|
||||
|
||||
export const LinkPlugin: PluginComponent<ClientProps> = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
@@ -29,7 +29,7 @@ export const LinkPlugin: PluginComponent<ClientProps> = () => {
|
||||
editor.registerCommand(
|
||||
TOGGLE_LINK_COMMAND,
|
||||
(payload: LinkPayload) => {
|
||||
toggleLink(payload)
|
||||
$toggleLink(payload)
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user