fix(live-preview): field recursion and relationship population (#4045)
This commit is contained in:
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -64,6 +64,13 @@
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Live Preview Integration",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts build",
|
||||
"env": {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type MergeLiveDataArgs<T> = {
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: Partial<T>
|
||||
initialData: T
|
||||
returnNumberOfRequests?: boolean
|
||||
serverURL: string
|
||||
}
|
||||
|
||||
@@ -17,8 +18,13 @@ export const mergeData = async <T>({
|
||||
fieldSchema,
|
||||
incomingData,
|
||||
initialData,
|
||||
returnNumberOfRequests,
|
||||
serverURL,
|
||||
}: MergeLiveDataArgs<T>): Promise<T> => {
|
||||
}: MergeLiveDataArgs<T>): Promise<
|
||||
T & {
|
||||
_numberOfRequests?: number
|
||||
}
|
||||
> => {
|
||||
const result = { ...initialData }
|
||||
|
||||
const populationPromises: Promise<void>[] = []
|
||||
@@ -35,5 +41,8 @@ export const mergeData = async <T>({
|
||||
|
||||
await Promise.all(populationPromises)
|
||||
|
||||
return result
|
||||
return {
|
||||
...result,
|
||||
...(returnNumberOfRequests ? { _numberOfRequests: populationPromises.length } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,20 @@ export const promise = async ({
|
||||
ref,
|
||||
serverURL,
|
||||
}: Args): Promise<void> => {
|
||||
const res: any = await fetch(
|
||||
`${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`,
|
||||
).then((res) => res.json())
|
||||
const url = `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`
|
||||
|
||||
let res: any = null
|
||||
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((res) => res.json())
|
||||
} catch (err) {
|
||||
console.error(err) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
ref[accessor] = res
|
||||
}
|
||||
|
||||
@@ -15,38 +15,39 @@ type Args<T> = {
|
||||
export const traverseFields = <T>({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema,
|
||||
fieldSchema: fieldSchemas,
|
||||
incomingData,
|
||||
populationPromises,
|
||||
result,
|
||||
serverURL,
|
||||
}: Args<T>): void => {
|
||||
fieldSchema.forEach((fieldJSON) => {
|
||||
if ('name' in fieldJSON && typeof fieldJSON.name === 'string') {
|
||||
const fieldName = fieldJSON.name
|
||||
fieldSchemas.forEach((fieldSchema) => {
|
||||
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
|
||||
const fieldName = fieldSchema.name
|
||||
|
||||
switch (fieldJSON.type) {
|
||||
switch (fieldSchema.type) {
|
||||
case 'array':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
||||
const hasExistingRow =
|
||||
Array.isArray(result[fieldName]) &&
|
||||
typeof result[fieldName][i] === 'object' &&
|
||||
result[fieldName][i] !== null
|
||||
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
const newRow = hasExistingRow ? { ...result[fieldName][i] } : {}
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema: fieldJSON.fields,
|
||||
incomingData: row,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingRow,
|
||||
populationPromises,
|
||||
result: newRow,
|
||||
result: result[fieldName][i],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
return newRow
|
||||
return result[fieldName][i]
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -54,18 +55,21 @@ export const traverseFields = <T>({
|
||||
case 'blocks':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
|
||||
const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType]
|
||||
const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType]
|
||||
|
||||
// Compare the index and id to determine if this block already exists in the result
|
||||
// If so, we want to use the existing block as the base, otherwise take the incoming block
|
||||
// Either way, we will traverse the fields of the block to populate relationships
|
||||
const isExistingBlock =
|
||||
Array.isArray(result[fieldName]) &&
|
||||
typeof result[fieldName][i] === 'object' &&
|
||||
result[fieldName][i] !== null &&
|
||||
result[fieldName][i].id === incomingBlock.id
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
const block = isExistingBlock ? result[fieldName][i] : incomingBlock
|
||||
if (
|
||||
!result[fieldName][i] ||
|
||||
result[fieldName][i].id !== incomingBlock.id ||
|
||||
result[fieldName][i].blockType !== incomingBlock.blockType
|
||||
) {
|
||||
result[fieldName][i] = {
|
||||
blockType: incomingBlock.blockType,
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
@@ -73,11 +77,11 @@ export const traverseFields = <T>({
|
||||
fieldSchema: incomingBlockJSON.fields,
|
||||
incomingData: incomingBlock,
|
||||
populationPromises,
|
||||
result: block,
|
||||
result: result[fieldName][i],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
return block
|
||||
return result[fieldName][i]
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = []
|
||||
@@ -94,7 +98,7 @@ export const traverseFields = <T>({
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema: fieldJSON.fields,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
populationPromises,
|
||||
result: result[fieldName],
|
||||
@@ -105,31 +109,35 @@ export const traverseFields = <T>({
|
||||
|
||||
case 'upload':
|
||||
case 'relationship':
|
||||
if (fieldJSON.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
|
||||
result[fieldName] = Array.isArray(result[fieldName])
|
||||
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
|
||||
: []
|
||||
|
||||
incomingData[fieldName].forEach((relation, i) => {
|
||||
// Handle `hasMany` polymorphic
|
||||
if (Array.isArray(fieldJSON.relationTo)) {
|
||||
const existingID = existingValue[i]?.value?.id
|
||||
|
||||
if (
|
||||
existingID !== relation.value ||
|
||||
existingValue[i]?.relationTo !== relation.relationTo
|
||||
) {
|
||||
result[fieldName][i] = {
|
||||
relationTo: relation.relationTo,
|
||||
// Handle `hasMany` relationships
|
||||
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
}
|
||||
|
||||
incomingData[fieldName].forEach((incomingRelation, i) => {
|
||||
// Handle `hasMany` polymorphic
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName][i]) {
|
||||
result[fieldName][i] = {
|
||||
relationTo: incomingRelation.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const oldID = result[fieldName][i]?.value?.id
|
||||
const oldRelation = result[fieldName][i]?.relationTo
|
||||
const newID = incomingRelation.value
|
||||
const newRelation = incomingRelation.relationTo
|
||||
|
||||
if (oldID !== newID || oldRelation !== newRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: relation.value,
|
||||
id: incomingRelation.value,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: relation.relationTo,
|
||||
collection: newRelation,
|
||||
depth,
|
||||
ref: result[fieldName][i],
|
||||
serverURL,
|
||||
@@ -138,15 +146,13 @@ export const traverseFields = <T>({
|
||||
}
|
||||
} else {
|
||||
// Handle `hasMany` monomorphic
|
||||
const existingID = existingValue[i]?.id
|
||||
|
||||
if (existingID !== relation) {
|
||||
if (result[fieldName][i]?.id !== incomingRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: relation,
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
apiRoute,
|
||||
collection: String(fieldJSON.relationTo),
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result[fieldName],
|
||||
serverURL,
|
||||
@@ -157,29 +163,49 @@ export const traverseFields = <T>({
|
||||
})
|
||||
} else {
|
||||
// Handle `hasOne` polymorphic
|
||||
if (Array.isArray(fieldJSON.relationTo)) {
|
||||
const hasNewValue =
|
||||
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
|
||||
const hasOldValue =
|
||||
typeof result[fieldName] === 'object' && result[fieldName] !== null
|
||||
|
||||
const newValue = hasNewValue ? incomingData[fieldName].value : ''
|
||||
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
|
||||
|
||||
const oldValue = hasOldValue ? result[fieldName].value : ''
|
||||
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
|
||||
|
||||
if (newValue !== oldValue || newRelation !== oldRelation) {
|
||||
if (newValue) {
|
||||
if (Array.isArray(fieldSchema.relationTo)) {
|
||||
// if the field doesn't exist on the result, create it
|
||||
// the value will be populated later
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = {
|
||||
relationTo: newRelation,
|
||||
relationTo: incomingData[fieldName]?.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
const hasNewValue =
|
||||
incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName] !== null
|
||||
|
||||
const hasOldValue =
|
||||
result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName] !== null
|
||||
|
||||
const newID = hasNewValue
|
||||
? typeof incomingData[fieldName].value === 'object'
|
||||
? incomingData[fieldName].value.id
|
||||
: incomingData[fieldName].value
|
||||
: ''
|
||||
|
||||
const oldID = hasOldValue
|
||||
? typeof result[fieldName].value === 'object'
|
||||
? result[fieldName].value.id
|
||||
: result[fieldName].value
|
||||
: ''
|
||||
|
||||
const newRelation = hasNewValue ? incomingData[fieldName].relationTo : ''
|
||||
const oldRelation = hasOldValue ? result[fieldName].relationTo : ''
|
||||
|
||||
// if the new value/relation is different from the old value/relation
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (newID !== oldID || newRelation !== oldRelation) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: newValue,
|
||||
id: newID,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: newRelation,
|
||||
@@ -188,34 +214,36 @@ export const traverseFields = <T>({
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle `hasOne` monomorphic
|
||||
const newID: string =
|
||||
(typeof incomingData[fieldName] === 'string' && incomingData[fieldName]) ||
|
||||
(typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName] !== null &&
|
||||
const newID: number | string | undefined =
|
||||
(incomingData[fieldName] &&
|
||||
typeof incomingData[fieldName] === 'object' &&
|
||||
incomingData[fieldName].id) ||
|
||||
''
|
||||
incomingData[fieldName]
|
||||
|
||||
const oldID: string =
|
||||
(typeof result[fieldName] === 'string' && result[fieldName]) ||
|
||||
(typeof result[fieldName] === 'object' &&
|
||||
result[fieldName] !== null &&
|
||||
const oldID: number | string | undefined =
|
||||
(result[fieldName] &&
|
||||
typeof result[fieldName] === 'object' &&
|
||||
result[fieldName].id) ||
|
||||
''
|
||||
result[fieldName]
|
||||
|
||||
// if the new value is different from the old value
|
||||
// populate the new value, otherwise leave it alone
|
||||
if (newID !== oldID) {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
apiRoute,
|
||||
collection: String(fieldJSON.relationTo),
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result as Record<string, unknown>,
|
||||
serverURL,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DndContext } from '@dnd-kit/core'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { LivePreviewConfig } from '../../../../../exports/config'
|
||||
import type { Field } from '../../../../../fields/config/types'
|
||||
|
||||
@@ -71,5 +71,47 @@ export const Pages: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Hidden fields for testing purposes
|
||||
{
|
||||
name: 'relationshipPolyHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: ['posts'],
|
||||
hasMany: true,
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'relationshipMonoHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'relationshipMonoHasOne',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'arrayOfRelationships',
|
||||
type: 'array',
|
||||
admin: {
|
||||
hidden: true,
|
||||
disabled: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'relationshipWithinArray',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
name: 'reference',
|
||||
label: 'Document to link to',
|
||||
type: 'relationship',
|
||||
relationTo: ['pages'],
|
||||
relationTo: ['posts', 'pages'],
|
||||
required: true,
|
||||
maxDepth: 1,
|
||||
admin: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
|
||||
import type { Media, Page } from './payload-types'
|
||||
import type { Media, Page, Post } from './payload-types'
|
||||
|
||||
import { handleMessage } from '../../packages/live-preview/src/handleMessage'
|
||||
import { mergeData } from '../../packages/live-preview/src/mergeData'
|
||||
@@ -10,20 +10,21 @@ import { fieldSchemaToJSON } from '../../packages/payload/src/utilities/fieldSch
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
import { Pages } from './collections/Pages'
|
||||
import { postsSlug } from './collections/Posts'
|
||||
import configPromise from './config'
|
||||
import { pagesSlug } from './shared'
|
||||
|
||||
require('isomorphic-fetch')
|
||||
|
||||
let client
|
||||
let serverURL
|
||||
|
||||
let page: Page
|
||||
let media: Media
|
||||
|
||||
const schemaJSON = fieldSchemaToJSON(Pages.fields)
|
||||
|
||||
describe('Collections - Live Preview', () => {
|
||||
let client
|
||||
let serverURL
|
||||
|
||||
let testPost: Post
|
||||
let media: Media
|
||||
|
||||
beforeAll(async () => {
|
||||
const { serverURL: incomingServerURL } = await initPayloadTest({
|
||||
__dirname,
|
||||
@@ -35,11 +36,11 @@ describe('Collections - Live Preview', () => {
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: pagesSlug })
|
||||
await client.login()
|
||||
|
||||
page = await payload.create({
|
||||
collection: pagesSlug,
|
||||
testPost = await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
slug: 'home',
|
||||
title: 'Test Page',
|
||||
slug: 'post-1',
|
||||
title: 'Test Post',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -63,18 +64,20 @@ describe('Collections - Live Preview', () => {
|
||||
event: {
|
||||
data: JSON.stringify({
|
||||
data: {
|
||||
title: 'Test Page (Change 1)',
|
||||
title: 'Test Page (Changed)',
|
||||
},
|
||||
fieldSchemaJSON: schemaJSON,
|
||||
type: 'payload-live-preview',
|
||||
}),
|
||||
origin: serverURL,
|
||||
} as MessageEvent,
|
||||
initialData: page,
|
||||
initialData: {
|
||||
title: 'Test Page',
|
||||
} as Page,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
expect(handledMessage.title).toEqual('Test Page (Change 1)')
|
||||
expect(handledMessage.title).toEqual('Test Page (Changed)')
|
||||
})
|
||||
|
||||
it('caches `fieldSchemaJSON`', async () => {
|
||||
@@ -83,68 +86,89 @@ describe('Collections - Live Preview', () => {
|
||||
event: {
|
||||
data: JSON.stringify({
|
||||
data: {
|
||||
title: 'Test Page (Change 2)',
|
||||
title: 'Test Page (Changed)',
|
||||
},
|
||||
type: 'payload-live-preview',
|
||||
}),
|
||||
origin: serverURL,
|
||||
} as MessageEvent,
|
||||
initialData: page,
|
||||
initialData: {
|
||||
title: 'Test Page',
|
||||
} as Page,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
expect(handledMessage.title).toEqual('Test Page (Change 2)')
|
||||
expect(handledMessage.title).toEqual('Test Page (Changed)')
|
||||
})
|
||||
|
||||
it('merges data', async () => {
|
||||
expect(page?.id).toBeDefined()
|
||||
const initialData: Partial<Page> = {
|
||||
id: '123',
|
||||
title: 'Test Page',
|
||||
}
|
||||
|
||||
const mergedData = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: page,
|
||||
initialData: page,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
expect(mergedData.id).toEqual(page.id)
|
||||
})
|
||||
|
||||
it('merges strings', async () => {
|
||||
const mergedData = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...page,
|
||||
title: 'Test Page (Change 3)',
|
||||
title: 'Test Page (Merged)',
|
||||
},
|
||||
initialData: page,
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(mergedData.title).toEqual('Test Page (Change 3)')
|
||||
expect(mergedData.id).toEqual(initialData.id)
|
||||
expect(mergedData._numberOfRequests).toEqual(0)
|
||||
})
|
||||
|
||||
it('— strings - merges data', async () => {
|
||||
const initialData: Partial<Page> = {
|
||||
title: 'Test Page',
|
||||
}
|
||||
|
||||
const mergedData = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...initialData,
|
||||
title: 'Test Page (Changed)',
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(mergedData.title).toEqual('Test Page (Changed)')
|
||||
expect(mergedData._numberOfRequests).toEqual(0)
|
||||
})
|
||||
|
||||
// TODO: this test is not working in Postgres
|
||||
// This is because of how relationships are handled in `mergeData`
|
||||
// This test passes in MongoDB, though
|
||||
it.skip('adds and removes uploads', async () => {
|
||||
it.skip('— uploads - adds and removes media', async () => {
|
||||
const initialData: Partial<Page> = {
|
||||
title: 'Test Page',
|
||||
}
|
||||
|
||||
// Add upload
|
||||
const mergedData = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...page,
|
||||
...initialData,
|
||||
hero: {
|
||||
type: 'highImpact',
|
||||
media: media.id,
|
||||
},
|
||||
},
|
||||
initialData: page,
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(mergedData.hero.media).toMatchObject(media)
|
||||
expect(mergedData._numberOfRequests).toEqual(1)
|
||||
|
||||
// Add upload
|
||||
const mergedDataWithoutUpload = await mergeData({
|
||||
@@ -161,20 +185,214 @@ describe('Collections - Live Preview', () => {
|
||||
serverURL,
|
||||
})
|
||||
|
||||
expect(mergedDataWithoutUpload.hero.media).toEqual(null)
|
||||
expect(mergedDataWithoutUpload.hero.media).toBeFalsy()
|
||||
})
|
||||
|
||||
it('add, reorder, and remove blocks', async () => {
|
||||
// Add new blocks
|
||||
it('— relationships - populates all types', async () => {
|
||||
const initialData: Partial<Page> = {
|
||||
title: 'Test Page',
|
||||
}
|
||||
|
||||
const merge1 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...page,
|
||||
...initialData,
|
||||
relationshipMonoHasOne: testPost.id,
|
||||
relationshipMonoHasMany: [testPost.id],
|
||||
relationshipPolyHasMany: [{ value: testPost.id, relationTo: postsSlug }],
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(3)
|
||||
expect(merge1.relationshipMonoHasOne).toMatchObject(testPost)
|
||||
expect(merge1.relationshipMonoHasMany).toMatchObject([testPost])
|
||||
expect(merge1.relationshipPolyHasMany).toMatchObject([
|
||||
{ value: testPost, relationTo: postsSlug },
|
||||
])
|
||||
|
||||
// Clear relationships
|
||||
const merge2 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...merge1,
|
||||
relationshipMonoHasOne: null,
|
||||
relationshipMonoHasMany: [],
|
||||
relationshipPolyHasMany: [],
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(merge2._numberOfRequests).toEqual(0)
|
||||
expect(merge2.relationshipMonoHasOne).toBeFalsy()
|
||||
expect(merge2.relationshipMonoHasMany).toEqual([])
|
||||
expect(merge2.relationshipPolyHasMany).toEqual([])
|
||||
|
||||
// Now populate the relationships again
|
||||
const merge3 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...merge2,
|
||||
relationshipMonoHasOne: testPost.id,
|
||||
relationshipMonoHasMany: [testPost.id],
|
||||
relationshipPolyHasMany: [{ value: testPost.id, relationTo: postsSlug }],
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(merge3._numberOfRequests).toEqual(3)
|
||||
expect(merge3.relationshipMonoHasOne).toMatchObject(testPost)
|
||||
expect(merge3.relationshipMonoHasMany).toMatchObject([testPost])
|
||||
expect(merge3.relationshipPolyHasMany).toMatchObject([
|
||||
{ value: testPost, relationTo: postsSlug },
|
||||
])
|
||||
})
|
||||
|
||||
it('— relationships - populates within arrays', async () => {
|
||||
const initialData: Partial<Page> = {
|
||||
title: 'Test Page',
|
||||
}
|
||||
|
||||
const merge1 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...initialData,
|
||||
arrayOfRelationships: [
|
||||
{
|
||||
id: '123',
|
||||
relationshipWithinArray: testPost.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(1)
|
||||
expect(merge1.arrayOfRelationships).toHaveLength(1)
|
||||
expect(merge1.arrayOfRelationships).toMatchObject([
|
||||
{
|
||||
id: '123',
|
||||
relationshipWithinArray: testPost,
|
||||
},
|
||||
])
|
||||
|
||||
// Add a new block before the populated one, then check to see that the relationship is still populated
|
||||
const merge2 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...merge1,
|
||||
arrayOfRelationships: [
|
||||
{
|
||||
id: '456',
|
||||
relationshipWithinArray: undefined,
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
relationshipWithinArray: testPost.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
expect(merge2._numberOfRequests).toEqual(1)
|
||||
expect(merge2.arrayOfRelationships).toHaveLength(2)
|
||||
expect(merge2.arrayOfRelationships).toMatchObject([
|
||||
{
|
||||
id: '456',
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
relationshipWithinArray: testPost,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('— relationships - populates within blocks', async () => {
|
||||
const block1 = (shallow?: boolean): Extract<Page['layout'][0], { blockType: 'cta' }> => ({
|
||||
blockType: 'cta',
|
||||
id: '123',
|
||||
links: [
|
||||
{
|
||||
link: {
|
||||
label: 'Link 1',
|
||||
type: 'reference',
|
||||
reference: {
|
||||
relationTo: 'posts',
|
||||
value: shallow ? testPost?.id : testPost,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const block2: Extract<Page['layout'][0], { blockType: 'content' }> = {
|
||||
blockType: 'content',
|
||||
id: '456',
|
||||
columns: [
|
||||
{
|
||||
id: '789',
|
||||
richText: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: 'Column 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const initialData: Partial<Page> = {
|
||||
title: 'Test Page',
|
||||
layout: [block1(), block2],
|
||||
}
|
||||
|
||||
// Add a new block before the populated one
|
||||
// Then check to see that the relationship is still populated
|
||||
const merge2 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...initialData,
|
||||
layout: [block2, block1(true)],
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
// Check that the relationship on the first has been removed
|
||||
// And that the relationship on the second has been populated
|
||||
expect(merge2.layout[0].links).toBeUndefined()
|
||||
expect(merge2.layout[1].links[0].link.reference.value).toMatchObject(testPost)
|
||||
expect(merge2._numberOfRequests).toEqual(1)
|
||||
})
|
||||
|
||||
it('— blocks - adds, reorders, and removes blocks', async () => {
|
||||
const block1ID = '123'
|
||||
const block2ID = '456'
|
||||
|
||||
const initialData: Partial<Page> = {
|
||||
title: 'Test Page',
|
||||
layout: [
|
||||
{
|
||||
blockType: 'cta',
|
||||
id: 'block-1', // use fake ID, this is a new block that is only assigned an ID on the client
|
||||
id: block1ID,
|
||||
richText: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -184,7 +402,7 @@ describe('Collections - Live Preview', () => {
|
||||
},
|
||||
{
|
||||
blockType: 'cta',
|
||||
id: 'block-2', // use fake ID, this is a new block that is only assigned an ID on the client
|
||||
id: block2ID,
|
||||
richText: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -193,30 +411,18 @@ describe('Collections - Live Preview', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
initialData: page,
|
||||
serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
// Check that the blocks have been merged and are in the correct order
|
||||
expect(merge1.layout).toHaveLength(2)
|
||||
const block1 = merge1.layout[0]
|
||||
expect(block1.id).toEqual('block-1')
|
||||
expect(block1.richText[0].text).toEqual('Block 1 (Position 1)')
|
||||
const block2 = merge1.layout[1]
|
||||
expect(block2.id).toEqual('block-2')
|
||||
expect(block2.richText[0].text).toEqual('Block 2 (Position 2)')
|
||||
|
||||
// Reorder the blocks using the same IDs from the previous merge
|
||||
const merge2 = await mergeData({
|
||||
// Reorder the blocks
|
||||
const merge1 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...merge1,
|
||||
...initialData,
|
||||
layout: [
|
||||
{
|
||||
id: block2.id,
|
||||
blockType: 'content',
|
||||
blockType: 'cta',
|
||||
id: block2ID,
|
||||
richText: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -225,8 +431,8 @@ describe('Collections - Live Preview', () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
id: block1.id,
|
||||
blockType: 'content',
|
||||
blockType: 'cta',
|
||||
id: block1ID,
|
||||
richText: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -236,27 +442,29 @@ describe('Collections - Live Preview', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
initialData: merge1,
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
// Check that the blocks have been reordered
|
||||
expect(merge2.layout).toHaveLength(2)
|
||||
expect(merge2.layout[0].id).toEqual(block2.id)
|
||||
expect(merge2.layout[1].id).toEqual(block1.id)
|
||||
expect(merge2.layout[0].richText[0].text).toEqual('Block 2 (Position 1)')
|
||||
expect(merge2.layout[1].richText[0].text).toEqual('Block 1 (Position 2)')
|
||||
expect(merge1.layout).toHaveLength(2)
|
||||
expect(merge1.layout[0].id).toEqual(block2ID)
|
||||
expect(merge1.layout[1].id).toEqual(block1ID)
|
||||
expect(merge1.layout[0].richText[0].text).toEqual('Block 2 (Position 1)')
|
||||
expect(merge1.layout[1].richText[0].text).toEqual('Block 1 (Position 2)')
|
||||
expect(merge1._numberOfRequests).toEqual(0)
|
||||
|
||||
// Remove a block
|
||||
const merge3 = await mergeData({
|
||||
const merge2 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...merge2,
|
||||
...initialData,
|
||||
layout: [
|
||||
{
|
||||
id: block2.id,
|
||||
blockType: 'content',
|
||||
blockType: 'cta',
|
||||
id: block2ID,
|
||||
richText: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -266,28 +474,32 @@ describe('Collections - Live Preview', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
initialData: merge2,
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
// Check that the block has been removed
|
||||
expect(merge3.layout).toHaveLength(1)
|
||||
expect(merge3.layout[0].id).toEqual(block2.id)
|
||||
expect(merge3.layout[0].richText[0].text).toEqual('Block 2 (Position 1)')
|
||||
expect(merge2.layout).toHaveLength(1)
|
||||
expect(merge2.layout[0].id).toEqual(block2ID)
|
||||
expect(merge2.layout[0].richText[0].text).toEqual('Block 2 (Position 1)')
|
||||
expect(merge2._numberOfRequests).toEqual(0)
|
||||
|
||||
// Remove the last block to ensure that all blocks can be cleared
|
||||
const merge4 = await mergeData({
|
||||
const merge3 = await mergeData({
|
||||
depth: 1,
|
||||
fieldSchema: schemaJSON,
|
||||
incomingData: {
|
||||
...merge3,
|
||||
...initialData,
|
||||
layout: [],
|
||||
},
|
||||
initialData: merge3,
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
})
|
||||
|
||||
// Check that the block has been removed
|
||||
expect(merge4.layout).toHaveLength(0)
|
||||
expect(merge3.layout).toHaveLength(0)
|
||||
expect(merge3._numberOfRequests).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@ const blockComponents = {
|
||||
}
|
||||
|
||||
export const Blocks: React.FC<{
|
||||
blocks: (Page['layout'][0] | RelatedPostsProps)[]
|
||||
blocks?: (Page['layout'][0] | RelatedPostsProps)[]
|
||||
disableTopPadding?: boolean
|
||||
}> = (props) => {
|
||||
const { disableTopPadding, blocks } = props
|
||||
|
||||
@@ -38,8 +38,8 @@ export const Image: React.FC<MediaProps> = (props) => {
|
||||
alt: altFromResource,
|
||||
} = resource
|
||||
|
||||
width = fullWidth
|
||||
height = fullHeight
|
||||
width = fullWidth || undefined
|
||||
height = fullHeight || undefined
|
||||
alt = altFromResource
|
||||
|
||||
const filename = fullFilename
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { Page } from '../../../payload/payload-types'
|
||||
import { Page } from '../../../payload-types'
|
||||
import { Gutter } from '../../_components/Gutter'
|
||||
import { CMSLink } from '../../_components/Link'
|
||||
import { Media } from '../../_components/Media'
|
||||
import RichText from '../../_components/RichText'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const HighImpactHero: React.FC<Page['hero']> = ({ richText, media, links }) => {
|
||||
export const HighImpactHero: React.FC<Page['hero']> = ({ richText, media }) => {
|
||||
return (
|
||||
<Gutter className={classes.hero}>
|
||||
<div className={classes.content}>
|
||||
<RichText content={richText} />
|
||||
{Array.isArray(links) && links.length > 0 && (
|
||||
<ul className={classes.links}>
|
||||
{links.map(({ link }, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<CMSLink {...link} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.media}>
|
||||
{typeof media === 'object' && (
|
||||
{typeof media === 'object' && media !== null && (
|
||||
<Fragment>
|
||||
<Media
|
||||
resource={media}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Page } from '../../../payload/payload-types'
|
||||
import { Page } from '../../../payload-types'
|
||||
import { Gutter } from '../../_components/Gutter'
|
||||
import RichText from '../../_components/RichText'
|
||||
import { VerticalPadding } from '../../_components/VerticalPadding'
|
||||
|
||||
@@ -26,13 +26,13 @@ export interface User {
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
email: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
salt?: string
|
||||
hash?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
password: string
|
||||
resetPasswordToken?: string | null
|
||||
resetPasswordExpiration?: string | null
|
||||
salt?: string | null
|
||||
hash?: string | null
|
||||
loginAttempts?: number | null
|
||||
lockUntil?: string | null
|
||||
password: string | null
|
||||
}
|
||||
export interface Page {
|
||||
id: string
|
||||
@@ -40,126 +40,150 @@ export interface Page {
|
||||
title: string
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact'
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
media: string | Media
|
||||
}
|
||||
layout?: (
|
||||
richText?:
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
links?: {
|
||||
| null
|
||||
media?: string | Media | null
|
||||
}
|
||||
layout?:
|
||||
| (
|
||||
| {
|
||||
invertBackground?: boolean | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
| null
|
||||
links?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'primary' | 'secondary'
|
||||
appearance?: ('primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'cta'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
columns?: {
|
||||
size?: 'oneThird' | 'half' | 'twoThirds' | 'full'
|
||||
richText?: {
|
||||
invertBackground?: boolean | null
|
||||
columns?:
|
||||
| {
|
||||
size?: ('oneThird' | 'half' | 'twoThirds' | 'full') | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
enableLink?: boolean
|
||||
| null
|
||||
enableLink?: boolean | null
|
||||
link?: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'content'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
position?: 'default' | 'fullscreen'
|
||||
invertBackground?: boolean | null
|
||||
position?: ('default' | 'fullscreen') | null
|
||||
media: string | Media
|
||||
id?: string
|
||||
blockName?: string
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'mediaBlock'
|
||||
}
|
||||
| {
|
||||
introContent?: {
|
||||
introContent?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
populateBy?: 'collection' | 'selection'
|
||||
relationTo?: 'posts'
|
||||
categories?: string[] | Category[]
|
||||
limit?: number
|
||||
| null
|
||||
populateBy?: ('collection' | 'selection') | null
|
||||
relationTo?: 'posts' | null
|
||||
categories?: (string | Category)[] | null
|
||||
limit?: number | null
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
value: string | Post
|
||||
}[]
|
||||
| null
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
value: string | Post
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
}[]
|
||||
populatedDocsTotal?: number
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
populatedDocsTotal?: number | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'archive'
|
||||
}
|
||||
)[]
|
||||
| null
|
||||
meta?: {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string | Media
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
image?: string | Media | null
|
||||
}
|
||||
relationshipPolyHasMany?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
}[]
|
||||
| null
|
||||
relationshipMonoHasMany?: (string | Post)[] | null
|
||||
relationshipMonoHasOne?: (string | null) | Post
|
||||
arrayOfRelationships?:
|
||||
| {
|
||||
relationshipWithinArray?: (string | null) | Post
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
export interface Media {
|
||||
id: string
|
||||
alt: string
|
||||
caption?: {
|
||||
caption?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
url?: string
|
||||
filename?: string
|
||||
mimeType?: string
|
||||
filesize?: number
|
||||
width?: number
|
||||
height?: number
|
||||
url?: string | null
|
||||
filename?: string | null
|
||||
mimeType?: string | null
|
||||
filesize?: number | null
|
||||
width?: number | null
|
||||
height?: number | null
|
||||
}
|
||||
export interface Category {
|
||||
id: string
|
||||
title?: string
|
||||
title?: string | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
@@ -169,105 +193,113 @@ export interface Post {
|
||||
title: string
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact'
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
media: string | Media
|
||||
}
|
||||
layout?: (
|
||||
richText?:
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
links?: {
|
||||
| null
|
||||
media?: string | Media | null
|
||||
}
|
||||
layout?:
|
||||
| (
|
||||
| {
|
||||
invertBackground?: boolean | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
| null
|
||||
links?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'primary' | 'secondary'
|
||||
appearance?: ('primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'cta'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
columns?: {
|
||||
size?: 'oneThird' | 'half' | 'twoThirds' | 'full'
|
||||
richText?: {
|
||||
invertBackground?: boolean | null
|
||||
columns?:
|
||||
| {
|
||||
size?: ('oneThird' | 'half' | 'twoThirds' | 'full') | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
enableLink?: boolean
|
||||
| null
|
||||
enableLink?: boolean | null
|
||||
link?: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'content'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
position?: 'default' | 'fullscreen'
|
||||
invertBackground?: boolean | null
|
||||
position?: ('default' | 'fullscreen') | null
|
||||
media: string | Media
|
||||
id?: string
|
||||
blockName?: string
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'mediaBlock'
|
||||
}
|
||||
| {
|
||||
introContent?: {
|
||||
introContent?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
populateBy?: 'collection' | 'selection'
|
||||
relationTo?: 'posts'
|
||||
categories?: string[] | Category[]
|
||||
limit?: number
|
||||
| null
|
||||
populateBy?: ('collection' | 'selection') | null
|
||||
relationTo?: 'posts' | null
|
||||
categories?: (string | Category)[] | null
|
||||
limit?: number | null
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
value: string | Post
|
||||
}[]
|
||||
| null
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
value: string | Post
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
}[]
|
||||
populatedDocsTotal?: number
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
populatedDocsTotal?: number | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'archive'
|
||||
}
|
||||
)[]
|
||||
relatedPosts?: string[] | Post[]
|
||||
| null
|
||||
relatedPosts?: (string | Post)[] | null
|
||||
meta?: {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string | Media
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
image?: string | Media | null
|
||||
}
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
@@ -278,7 +310,7 @@ export interface PayloadPreference {
|
||||
relationTo: 'users'
|
||||
value: string | User
|
||||
}
|
||||
key?: string
|
||||
key?: string | null
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
@@ -293,48 +325,52 @@ export interface PayloadPreference {
|
||||
}
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string
|
||||
batch?: number
|
||||
name?: string | null
|
||||
batch?: number | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
export interface Header {
|
||||
id: string
|
||||
navItems?: {
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
updatedAt?: string
|
||||
createdAt?: string
|
||||
| null
|
||||
updatedAt?: string | null
|
||||
createdAt?: string | null
|
||||
}
|
||||
export interface Footer {
|
||||
id: string
|
||||
navItems?: {
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
updatedAt?: string
|
||||
createdAt?: string
|
||||
| null
|
||||
updatedAt?: string | null
|
||||
createdAt?: string | null
|
||||
}
|
||||
|
||||
declare module 'payload' {
|
||||
|
||||
@@ -26,13 +26,13 @@ export interface User {
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
email: string
|
||||
resetPasswordToken?: string
|
||||
resetPasswordExpiration?: string
|
||||
salt?: string
|
||||
hash?: string
|
||||
loginAttempts?: number
|
||||
lockUntil?: string
|
||||
password: string
|
||||
resetPasswordToken?: string | null
|
||||
resetPasswordExpiration?: string | null
|
||||
salt?: string | null
|
||||
hash?: string | null
|
||||
loginAttempts?: number | null
|
||||
lockUntil?: string | null
|
||||
password: string | null
|
||||
}
|
||||
export interface Page {
|
||||
id: string
|
||||
@@ -40,128 +40,156 @@ export interface Page {
|
||||
title: string
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact'
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
media: string | Media
|
||||
}
|
||||
layout?: (
|
||||
richText?:
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
links?: {
|
||||
| null
|
||||
media?: string | Media | null
|
||||
}
|
||||
layout?:
|
||||
| (
|
||||
| {
|
||||
invertBackground?: boolean | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
| null
|
||||
links?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null)
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'primary' | 'secondary'
|
||||
appearance?: ('primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'cta'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
columns?: {
|
||||
size?: 'oneThird' | 'half' | 'twoThirds' | 'full'
|
||||
richText?: {
|
||||
invertBackground?: boolean | null
|
||||
columns?:
|
||||
| {
|
||||
size?: ('oneThird' | 'half' | 'twoThirds' | 'full') | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
enableLink?: boolean
|
||||
| null
|
||||
enableLink?: boolean | null
|
||||
link?: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null)
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'content'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
position?: 'default' | 'fullscreen'
|
||||
invertBackground?: boolean | null
|
||||
position?: ('default' | 'fullscreen') | null
|
||||
media: string | Media
|
||||
id?: string
|
||||
blockName?: string
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'mediaBlock'
|
||||
}
|
||||
| {
|
||||
introContent?: {
|
||||
introContent?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
populateBy?: 'collection' | 'selection'
|
||||
relationTo?: 'posts'
|
||||
categories?: string[] | Category[]
|
||||
limit?: number
|
||||
| null
|
||||
populateBy?: ('collection' | 'selection') | null
|
||||
relationTo?: 'posts' | null
|
||||
categories?: (string | Category)[] | null
|
||||
limit?: number | null
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
value: string | Post
|
||||
}[]
|
||||
| null
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
value: string | Post
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
}[]
|
||||
populatedDocsTotal?: number
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
populatedDocsTotal?: number | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'archive'
|
||||
}
|
||||
)[]
|
||||
| null
|
||||
meta?: {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string | Media
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
image?: string | Media | null
|
||||
}
|
||||
relationshipPolyHasMany?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
}[]
|
||||
| null
|
||||
relationshipMonoHasMany?: (string | Post)[] | null
|
||||
relationshipMonoHasOne?: (string | null) | Post
|
||||
arrayOfRelationships?:
|
||||
| {
|
||||
relationshipWithinArray?: (string | null) | Post
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
export interface Media {
|
||||
id: string
|
||||
alt: string
|
||||
caption?: {
|
||||
caption?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
url?: string
|
||||
filename?: string
|
||||
mimeType?: string
|
||||
filesize?: number
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
export interface Category {
|
||||
id: string
|
||||
title?: string
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
url?: string | null
|
||||
filename?: string | null
|
||||
mimeType?: string | null
|
||||
filesize?: number | null
|
||||
width?: number | null
|
||||
height?: number | null
|
||||
}
|
||||
export interface Post {
|
||||
id: string
|
||||
@@ -169,116 +197,140 @@ export interface Post {
|
||||
title: string
|
||||
hero: {
|
||||
type: 'none' | 'highImpact' | 'lowImpact'
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
media: string | Media
|
||||
}
|
||||
layout?: (
|
||||
richText?:
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
richText?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
links?: {
|
||||
| null
|
||||
media?: string | Media | null
|
||||
}
|
||||
layout?:
|
||||
| (
|
||||
| {
|
||||
invertBackground?: boolean | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
| null
|
||||
links?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null)
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'primary' | 'secondary'
|
||||
appearance?: ('primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'cta'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
columns?: {
|
||||
size?: 'oneThird' | 'half' | 'twoThirds' | 'full'
|
||||
richText?: {
|
||||
invertBackground?: boolean | null
|
||||
columns?:
|
||||
| {
|
||||
size?: ('oneThird' | 'half' | 'twoThirds' | 'full') | null
|
||||
richText?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
enableLink?: boolean
|
||||
| null
|
||||
enableLink?: boolean | null
|
||||
link?: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null)
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'content'
|
||||
}
|
||||
| {
|
||||
invertBackground?: boolean
|
||||
position?: 'default' | 'fullscreen'
|
||||
invertBackground?: boolean | null
|
||||
position?: ('default' | 'fullscreen') | null
|
||||
media: string | Media
|
||||
id?: string
|
||||
blockName?: string
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'mediaBlock'
|
||||
}
|
||||
| {
|
||||
introContent?: {
|
||||
introContent?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
populateBy?: 'collection' | 'selection'
|
||||
relationTo?: 'posts'
|
||||
categories?: string[] | Category[]
|
||||
limit?: number
|
||||
| null
|
||||
populateBy?: ('collection' | 'selection') | null
|
||||
relationTo?: 'posts' | null
|
||||
categories?: (string | Category)[] | null
|
||||
limit?: number | null
|
||||
selectedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
value: string | Post
|
||||
}[]
|
||||
| null
|
||||
populatedDocs?:
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: string
|
||||
value: string | Post
|
||||
}[]
|
||||
| {
|
||||
relationTo: 'posts'
|
||||
value: Post
|
||||
}[]
|
||||
populatedDocsTotal?: number
|
||||
id?: string
|
||||
blockName?: string
|
||||
| null
|
||||
populatedDocsTotal?: number | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'archive'
|
||||
}
|
||||
)[]
|
||||
relatedPosts?: string[] | Post[]
|
||||
| null
|
||||
relatedPosts?: (string | Post)[] | null
|
||||
meta?: {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string | Media
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
image?: string | Media | null
|
||||
}
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
export interface Category {
|
||||
id: string
|
||||
title?: string | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
export interface PayloadPreference {
|
||||
id: string
|
||||
user: {
|
||||
relationTo: 'users'
|
||||
value: string | User
|
||||
}
|
||||
key?: string
|
||||
key?: string | null
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
@@ -293,48 +345,62 @@ export interface PayloadPreference {
|
||||
}
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string
|
||||
batch?: number
|
||||
name?: string | null
|
||||
batch?: number | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
export interface Header {
|
||||
id: string
|
||||
navItems?: {
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null)
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
updatedAt?: string
|
||||
createdAt?: string
|
||||
| null
|
||||
updatedAt?: string | null
|
||||
createdAt?: string | null
|
||||
}
|
||||
export interface Footer {
|
||||
id: string
|
||||
navItems?: {
|
||||
navItems?:
|
||||
| {
|
||||
link: {
|
||||
type?: 'reference' | 'custom'
|
||||
newTab?: boolean
|
||||
reference: {
|
||||
type?: ('reference' | 'custom') | null
|
||||
newTab?: boolean | null
|
||||
reference?:
|
||||
| ({
|
||||
relationTo: 'posts'
|
||||
value: string | Post
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages'
|
||||
value: string | Page
|
||||
}
|
||||
url: string
|
||||
} | null)
|
||||
url?: string | null
|
||||
label: string
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
appearance?: ('default' | 'primary' | 'secondary') | null
|
||||
}
|
||||
id?: string
|
||||
id?: string | null
|
||||
}[]
|
||||
updatedAt?: string
|
||||
createdAt?: string
|
||||
| null
|
||||
updatedAt?: string | null
|
||||
createdAt?: string | null
|
||||
}
|
||||
|
||||
declare module 'payload' {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const home: Page = {
|
||||
export const home: Omit<Page, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||
slug: 'home',
|
||||
title: 'Home',
|
||||
id: '',
|
||||
meta: {
|
||||
description: 'This is an example of live preview on a page.',
|
||||
},
|
||||
@@ -94,4 +93,12 @@ export const home: Page = {
|
||||
],
|
||||
},
|
||||
],
|
||||
relationshipMonoHasMany: ['{{POST_1_ID}}'],
|
||||
relationshipMonoHasOne: '{{POST_1_ID}}',
|
||||
relationshipPolyHasMany: [{ relationTo: 'posts', value: '{{POST_1_ID}}' }],
|
||||
arrayOfRelationships: [
|
||||
{
|
||||
relationshipWithinArray: '{{POST_1_ID}}',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Post } from '../payload-types'
|
||||
export const post1: Partial<Post> = {
|
||||
export const post1: Omit<Post, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||
title: 'Post 1',
|
||||
slug: 'post-1',
|
||||
meta: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Post } from '../payload-types'
|
||||
|
||||
export const post2: Partial<Post> = {
|
||||
export const post2: Omit<Post, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||
title: 'Post 2',
|
||||
slug: 'post-2',
|
||||
meta: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Post } from '../payload-types'
|
||||
|
||||
export const post3: Partial<Post> = {
|
||||
export const post3: Omit<Post, 'createdAt' | 'id' | 'updatedAt'> = {
|
||||
title: 'Post 3',
|
||||
slug: 'post-3',
|
||||
meta: {
|
||||
|
||||
Reference in New Issue
Block a user