Compare commits
1 Commits
fix/list-f
...
ci/node-23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe8edc014 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
NODE_VERSION: 23.6.1
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -25,18 +25,6 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
- Adds a `tenant` field to each specified collection
|
||||
- Adds a tenant selector to the admin panel, allowing you to switch between tenants
|
||||
- Filters list view results by selected tenant
|
||||
- Filters relationship fields by selected tenant
|
||||
- Ability to create "global" like collections, 1 doc per tenant
|
||||
- Automatically assign a tenant to new documents
|
||||
|
||||
<Banner type="error">
|
||||
**Warning**
|
||||
|
||||
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
||||
strong access control on your tenants collection to prevent deletions by unauthorized users.
|
||||
|
||||
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
</Banner>
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -52,7 +40,7 @@ The plugin accepts an object with the following properties:
|
||||
|
||||
```ts
|
||||
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
/**
|
||||
/**
|
||||
* After a tenant is deleted, the plugin will attempt to clean up related documents
|
||||
* - removing documents with the tenant ID
|
||||
* - removing the tenant from users
|
||||
@@ -156,12 +144,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Useful for super-admin type users
|
||||
*/
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
user: ConfigTypes extends { user } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ To securely allow headless operation you will need to configure the allowed orig
|
||||
|
||||
## Limiting GraphQL Complexity
|
||||
|
||||
Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way to specify complexity limits. These limits are based on a complexity score calculated for each request.
|
||||
Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way specify complexity limits which are based on a complexity score that is calculated for each request.
|
||||
|
||||
Any GraphQL request that is calculated to be too expensive is rejected. On the Payload Config, in `graphQL` you can set the `maxComplexity` value as an integer. For reference, the default complexity value for each added field is 1, and all `relationship` and `upload` fields are assigned a value of 10.
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: unset;
|
||||
font-weight: unset;
|
||||
font-size: auto;
|
||||
font-weight: auto;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
"payload": "latest",
|
||||
"payload-app": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sharp": "0.32.6"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.15.2",
|
||||
|
||||
@@ -88,10 +88,6 @@ export const traverseFields = ({
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
if (row._uuid) {
|
||||
data._uuid = row._uuid
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
let columnName = ''
|
||||
let fieldName = ''
|
||||
|
||||
@@ -20,10 +20,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
db,
|
||||
fields,
|
||||
ignoreResult,
|
||||
// TODO:
|
||||
// When we support joins for write operations (create/update) - pass collectionSlug to the buildFindManyArgs
|
||||
// Make a new argument in upsertRow.ts and pass the slug from every operation.
|
||||
joinQuery: _joinQuery,
|
||||
joinQuery,
|
||||
operation,
|
||||
path = '',
|
||||
req,
|
||||
@@ -266,9 +263,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
}
|
||||
}
|
||||
|
||||
// When versions are enabled, this is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
|
||||
const arraysBlocksUUIDMap: Record<string, number | string> = {}
|
||||
|
||||
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
|
||||
insertedBlockRows[blockName] = await adapter.insert({
|
||||
@@ -279,12 +273,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
|
||||
insertedBlockRows[blockName].forEach((row, i) => {
|
||||
blockRows[i].row = row
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
(typeof row.id === 'string' || typeof row.id === 'number')
|
||||
) {
|
||||
arraysBlocksUUIDMap[row._uuid] = row.id
|
||||
}
|
||||
})
|
||||
|
||||
const blockLocaleIndexMap: number[] = []
|
||||
@@ -317,7 +305,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
arrays: blockRows.map(({ arrays }) => arrays),
|
||||
db,
|
||||
parentRows: insertedBlockRows[blockName],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -341,7 +328,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
arrays: [rowToInsert.arrays],
|
||||
db,
|
||||
parentRows: [insertedRow],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
|
||||
// //////////////////////////////////
|
||||
@@ -358,14 +344,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
})
|
||||
}
|
||||
|
||||
if (Object.keys(arraysBlocksUUIDMap).length > 0) {
|
||||
tableRows.forEach((row: any) => {
|
||||
if (row.parent in arraysBlocksUUIDMap) {
|
||||
row.parent = arraysBlocksUUIDMap[row.parent]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (tableRows.length) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
@@ -436,11 +414,13 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
// RETRIEVE NEWLY UPDATED ROW
|
||||
// //////////////////////////////////
|
||||
|
||||
joinQuery = operation === 'create' ? false : joinQuery
|
||||
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter,
|
||||
depth: 0,
|
||||
fields,
|
||||
joinQuery: false,
|
||||
joinQuery,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
@@ -458,7 +438,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
config: adapter.payload.config,
|
||||
data: doc,
|
||||
fields,
|
||||
joinQuery: false,
|
||||
joinQuery,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@@ -8,7 +8,6 @@ type Args = {
|
||||
}[]
|
||||
db: DrizzleAdapter['drizzle'] | DrizzleTransaction
|
||||
parentRows: Record<string, unknown>[]
|
||||
uuidMap?: Record<string, number | string>
|
||||
}
|
||||
|
||||
type RowsByTable = {
|
||||
@@ -21,13 +20,7 @@ type RowsByTable = {
|
||||
}
|
||||
}
|
||||
|
||||
export const insertArrays = async ({
|
||||
adapter,
|
||||
arrays,
|
||||
db,
|
||||
parentRows,
|
||||
uuidMap = {},
|
||||
}: Args): Promise<void> => {
|
||||
export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): Promise<void> => {
|
||||
// Maintain a map of flattened rows by table
|
||||
const rowsByTable: RowsByTable = {}
|
||||
|
||||
@@ -81,15 +74,6 @@ export const insertArrays = async ({
|
||||
tableName,
|
||||
values: row.rows,
|
||||
})
|
||||
|
||||
insertedRows.forEach((row) => {
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
(typeof row.id === 'string' || typeof row.id === 'number')
|
||||
) {
|
||||
uuidMap[row._uuid] = row.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Insert locale rows
|
||||
|
||||
@@ -32,12 +32,7 @@ export const RenderVersionFieldsToDiff = ({
|
||||
const LocaleComponents: React.ReactNode[] = []
|
||||
for (const [locale, baseField] of Object.entries(field.fieldByLocale)) {
|
||||
LocaleComponents.push(
|
||||
<div
|
||||
className={`${baseClass}__locale`}
|
||||
data-field-path={baseField.path}
|
||||
data-locale={locale}
|
||||
key={[locale, fieldIndex].join('-')}
|
||||
>
|
||||
<div className={`${baseClass}__locale`} key={[locale, fieldIndex].join('-')}>
|
||||
<div className={`${baseClass}__locale-value`}>{baseField.CustomComponent}</div>
|
||||
</div>,
|
||||
)
|
||||
|
||||
@@ -282,13 +282,15 @@ const buildVersionField = ({
|
||||
}
|
||||
} // At this point, we are dealing with a `row`, etc
|
||||
else if ('fields' in field) {
|
||||
if (field.type === 'array' && versionValue) {
|
||||
const arrayValue = Array.isArray(versionValue) ? versionValue : []
|
||||
if (field.type === 'array') {
|
||||
if (!Array.isArray(versionValue)) {
|
||||
throw new Error('Expected versionValue to be an array')
|
||||
}
|
||||
baseVersionField.rows = []
|
||||
|
||||
for (let i = 0; i < arrayValue.length; i++) {
|
||||
for (let i = 0; i < versionValue.length; i++) {
|
||||
const comparisonRow = comparisonValue?.[i] || {}
|
||||
const versionRow = arrayValue?.[i] || {}
|
||||
const versionRow = versionValue?.[i] || {}
|
||||
baseVersionField.rows[i] = buildVersionFields({
|
||||
clientSchemaMap,
|
||||
comparisonSiblingData: comparisonRow,
|
||||
@@ -327,11 +329,13 @@ const buildVersionField = ({
|
||||
} else if (field.type === 'blocks') {
|
||||
baseVersionField.rows = []
|
||||
|
||||
const blocksValue = Array.isArray(versionValue) ? versionValue : []
|
||||
if (!Array.isArray(versionValue)) {
|
||||
throw new Error('Expected versionValue to be an array')
|
||||
}
|
||||
|
||||
for (let i = 0; i < blocksValue.length; i++) {
|
||||
for (let i = 0; i < versionValue.length; i++) {
|
||||
const comparisonRow = comparisonValue?.[i] || {}
|
||||
const versionRow = blocksValue[i] || {}
|
||||
const versionRow = versionValue[i] || {}
|
||||
const versionBlock = field.blocks.find((block) => block.slug === versionRow.blockType)
|
||||
|
||||
let fields = []
|
||||
|
||||
@@ -128,17 +128,15 @@ export const multiTenantPlugin =
|
||||
if (collection.slug === tenantsCollectionSlug) {
|
||||
tenantCollection = collection
|
||||
|
||||
if (pluginConfig.useTenantsCollectionAccess !== false) {
|
||||
/**
|
||||
* Add access control constraint to tenants collection
|
||||
* - constrains access a users assigned tenants
|
||||
*/
|
||||
addCollectionAccess({
|
||||
collection,
|
||||
fieldName: 'id',
|
||||
userHasAccessToAllTenants,
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Add access control constraint to tenants collection
|
||||
* - constrains access a users assigned tenants
|
||||
*/
|
||||
addCollectionAccess({
|
||||
collection,
|
||||
fieldName: 'id',
|
||||
userHasAccessToAllTenants,
|
||||
})
|
||||
|
||||
if (pluginConfig.cleanupAfterTenantDelete !== false) {
|
||||
/**
|
||||
|
||||
@@ -121,10 +121,6 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
}
|
||||
|
||||
export type Tenant<IDType = number | string> = {
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@faceless-ui/modal": "3.0.0-beta.2",
|
||||
"@faceless-ui/scroll-info": "2.0.0",
|
||||
"@faceless-ui/scroll-info": "2.0.0-beta.0",
|
||||
"@lexical/headless": "0.21.0",
|
||||
"@lexical/html": "0.21.0",
|
||||
"@lexical/link": "0.21.0",
|
||||
|
||||
@@ -79,10 +79,6 @@ const RichTextComponent: React.FC<
|
||||
const disabled = readOnlyFromProps || formProcessing || formInitializing
|
||||
|
||||
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState<boolean>(false)
|
||||
const [rerenderProviderKey, setRerenderProviderKey] = useState<Date>()
|
||||
|
||||
const prevInitialValueRef = React.useRef<SerializedEditorState | undefined>(initialValue)
|
||||
const prevValueRef = React.useRef<SerializedEditorState | undefined>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const updateViewPortWidth = () => {
|
||||
@@ -117,24 +113,13 @@ const RichTextComponent: React.FC<
|
||||
|
||||
const handleChange = useCallback(
|
||||
(editorState: EditorState) => {
|
||||
const newState = editorState.toJSON()
|
||||
prevValueRef.current = newState
|
||||
setValue(newState)
|
||||
setValue(editorState.toJSON())
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
|
||||
const styles = useMemo(() => mergeFieldStyles(field), [field])
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(initialValue) !== JSON.stringify(prevInitialValueRef.current)) {
|
||||
prevInitialValueRef.current = initialValue
|
||||
if (JSON.stringify(prevValueRef.current) !== JSON.stringify(value)) {
|
||||
setRerenderProviderKey(new Date())
|
||||
}
|
||||
}
|
||||
}, [initialValue, value])
|
||||
|
||||
return (
|
||||
<div className={classes} key={pathWithEditDepth} style={styles}>
|
||||
<RenderCustomComponent
|
||||
@@ -150,7 +135,7 @@ const RichTextComponent: React.FC<
|
||||
editorConfig={editorConfig}
|
||||
fieldProps={props}
|
||||
isSmallWidthViewport={isSmallWidthViewport}
|
||||
key={JSON.stringify({ path, rerenderProviderKey })} // makes sure lexical is completely re-rendered when initialValue changes, bypassing the lexical-internal value memoization. That way, external changes to the form will update the editor. More infos in PR description (https://github.com/payloadcms/payload/pull/5010)
|
||||
key={JSON.stringify({ initialValue, path })} // makes sure lexical is completely re-rendered when initialValue changes, bypassing the lexical-internal value memoization. That way, external changes to the form will update the editor. More infos in PR description (https://github.com/payloadcms/payload/pull/5010)
|
||||
onChange={handleChange}
|
||||
readOnly={disabled}
|
||||
value={value}
|
||||
|
||||
@@ -119,10 +119,11 @@
|
||||
"@dnd-kit/core": "6.0.8",
|
||||
"@dnd-kit/sortable": "7.0.2",
|
||||
"@faceless-ui/modal": "3.0.0-beta.2",
|
||||
"@faceless-ui/scroll-info": "2.0.0",
|
||||
"@faceless-ui/window-info": "3.0.1",
|
||||
"@faceless-ui/scroll-info": "2.0.0-beta.0",
|
||||
"@faceless-ui/window-info": "3.0.0-beta.0",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
"bson-objectid": "2.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"dequal": "2.0.3",
|
||||
@@ -146,6 +147,7 @@
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/body-scroll-lock": "^3.1.0",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/uuid": "10.0.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import { useWindowInfo } from '@faceless-ui/window-info'
|
||||
import { clearAllBodyScrollLocks, disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
import { usePreferences } from '../../providers/Preferences/index.js'
|
||||
@@ -72,9 +73,9 @@ export const NavProvider: React.FC<{
|
||||
useEffect(() => {
|
||||
if (navRef.current) {
|
||||
if (navOpen && midBreak) {
|
||||
navRef.current.style.overscrollBehavior = 'contain'
|
||||
disableBodyScroll(navRef.current)
|
||||
} else {
|
||||
navRef.current.style.overscrollBehavior = 'auto'
|
||||
enableBodyScroll(navRef.current)
|
||||
}
|
||||
}
|
||||
}, [navOpen, midBreak])
|
||||
@@ -96,7 +97,7 @@ export const NavProvider: React.FC<{
|
||||
// when the component unmounts, clear all body scroll locks
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
navRef.current.style.overscrollBehavior = 'auto'
|
||||
clearAllBodyScrollLocks()
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
||||
disabled,
|
||||
field: { admin: { isSortable } = {}, hasMany, relationTo },
|
||||
onChange,
|
||||
operator,
|
||||
value,
|
||||
} = props
|
||||
|
||||
@@ -97,40 +96,38 @@ export const RelationshipField: React.FC<Props> = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (operator) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relationSlug}${qs.stringify(query, { addQueryPrefix: true })}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
signal: abortController.signal,
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relationSlug}${qs.stringify(query, { addQueryPrefix: true })}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
)
|
||||
signal: abortController.signal,
|
||||
},
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs = await response.json()
|
||||
if (data.docs.length > 0) {
|
||||
addOptions(data, relationSlug)
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs = await response.json()
|
||||
if (data.docs.length > 0) {
|
||||
addOptions(data, relationSlug)
|
||||
|
||||
if (data.nextPage) {
|
||||
nextPageByRelationshipRef.current.set(relationSlug, data.nextPage)
|
||||
} else {
|
||||
partiallyLoadedRelationshipSlugs.current =
|
||||
partiallyLoadedRelationshipSlugs.current.filter(
|
||||
(partiallyLoadedRelation) => partiallyLoadedRelation !== relationSlug,
|
||||
)
|
||||
}
|
||||
if (data.nextPage) {
|
||||
nextPageByRelationshipRef.current.set(relationSlug, data.nextPage)
|
||||
} else {
|
||||
partiallyLoadedRelationshipSlugs.current =
|
||||
partiallyLoadedRelationshipSlugs.current.filter(
|
||||
(partiallyLoadedRelation) => partiallyLoadedRelation !== relationSlug,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setErrorLoading(t('error:unspecific'))
|
||||
}
|
||||
} catch (e) {
|
||||
if (!abortController.signal.aborted) {
|
||||
console.error(e)
|
||||
}
|
||||
} else {
|
||||
setErrorLoading(t('error:unspecific'))
|
||||
}
|
||||
} catch (e) {
|
||||
if (!abortController.signal.aborted) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,6 @@ export const Condition: React.FC<Props> = (props) => {
|
||||
isClearable={false}
|
||||
onChange={(operator: Option<Operator>) => {
|
||||
setInternalOperatorOption(operator.value)
|
||||
setInternalQueryValue(undefined)
|
||||
}}
|
||||
options={fieldOption?.operators}
|
||||
value={
|
||||
|
||||
@@ -284,39 +284,20 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
||||
// ..
|
||||
// This is a performance enhancement for saving
|
||||
// large documents with hundreds of fields
|
||||
const newState: FormState = {}
|
||||
const newState = {}
|
||||
|
||||
for (const [path, newField] of Object.entries(action.state)) {
|
||||
Object.entries(action.state).forEach(([path, field]) => {
|
||||
const oldField = state[path]
|
||||
|
||||
if (newField.valid !== false) {
|
||||
newField.valid = true
|
||||
}
|
||||
if (newField.passesCondition !== false) {
|
||||
newField.passesCondition = true
|
||||
}
|
||||
const newField = field
|
||||
|
||||
if (!dequal(oldField, newField)) {
|
||||
newState[path] = newField
|
||||
} else if (oldField) {
|
||||
newState[path] = oldField
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
return newState
|
||||
}
|
||||
|
||||
//TODO: Remove this in 4.0 - this is a temporary fix to prevent a breaking change
|
||||
if (action.sanitize) {
|
||||
for (const field of Object.values(action.state)) {
|
||||
if (field.valid !== false) {
|
||||
field.valid = true
|
||||
}
|
||||
if (field.passesCondition !== false) {
|
||||
field.passesCondition = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we're not optimizing, just set the state to the new state
|
||||
return action.state
|
||||
}
|
||||
|
||||
@@ -637,12 +637,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
useEffect(() => {
|
||||
if (initialState) {
|
||||
contextRef.current = { ...initContextState } as FormContextType
|
||||
dispatchFields({
|
||||
type: 'REPLACE_STATE',
|
||||
optimize: false,
|
||||
sanitize: true,
|
||||
state: initialState,
|
||||
})
|
||||
dispatchFields({ type: 'REPLACE_STATE', optimize: false, state: initialState })
|
||||
}
|
||||
}, [initialState, dispatchFields])
|
||||
|
||||
|
||||
@@ -22,29 +22,28 @@ export const mergeServerFormState = ({
|
||||
existingState,
|
||||
incomingState,
|
||||
}: Args): { changed: boolean; newState: FormState } => {
|
||||
const serverPropsToAccept = [
|
||||
'passesCondition',
|
||||
'valid',
|
||||
'errorMessage',
|
||||
'rows',
|
||||
'customComponents',
|
||||
'requiresRender',
|
||||
]
|
||||
|
||||
if (acceptValues) {
|
||||
serverPropsToAccept.push('value')
|
||||
}
|
||||
|
||||
let changed = false
|
||||
|
||||
const newState = {}
|
||||
|
||||
if (existingState) {
|
||||
const serverPropsToAccept = [
|
||||
'passesCondition',
|
||||
'valid',
|
||||
'errorMessage',
|
||||
'rows',
|
||||
'customComponents',
|
||||
'requiresRender',
|
||||
]
|
||||
|
||||
if (acceptValues) {
|
||||
serverPropsToAccept.push('value')
|
||||
}
|
||||
|
||||
for (const [path, newFieldState] of Object.entries(existingState)) {
|
||||
Object.entries(existingState).forEach(([path, newFieldState]) => {
|
||||
if (!incomingState[path]) {
|
||||
continue
|
||||
return
|
||||
}
|
||||
let fieldChanged = false
|
||||
|
||||
/**
|
||||
* Handle error paths
|
||||
@@ -66,7 +65,6 @@ export const mergeServerFormState = ({
|
||||
if (incomingState[path]?.filterOptions || newFieldState.filterOptions) {
|
||||
if (!dequal(incomingState[path]?.filterOptions, newFieldState.filterOptions)) {
|
||||
changed = true
|
||||
fieldChanged = true
|
||||
newFieldState.filterOptions = incomingState[path].filterOptions
|
||||
}
|
||||
}
|
||||
@@ -77,7 +75,6 @@ export const mergeServerFormState = ({
|
||||
serverPropsToAccept.forEach((prop) => {
|
||||
if (!dequal(incomingState[path]?.[prop], newFieldState[prop])) {
|
||||
changed = true
|
||||
fieldChanged = true
|
||||
if (!(prop in incomingState[path])) {
|
||||
// Regarding excluding the customComponents prop from being deleted: the incoming state might not have been rendered, as rendering components for every form onchange is expensive.
|
||||
// Thus, we simply re-use the initial render state
|
||||
@@ -90,25 +87,18 @@ export const mergeServerFormState = ({
|
||||
}
|
||||
})
|
||||
|
||||
if (newFieldState.valid !== false) {
|
||||
newFieldState.valid = true
|
||||
}
|
||||
if (newFieldState.passesCondition !== false) {
|
||||
newFieldState.passesCondition = true
|
||||
}
|
||||
|
||||
// Conditions don't work if we don't memcopy the new state, as the object references would otherwise be the same
|
||||
newState[path] = fieldChanged ? { ...newFieldState } : newFieldState
|
||||
}
|
||||
newState[path] = { ...newFieldState }
|
||||
})
|
||||
|
||||
// Now loop over values that are part of incoming state but not part of existing state, and add them to the new state.
|
||||
// This can happen if a new array row was added. In our local state, we simply add out stubbed `array` and `array.[index].id` entries to the local form state.
|
||||
// However, all other array sub-fields are not added to the local state - those will be added by the server and may be incoming here.
|
||||
|
||||
for (const [path, field] of Object.entries(incomingState)) {
|
||||
for (const [path, newFieldState] of Object.entries(incomingState)) {
|
||||
if (!existingState[path]) {
|
||||
changed = true
|
||||
newState[path] = field
|
||||
newState[path] = newFieldState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,11 +91,6 @@ export type Reset = (data: unknown) => Promise<void>
|
||||
|
||||
export type REPLACE_STATE = {
|
||||
optimize?: boolean
|
||||
/**
|
||||
* If `sanitize` is true, default values will be set for form field properties that are not present in the incoming state.
|
||||
* For example, `valid` will be set to true if it is not present in the incoming state.
|
||||
*/
|
||||
sanitize?: boolean
|
||||
state: FormState
|
||||
type: 'REPLACE_STATE'
|
||||
}
|
||||
|
||||
11836
pnpm-lock.yaml
generated
11836
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,3 @@ NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
|
||||
# Secret used to authenticate cron jobs
|
||||
CRON_SECRET=YOUR_CRON_SECRET_HERE
|
||||
|
||||
# Used to validate preview requests
|
||||
PREVIEW_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: unset;
|
||||
font-weight: unset;
|
||||
font-size: auto;
|
||||
font-weight: auto;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
@@ -1,63 +1,91 @@
|
||||
import type { CollectionSlug, PayloadRequest } from 'payload'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { getPayload, type PayloadRequest } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { CollectionSlug } from 'payload'
|
||||
|
||||
export async function GET(
|
||||
req: {
|
||||
req: Request & {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
} & Request,
|
||||
},
|
||||
): Promise<Response> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
const path = searchParams.get('path')
|
||||
const collection = searchParams.get('collection') as CollectionSlug
|
||||
const slug = searchParams.get('slug')
|
||||
|
||||
const previewSecret = searchParams.get('previewSecret')
|
||||
|
||||
if (previewSecret !== process.env.PREVIEW_SECRET) {
|
||||
if (previewSecret) {
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
} else {
|
||||
if (!path) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
return new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// Verify the given slug exists
|
||||
try {
|
||||
const docs = await payload.find({
|
||||
collection,
|
||||
draft: true,
|
||||
limit: 1,
|
||||
// pagination: false reduces overhead if you don't need totalDocs
|
||||
pagination: false,
|
||||
depth: 0,
|
||||
select: {},
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!docs.docs.length) {
|
||||
return new Response('Document not found', { status: 404 })
|
||||
}
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
}
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
|
||||
if (!path || !collection || !slug) {
|
||||
return new Response('Insufficient search params', { status: 404 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
return new Response('This endpoint can only be used for relative previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
|
||||
@@ -12,16 +12,22 @@ type Props = {
|
||||
}
|
||||
|
||||
export const generatePreviewPath = ({ collection, slug, req }: Props) => {
|
||||
const encodedParams = new URLSearchParams({
|
||||
const path = `${collectionPrefixMap[collection]}/${slug}`
|
||||
|
||||
const params = {
|
||||
slug,
|
||||
collection,
|
||||
path: `${collectionPrefixMap[collection]}/${slug}`,
|
||||
previewSecret: process.env.PREVIEW_SECRET || '',
|
||||
path,
|
||||
}
|
||||
|
||||
const encodedParams = new URLSearchParams()
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
encodedParams.append(key, value)
|
||||
})
|
||||
|
||||
const isProduction =
|
||||
process.env.NODE_ENV === 'production' || Boolean(process.env.VERCEL_PROJECT_PRODUCTION_URL)
|
||||
|
||||
const protocol = isProduction ? 'https:' : req.protocol
|
||||
|
||||
const url = `${protocol}//${req.host}/next/preview?${encodedParams.toString()}`
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
# Database connection string
|
||||
POSTGRES_URL=postgresql://127.0.0.1:5432/your-database-name
|
||||
|
||||
# Used to encrypt JWT tokens
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
# Used to configure CORS, format links and more. No trailing slash
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
|
||||
# Secret used to authenticate cron jobs
|
||||
CRON_SECRET=YOUR_CRON_SECRET_HERE
|
||||
|
||||
# Used to validate preview requests
|
||||
PREVIEW_SECRET=YOUR_SECRET_HERE
|
||||
CRON_SECRET=YOUR_CRON_SECRET_HERE
|
||||
@@ -4,7 +4,7 @@ This is the official [Payload Website Template](https://github.com/payloadcms/pa
|
||||
|
||||
You can deploy to Vercel, using Neon and Vercel Blob Storage with one click:
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/payloadcms/payload/tree/main/templates/with-vercel-website&project-name=payload-project&env=PAYLOAD_SECRET%2CCRON_SECRET%2CPREVIEW_SECRET&build-command=pnpm%20run%20ci&stores=%5B%7B%22type%22:%22postgres%22%7D,%7B%22type%22:%22blob%22%7D%5D)
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/payloadcms/payload/tree/main/templates/with-vercel-website&project-name=payload-project&env=PAYLOAD_SECRET%2CCRON_SECRET&build-command=pnpm%20run%20ci&stores=%5B%7B%22type%22:%22postgres%22%7D,%7B%22type%22:%22blob%22%7D%5D)
|
||||
|
||||
This template is right for you if you are working on:
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: unset;
|
||||
font-weight: unset;
|
||||
font-size: auto;
|
||||
font-weight: auto;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
@@ -1,63 +1,91 @@
|
||||
import type { CollectionSlug, PayloadRequest } from 'payload'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { getPayload, type PayloadRequest } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { CollectionSlug } from 'payload'
|
||||
|
||||
export async function GET(
|
||||
req: {
|
||||
req: Request & {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
} & Request,
|
||||
},
|
||||
): Promise<Response> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
const path = searchParams.get('path')
|
||||
const collection = searchParams.get('collection') as CollectionSlug
|
||||
const slug = searchParams.get('slug')
|
||||
|
||||
const previewSecret = searchParams.get('previewSecret')
|
||||
|
||||
if (previewSecret !== process.env.PREVIEW_SECRET) {
|
||||
if (previewSecret) {
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
} else {
|
||||
if (!path) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
return new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// Verify the given slug exists
|
||||
try {
|
||||
const docs = await payload.find({
|
||||
collection,
|
||||
draft: true,
|
||||
limit: 1,
|
||||
// pagination: false reduces overhead if you don't need totalDocs
|
||||
pagination: false,
|
||||
depth: 0,
|
||||
select: {},
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!docs.docs.length) {
|
||||
return new Response('Document not found', { status: 404 })
|
||||
}
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
}
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
|
||||
if (!path || !collection || !slug) {
|
||||
return new Response('Insufficient search params', { status: 404 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
return new Response('This endpoint can only be used for relative previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ export const contact: Partial<Page> = {
|
||||
{
|
||||
blockType: 'formBlock',
|
||||
enableIntro: true,
|
||||
// @ts-ignore
|
||||
form: '{{CONTACT_FORM_ID}}',
|
||||
introContent: {
|
||||
root: {
|
||||
|
||||
@@ -84,7 +84,6 @@ export const homeStatic: Page = {
|
||||
title: 'Payload Website Template',
|
||||
},
|
||||
title: 'Home',
|
||||
// @ts-ignore
|
||||
id: '',
|
||||
layout: [],
|
||||
updatedAt: '',
|
||||
|
||||
@@ -23,7 +23,6 @@ export const home: RequiredDataFromCollectionSlug<'pages'> = {
|
||||
},
|
||||
},
|
||||
],
|
||||
// @ts-ignore
|
||||
media: '{{IMAGE_1}}',
|
||||
richText: {
|
||||
root: {
|
||||
@@ -502,7 +501,6 @@ export const home: RequiredDataFromCollectionSlug<'pages'> = {
|
||||
{
|
||||
blockName: 'Media Block',
|
||||
blockType: 'mediaBlock',
|
||||
// @ts-ignore
|
||||
media: '{{IMAGE_2}}',
|
||||
},
|
||||
{
|
||||
@@ -659,7 +657,6 @@ export const home: RequiredDataFromCollectionSlug<'pages'> = {
|
||||
],
|
||||
meta: {
|
||||
description: 'An open-source website built with Payload and Next.js.',
|
||||
// @ts-ignore
|
||||
image: '{{IMAGE_1}}',
|
||||
title: 'Payload Website Template',
|
||||
},
|
||||
|
||||
@@ -12,16 +12,22 @@ type Props = {
|
||||
}
|
||||
|
||||
export const generatePreviewPath = ({ collection, slug, req }: Props) => {
|
||||
const encodedParams = new URLSearchParams({
|
||||
const path = `${collectionPrefixMap[collection]}/${slug}`
|
||||
|
||||
const params = {
|
||||
slug,
|
||||
collection,
|
||||
path: `${collectionPrefixMap[collection]}/${slug}`,
|
||||
previewSecret: process.env.PREVIEW_SECRET || '',
|
||||
path,
|
||||
}
|
||||
|
||||
const encodedParams = new URLSearchParams()
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
encodedParams.append(key, value)
|
||||
})
|
||||
|
||||
const isProduction =
|
||||
process.env.NODE_ENV === 'production' || Boolean(process.env.VERCEL_PROJECT_PRODUCTION_URL)
|
||||
|
||||
const protocol = isProduction ? 'https:' : req.protocol
|
||||
|
||||
const url = `${protocol}//${req.host}/next/preview?${encodedParams.toString()}`
|
||||
|
||||
@@ -327,29 +327,6 @@ describe('List View', () => {
|
||||
await expect(page.locator('.condition__value input')).toHaveValue('')
|
||||
})
|
||||
|
||||
test('should reset query value when operator is changed', async () => {
|
||||
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
|
||||
await page.locator('.list-controls__toggle-columns').click()
|
||||
await openListFilters(page, {})
|
||||
await page.locator('.where-builder__add-first-filter').click()
|
||||
|
||||
const operatorField = page.locator('.condition__operator')
|
||||
await operatorField.click()
|
||||
|
||||
const dropdownOperatorOptions = operatorField.locator('.rs__option')
|
||||
await dropdownOperatorOptions.locator('text=equals').click()
|
||||
const valueField = page.locator('.condition__value > input')
|
||||
await valueField.fill(id)
|
||||
|
||||
// change operator to "not equals"
|
||||
await operatorField.click()
|
||||
await dropdownOperatorOptions.locator('text=not equals').click()
|
||||
|
||||
// expect value field to reset (be empty)
|
||||
await expect(valueField.locator('.rs__placeholder')).toContainText('Select a value')
|
||||
await expect(page.locator('.condition__value input')).toHaveValue('')
|
||||
})
|
||||
|
||||
test('should accept where query from valid URL where parameter', async () => {
|
||||
// delete all posts created by the seed
|
||||
await deleteAllPosts()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { selectVersionsFieldsSlug } from '../../slugs.js'
|
||||
|
||||
const SelectVersionsFields: CollectionConfig = {
|
||||
slug: selectVersionsFieldsSlug,
|
||||
versions: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['a', 'b', 'c'],
|
||||
name: 'hasMany',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
name: 'array',
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['a', 'b', 'c'],
|
||||
name: 'hasManyArr',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['a', 'b', 'c'],
|
||||
name: 'hasManyBlocks',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default SelectVersionsFields
|
||||
@@ -33,7 +33,6 @@ import RelationshipFields from './collections/Relationship/index.js'
|
||||
import RichTextFields from './collections/RichText/index.js'
|
||||
import RowFields from './collections/Row/index.js'
|
||||
import SelectFields from './collections/Select/index.js'
|
||||
import SelectVersionsFields from './collections/SelectVersions/index.js'
|
||||
import TabsFields from './collections/Tabs/index.js'
|
||||
import { TabsFields2 } from './collections/Tabs2/index.js'
|
||||
import TextFields from './collections/Text/index.js'
|
||||
@@ -68,7 +67,7 @@ export const collectionSlugs: CollectionConfig[] = [
|
||||
],
|
||||
},
|
||||
LexicalInBlock,
|
||||
SelectVersionsFields,
|
||||
|
||||
ArrayFields,
|
||||
BlockFields,
|
||||
CheckboxFields,
|
||||
|
||||
@@ -678,30 +678,6 @@ describe('Fields', () => {
|
||||
|
||||
expect(upd.array[0].group.selectHasMany).toStrictEqual(['six'])
|
||||
})
|
||||
|
||||
it('should work with versions', async () => {
|
||||
const base = await payload.create({
|
||||
collection: 'select-versions-fields',
|
||||
data: { hasMany: ['a', 'b'] },
|
||||
})
|
||||
|
||||
expect(base.hasMany).toStrictEqual(['a', 'b'])
|
||||
|
||||
const array = await payload.create({
|
||||
collection: 'select-versions-fields',
|
||||
data: { array: [{ hasManyArr: ['a', 'b'] }] },
|
||||
draft: true,
|
||||
})
|
||||
|
||||
expect(array.array[0]?.hasManyArr).toStrictEqual(['a', 'b'])
|
||||
|
||||
const block = await payload.create({
|
||||
collection: 'select-versions-fields',
|
||||
data: { blocks: [{ blockType: 'block', hasManyBlocks: ['a', 'b'] }] },
|
||||
})
|
||||
|
||||
expect(block.blocks[0]?.hasManyBlocks).toStrictEqual(['a', 'b'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('number', () => {
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface Config {
|
||||
lexicalObjectReferenceBug: LexicalObjectReferenceBug;
|
||||
users: User;
|
||||
LexicalInBlock: LexicalInBlock;
|
||||
'select-versions-fields': SelectVersionsField;
|
||||
'array-fields': ArrayField;
|
||||
'block-fields': BlockField;
|
||||
'checkbox-fields': CheckboxField;
|
||||
@@ -80,7 +79,6 @@ export interface Config {
|
||||
lexicalObjectReferenceBug: LexicalObjectReferenceBugSelect<false> | LexicalObjectReferenceBugSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
LexicalInBlock: LexicalInBlockSelect<false> | LexicalInBlockSelect<true>;
|
||||
'select-versions-fields': SelectVersionsFieldsSelect<false> | SelectVersionsFieldsSelect<true>;
|
||||
'array-fields': ArrayFieldsSelect<false> | ArrayFieldsSelect<true>;
|
||||
'block-fields': BlockFieldsSelect<false> | BlockFieldsSelect<true>;
|
||||
'checkbox-fields': CheckboxFieldsSelect<false> | CheckboxFieldsSelect<true>;
|
||||
@@ -454,30 +452,6 @@ export interface LexicalInBlock {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "select-versions-fields".
|
||||
*/
|
||||
export interface SelectVersionsField {
|
||||
id: string;
|
||||
hasMany?: ('a' | 'b' | 'c')[] | null;
|
||||
array?:
|
||||
| {
|
||||
hasManyArr?: ('a' | 'b' | 'c')[] | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
blocks?:
|
||||
| {
|
||||
hasManyArr?: ('a' | 'b' | 'c')[] | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'block';
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "array-fields".
|
||||
@@ -1830,10 +1804,6 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'LexicalInBlock';
|
||||
value: string | LexicalInBlock;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'select-versions-fields';
|
||||
value: string | SelectVersionsField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
@@ -2104,32 +2074,6 @@ export interface LexicalInBlockSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "select-versions-fields_select".
|
||||
*/
|
||||
export interface SelectVersionsFieldsSelect<T extends boolean = true> {
|
||||
hasMany?: T;
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
hasManyArr?: T;
|
||||
id?: T;
|
||||
};
|
||||
blocks?:
|
||||
| T
|
||||
| {
|
||||
block?:
|
||||
| T
|
||||
| {
|
||||
hasManyArr?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "array-fields_select".
|
||||
|
||||
@@ -24,7 +24,6 @@ export const relationshipFieldsSlug = 'relationship-fields'
|
||||
export const richTextFieldsSlug = 'rich-text-fields'
|
||||
export const rowFieldsSlug = 'row-fields'
|
||||
export const selectFieldsSlug = 'select-fields'
|
||||
export const selectVersionsFieldsSlug = 'select-versions-fields'
|
||||
export const tabsFieldsSlug = 'tabs-fields'
|
||||
export const tabsFields2Slug = 'tabs-fields-2'
|
||||
export const textFieldsSlug = 'text-fields'
|
||||
|
||||
@@ -428,32 +428,6 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'relations',
|
||||
fields: [
|
||||
{
|
||||
name: 'item',
|
||||
type: 'relationship',
|
||||
relationTo: ['items'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'items',
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
options: ['completed', 'failed', 'pending'],
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
type: 'join',
|
||||
on: 'item',
|
||||
collection: 'relations',
|
||||
name: 'relation',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
|
||||
@@ -1577,23 +1577,6 @@ describe('Relationships', () => {
|
||||
expect(res.docs).toHaveLength(1)
|
||||
expect(res.docs[0].id).toBe(id)
|
||||
})
|
||||
|
||||
it('should update document that polymorphicaly joined to another collection', async () => {
|
||||
const item = await payload.create({ collection: 'items', data: { status: 'pending' } })
|
||||
|
||||
await payload.create({
|
||||
collection: 'relations',
|
||||
data: { item: { relationTo: 'items', value: item } },
|
||||
})
|
||||
|
||||
const updated = await payload.update({
|
||||
collection: 'items',
|
||||
data: { status: 'completed' },
|
||||
id: item.id,
|
||||
})
|
||||
|
||||
expect(updated.status).toBe('completed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -29,18 +29,12 @@ export interface Config {
|
||||
'rels-to-pages-and-custom-text-ids': RelsToPagesAndCustomTextId;
|
||||
'object-writes': ObjectWrite;
|
||||
'deep-nested': DeepNested;
|
||||
relations: Relation1;
|
||||
items: Item;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {
|
||||
items: {
|
||||
relation: 'relations';
|
||||
};
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
postsLocalized: PostsLocalizedSelect<false> | PostsLocalizedSelect<true>;
|
||||
@@ -60,8 +54,6 @@ export interface Config {
|
||||
'rels-to-pages-and-custom-text-ids': RelsToPagesAndCustomTextIdsSelect<false> | RelsToPagesAndCustomTextIdsSelect<true>;
|
||||
'object-writes': ObjectWritesSelect<false> | ObjectWritesSelect<true>;
|
||||
'deep-nested': DeepNestedSelect<false> | DeepNestedSelect<true>;
|
||||
relations: RelationsSelect<false> | RelationsSelect<true>;
|
||||
items: ItemsSelect<false> | ItemsSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
@@ -381,33 +373,6 @@ export interface DeepNested {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relations".
|
||||
*/
|
||||
export interface Relation1 {
|
||||
id: string;
|
||||
item?: {
|
||||
relationTo: 'items';
|
||||
value: string | Item;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "items".
|
||||
*/
|
||||
export interface Item {
|
||||
id: string;
|
||||
status?: ('completed' | 'failed' | 'pending') | null;
|
||||
relation?: {
|
||||
docs?: (string | Relation1)[] | null;
|
||||
hasNextPage?: boolean | null;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
@@ -487,14 +452,6 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'deep-nested';
|
||||
value: string | DeepNested;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'relations';
|
||||
value: string | Relation1;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'items';
|
||||
value: string | Item;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
@@ -764,25 +721,6 @@ export interface DeepNestedSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relations_select".
|
||||
*/
|
||||
export interface RelationsSelect<T extends boolean = true> {
|
||||
item?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "items_select".
|
||||
*/
|
||||
export interface ItemsSelect<T extends boolean = true> {
|
||||
status?: T;
|
||||
relation?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
|
||||
@@ -15,17 +15,6 @@ export const Diff: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'arrayLocalized',
|
||||
type: 'array',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'textInArrayLocalized',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
|
||||
@@ -945,21 +945,6 @@ describe('Versions', () => {
|
||||
await expect(textInArray.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInArray2')
|
||||
})
|
||||
|
||||
test('correctly renders diff for localized array fields', async () => {
|
||||
await navigateToVersionFieldsDiff()
|
||||
|
||||
const textInArray = page
|
||||
.locator('[data-field-path="arrayLocalized"][data-locale="en"]')
|
||||
.locator('[data-field-path="arrayLocalized.0.textInArrayLocalized"]')
|
||||
|
||||
await expect(textInArray.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
|
||||
'textInArrayLocalized',
|
||||
)
|
||||
await expect(textInArray.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
|
||||
'textInArrayLocalized2',
|
||||
)
|
||||
})
|
||||
|
||||
test('correctly renders diff for block fields', async () => {
|
||||
await navigateToVersionFieldsDiff()
|
||||
|
||||
|
||||
@@ -224,12 +224,6 @@ export interface Diff {
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
arrayLocalized?:
|
||||
| {
|
||||
textInArrayLocalized?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
blocks?:
|
||||
| {
|
||||
textInBlock?: string | null;
|
||||
@@ -647,12 +641,6 @@ export interface DiffSelect<T extends boolean = true> {
|
||||
textInArray?: T;
|
||||
id?: T;
|
||||
};
|
||||
arrayLocalized?:
|
||||
| T
|
||||
| {
|
||||
textInArrayLocalized?: T;
|
||||
id?: T;
|
||||
};
|
||||
blocks?:
|
||||
| T
|
||||
| {
|
||||
|
||||
@@ -122,18 +122,12 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
|
||||
const diffDoc = await _payload.create({
|
||||
collection: diffCollectionSlug,
|
||||
locale: 'en',
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
textInArray: 'textInArray',
|
||||
},
|
||||
],
|
||||
arrayLocalized: [
|
||||
{
|
||||
textInArrayLocalized: 'textInArrayLocalized',
|
||||
},
|
||||
],
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'TextBlock',
|
||||
@@ -170,18 +164,12 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
const updatedDiffDoc = await _payload.update({
|
||||
id: diffDoc.id,
|
||||
collection: diffCollectionSlug,
|
||||
locale: 'en',
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
textInArray: 'textInArray2',
|
||||
},
|
||||
],
|
||||
arrayLocalized: [
|
||||
{
|
||||
textInArrayLocalized: 'textInArrayLocalized2',
|
||||
},
|
||||
],
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'TextBlock',
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/fields/config.ts"],
|
||||
"@payload-config": ["./test/_community/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user