Compare commits
1 Commits
fix/graphq
...
fix/5285/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a44857a2ee |
@@ -81,7 +81,7 @@ To install a Database Adapter, you can run **one** of the following commands:
|
||||
|
||||
#### 2. Copy Payload files into your Next.js app folder
|
||||
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/%28payload%29) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](<https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)>) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
|
||||
@@ -37,12 +37,11 @@ import {
|
||||
GraphQLString,
|
||||
} from 'graphql'
|
||||
import { flattenTopLevelFields, toWords } from 'payload'
|
||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||
import { fieldAffectsData, optionIsObject, tabHasName } from 'payload/shared'
|
||||
|
||||
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
|
||||
import { combineParentName } from '../utilities/combineParentName.js'
|
||||
import { formatName } from '../utilities/formatName.js'
|
||||
import { formatOptions } from '../utilities/formatOptions.js'
|
||||
import { groupOrTabHasRequiredSubfield } from '../utilities/groupOrTabHasRequiredSubfield.js'
|
||||
import { withNullableType } from './withNullableType.js'
|
||||
|
||||
@@ -210,18 +209,12 @@ export function buildMutationInputType({
|
||||
}),
|
||||
},
|
||||
}),
|
||||
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => {
|
||||
const type = new GraphQLEnumType({
|
||||
name: `${combineParentName(parentName, field.name)}_MutationInput`,
|
||||
values: formatOptions(field),
|
||||
})
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}
|
||||
},
|
||||
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
relationship: (inputObjectTypeConfig: InputObjectTypeConfig, field: RelationshipField) => {
|
||||
const { relationTo } = field
|
||||
type PayloadGraphQLRelationshipType =
|
||||
@@ -285,7 +278,23 @@ export function buildMutationInputType({
|
||||
const formattedName = `${combineParentName(parentName, field.name)}_MutationInput`
|
||||
let type: GraphQLType = new GraphQLEnumType({
|
||||
name: formattedName,
|
||||
values: formatOptions(field),
|
||||
values: field.options.reduce((values, option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return {
|
||||
...values,
|
||||
[formatName(option.value)]: {
|
||||
value: option.value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...values,
|
||||
[formatName(option)]: {
|
||||
value: option,
|
||||
},
|
||||
}
|
||||
}, {}),
|
||||
})
|
||||
|
||||
type = field.hasMany ? new GraphQLList(type) : type
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
GraphQLString,
|
||||
} from 'graphql'
|
||||
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
|
||||
import { optionIsObject } from 'payload/shared'
|
||||
|
||||
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
|
||||
import { combineParentName } from '../utilities/combineParentName.js'
|
||||
import { formatOptions } from '../utilities/formatOptions.js'
|
||||
import { formatName } from '../utilities/formatName.js'
|
||||
import { operators } from './operators.js'
|
||||
|
||||
type staticTypes =
|
||||
@@ -146,7 +147,23 @@ const defaults: DefaultsType = {
|
||||
type: (field: RadioField, parentName): GraphQLType =>
|
||||
new GraphQLEnumType({
|
||||
name: `${combineParentName(parentName, field.name)}_Input`,
|
||||
values: formatOptions(field),
|
||||
values: field.options.reduce((values, option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return {
|
||||
...values,
|
||||
[formatName(option.value)]: {
|
||||
value: option.value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...values,
|
||||
[formatName(option)]: {
|
||||
value: option,
|
||||
},
|
||||
}
|
||||
}, {}),
|
||||
}),
|
||||
})),
|
||||
],
|
||||
@@ -174,7 +191,23 @@ const defaults: DefaultsType = {
|
||||
type: (field: SelectField, parentName): GraphQLType =>
|
||||
new GraphQLEnumType({
|
||||
name: `${combineParentName(parentName, field.name)}_Input`,
|
||||
values: formatOptions(field),
|
||||
values: field.options.reduce((values, option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return {
|
||||
...values,
|
||||
[formatName(option.value)]: {
|
||||
value: option.value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...values,
|
||||
[formatName(option)]: {
|
||||
value: option,
|
||||
},
|
||||
}
|
||||
}, {}),
|
||||
}),
|
||||
})),
|
||||
],
|
||||
|
||||
@@ -1,56 +1,23 @@
|
||||
import type { GraphQLEnumValueConfigMap } from 'graphql'
|
||||
import type { RadioField, SelectField } from 'payload'
|
||||
|
||||
import { formatName } from './formatName.js'
|
||||
|
||||
/**
|
||||
* Convert an arbitrary string into a valid GraphQL enum *name* (token).
|
||||
* Keeps formatName’s backwards-compat behavior, then enforces GraphQL rules.
|
||||
*/
|
||||
const sanitizeEnumName = (value: string) => {
|
||||
let key = formatName(value)
|
||||
.replace(/\W/g, '_') // non-word chars → underscore
|
||||
.replace(/_+/g, '_') // collapse repeated underscores
|
||||
.replace(/^_+|_+$/g, '') // trim leading/trailing underscores
|
||||
|
||||
// GraphQL names must start with a letter or underscore
|
||||
if (!/^[A-Z_]/i.test(key)) {
|
||||
key = `_${key}`
|
||||
}
|
||||
|
||||
return key || '_'
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a GraphQL enum value map from select/radio options.
|
||||
* - Keys: safe GraphQL enum names
|
||||
* - Values: original option values (string/number/boolean)
|
||||
* - Deterministically disambiguates collisions by suffixing _2, _3, ...
|
||||
*/
|
||||
|
||||
export const formatOptions = (field: RadioField | SelectField): GraphQLEnumValueConfigMap => {
|
||||
const enumValueMap: GraphQLEnumValueConfigMap = {}
|
||||
const nameCounts = new Map<string, number>()
|
||||
|
||||
const optionsArray = Array.isArray(field.options) ? field.options : []
|
||||
|
||||
for (const option of optionsArray) {
|
||||
const rawValue = typeof option === 'object' ? String(option.value) : String(option)
|
||||
|
||||
let enumName = sanitizeEnumName(rawValue)
|
||||
|
||||
// De-duplicate if multiple raw values sanitize to the same enum name
|
||||
const nextCount = (nameCounts.get(enumName) ?? 0) + 1
|
||||
|
||||
nameCounts.set(enumName, nextCount)
|
||||
if (nextCount > 1) {
|
||||
enumName = `${enumName}_${nextCount}`
|
||||
export const formatOptions = (field: RadioField | SelectField) => {
|
||||
return field.options.reduce((values, option) => {
|
||||
if (typeof option === 'object') {
|
||||
return {
|
||||
...values,
|
||||
[formatName(option.value)]: {
|
||||
value: option.value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
enumValueMap[enumName] = {
|
||||
value: typeof option === 'object' ? option.value : option,
|
||||
return {
|
||||
...values,
|
||||
[formatName(option)]: {
|
||||
value: option,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return enumValueMap
|
||||
}, {})
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ export const renderDocument = async ({
|
||||
globalSlug,
|
||||
locale: locale?.code,
|
||||
operation,
|
||||
readOnly: isTrashedDoc || isLocked,
|
||||
readOnly: isTrashedDoc,
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionSlug || globalSlug,
|
||||
|
||||
@@ -11,7 +11,6 @@ export type Data = {
|
||||
}
|
||||
|
||||
export type Row = {
|
||||
addedByServer?: FieldState['addedByServer']
|
||||
blockType?: string
|
||||
collapsed?: boolean
|
||||
customComponents?: {
|
||||
|
||||
@@ -35,10 +35,6 @@ export type JobStats = {
|
||||
export const getJobStatsGlobal: (config: Config) => GlobalConfig = (config) => {
|
||||
return {
|
||||
slug: jobStatsGlobalSlug,
|
||||
admin: {
|
||||
group: 'System',
|
||||
hidden: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'stats',
|
||||
|
||||
@@ -9,7 +9,14 @@ type Props = {
|
||||
label: MultiTenantPluginConfig['tenantSelectorLabel']
|
||||
} & ServerProps
|
||||
export const TenantSelector = (props: Props) => {
|
||||
const { label, viewType } = props
|
||||
const { enabledSlugs, label, params, viewType } = props
|
||||
const enabled = Boolean(
|
||||
params?.segments &&
|
||||
Array.isArray(params.segments) &&
|
||||
params.segments[0] === 'collections' &&
|
||||
params.segments[1] &&
|
||||
enabledSlugs.includes(params.segments[1]),
|
||||
)
|
||||
|
||||
return <TenantSelectorClient label={label} viewType={viewType} />
|
||||
return <TenantSelectorClient disabled={!enabled} label={label} viewType={viewType} />
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type Args = {
|
||||
view: ViewTypes
|
||||
}
|
||||
export async function getGlobalViewRedirect({
|
||||
slug: collectionSlug,
|
||||
slug,
|
||||
basePath,
|
||||
docID,
|
||||
headers,
|
||||
@@ -64,67 +64,45 @@ export async function getGlobalViewRedirect({
|
||||
tenant = tenantOptions[0]?.value || null
|
||||
}
|
||||
|
||||
if (tenant) {
|
||||
try {
|
||||
const globalTenantDocQuery = await payload.find({
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
select: {
|
||||
id: true,
|
||||
try {
|
||||
const { docs } = await payload.find({
|
||||
collection: slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
overrideAccess: false,
|
||||
pagination: false,
|
||||
user,
|
||||
where: {
|
||||
[tenantFieldName]: {
|
||||
equals: tenant,
|
||||
},
|
||||
where: {
|
||||
[tenantFieldName]: {
|
||||
equals: tenant,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const globalTenantDocID = globalTenantDocQuery?.docs?.[0]?.id
|
||||
|
||||
if (view === 'document') {
|
||||
// global tenant document edit view
|
||||
if (globalTenantDocID && docID !== globalTenantDocID) {
|
||||
// tenant document already exists but does not match current route docID
|
||||
// redirect to matching tenant docID from query
|
||||
redirectRoute = `/collections/${collectionSlug}/${globalTenantDocID}`
|
||||
} else if (docID && !globalTenantDocID) {
|
||||
// a docID was found in the route but no global document with this tenant exists
|
||||
// so we need to generate a redirect to the create route
|
||||
redirectRoute = await generateCreateRedirect({
|
||||
collectionSlug,
|
||||
payload,
|
||||
tenantID: tenant,
|
||||
})
|
||||
}
|
||||
} else if (view === 'list') {
|
||||
// global tenant document list view
|
||||
if (globalTenantDocID) {
|
||||
// tenant document exists, redirect from list view to the document edit view
|
||||
redirectRoute = `/collections/${collectionSlug}/${globalTenantDocID}`
|
||||
} else {
|
||||
// no matching document was found for the current tenant
|
||||
// so we need to generate a redirect to the create route
|
||||
redirectRoute = await generateCreateRedirect({
|
||||
collectionSlug,
|
||||
payload,
|
||||
tenantID: tenant,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const prefix = `${e && typeof e === 'object' && 'message' in e && typeof e.message === 'string' ? `${e.message} - ` : ''}`
|
||||
payload.logger.error(e, `${prefix}Multi Tenant Redirect Error`)
|
||||
}
|
||||
} else {
|
||||
// no tenants were found, redirect to the admin view
|
||||
return formatAdminURL({
|
||||
adminRoute: payload.config.routes.admin,
|
||||
basePath,
|
||||
path: '',
|
||||
serverURL: payload.config.serverURL,
|
||||
},
|
||||
})
|
||||
|
||||
const tenantDocID = docs?.[0]?.id
|
||||
|
||||
if (view === 'document') {
|
||||
if (docID && !tenantDocID) {
|
||||
// viewing a document with an id but does not match the selected tenant, redirect to create route
|
||||
redirectRoute = `/collections/${slug}/create`
|
||||
} else if (tenantDocID && docID !== tenantDocID) {
|
||||
// tenant document already exists but does not match current route doc ID, redirect to matching tenant doc
|
||||
redirectRoute = `/collections/${slug}/${tenantDocID}`
|
||||
}
|
||||
} else if (view === 'list') {
|
||||
if (tenantDocID) {
|
||||
// tenant document exists, redirect to edit view
|
||||
redirectRoute = `/collections/${slug}/${tenantDocID}`
|
||||
} else {
|
||||
// tenant document does not exist, redirect to create route
|
||||
redirectRoute = `/collections/${slug}/create`
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
payload.logger.error(
|
||||
e,
|
||||
`${typeof e === 'object' && e && 'message' in e ? `e?.message - ` : ''}Multi Tenant Redirect Error`,
|
||||
)
|
||||
}
|
||||
|
||||
if (redirectRoute) {
|
||||
@@ -136,57 +114,5 @@ export async function getGlobalViewRedirect({
|
||||
})
|
||||
}
|
||||
|
||||
// no redirect is needed
|
||||
// the current route is valid
|
||||
return undefined
|
||||
}
|
||||
|
||||
type GenerateCreateArgs = {
|
||||
collectionSlug: string
|
||||
payload: Payload
|
||||
tenantID: number | string
|
||||
}
|
||||
/**
|
||||
* Generate a redirect URL for creating a new document in a multi-tenant collection.
|
||||
*
|
||||
* If autosave is enabled on the collection, we need to create the document and then redirect to it.
|
||||
* Otherwise we can redirect to the default create route.
|
||||
*/
|
||||
async function generateCreateRedirect({
|
||||
collectionSlug,
|
||||
payload,
|
||||
tenantID,
|
||||
}: GenerateCreateArgs): Promise<`/${string}` | undefined> {
|
||||
const collection = payload.collections[collectionSlug]
|
||||
if (
|
||||
collection?.config.versions?.drafts &&
|
||||
typeof collection.config.versions.drafts === 'object' &&
|
||||
collection.config.versions.drafts.autosave
|
||||
) {
|
||||
// Autosave is enabled, create a document first
|
||||
try {
|
||||
const doc = await payload.create({
|
||||
collection: collectionSlug,
|
||||
data: {
|
||||
tenant: tenantID,
|
||||
},
|
||||
depth: 0,
|
||||
draft: true,
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
return `/collections/${collectionSlug}/${doc.id}`
|
||||
} catch (error) {
|
||||
payload.logger.error(
|
||||
error,
|
||||
`Error creating autosave global multi tenant document for ${collectionSlug}`,
|
||||
)
|
||||
}
|
||||
|
||||
return '/'
|
||||
}
|
||||
|
||||
// Autosave is not enabled, redirect to default create route
|
||||
return `/collections/${collectionSlug}/create`
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const getTenantOptions = async ({
|
||||
overrideAccess: false,
|
||||
select: {
|
||||
[useAsTitle]: true,
|
||||
...(isOrderable && { _order: true }),
|
||||
...(isOrderable ? { _order: true } : {}),
|
||||
},
|
||||
sort: isOrderable ? '_order' : useAsTitle,
|
||||
user,
|
||||
|
||||
@@ -89,38 +89,30 @@ export const mergeServerFormState = ({
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply merge the rows array to ensure changes to local state are not lost while the request was pending
|
||||
* Intelligently merge the rows array to ensure changes to local state are not lost while the request was pending
|
||||
* For example, the server response could come back with a row which has been deleted on the client
|
||||
* Loop over the incoming rows, if it exists in client side form state, merge in any new properties from the server
|
||||
* Note: read `currentState` and not `newState` here, as the `rows` property have already been merged above
|
||||
*/
|
||||
if (Array.isArray(incomingField.rows) && path in currentState) {
|
||||
newState[path].rows = [...(currentState[path]?.rows || [])] // shallow copy to avoid mutating the original array
|
||||
if (Array.isArray(incomingField.rows)) {
|
||||
if (acceptValues === true) {
|
||||
newState[path].rows = incomingField.rows
|
||||
} else if (path in currentState) {
|
||||
newState[path].rows = [...(currentState[path]?.rows || [])] // shallow copy to avoid mutating the original array
|
||||
|
||||
incomingField.rows.forEach((row) => {
|
||||
const indexInCurrentState = currentState[path].rows?.findIndex(
|
||||
(existingRow) => existingRow.id === row.id,
|
||||
)
|
||||
incomingField.rows.forEach((row) => {
|
||||
const indexInCurrentState = currentState[path].rows?.findIndex(
|
||||
(existingRow) => existingRow.id === row.id,
|
||||
)
|
||||
|
||||
if (indexInCurrentState > -1) {
|
||||
newState[path].rows[indexInCurrentState] = {
|
||||
...currentState[path].rows[indexInCurrentState],
|
||||
...row,
|
||||
if (indexInCurrentState > -1) {
|
||||
newState[path].rows[indexInCurrentState] = {
|
||||
...currentState[path].rows[indexInCurrentState],
|
||||
...row,
|
||||
}
|
||||
}
|
||||
} else if (row.addedByServer) {
|
||||
/**
|
||||
* Note: This is a known limitation of computed array and block rows
|
||||
* If a new row was added by the server, we append it to the _end_ of this array
|
||||
* This is because the client is the source of truth, and it has arrays ordered in a certain position
|
||||
* For example, the user may have re-ordered rows client-side while a long running request is processing
|
||||
* This means that we _cannot_ slice a new row into the second position on the server, for example
|
||||
* By the time it gets back to the client, its index is stale
|
||||
*/
|
||||
const newRow = { ...row }
|
||||
delete newRow.addedByServer
|
||||
newState[path].rows.push(newRow)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If `valid` is `undefined`, mark it as `true`
|
||||
|
||||
@@ -352,10 +352,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
newRow.lastRenderedPath = previousRow.lastRenderedPath
|
||||
}
|
||||
|
||||
// add addedByServer flag
|
||||
if (!previousRow) {
|
||||
newRow.addedByServer = true
|
||||
}
|
||||
acc.rows.push(newRow)
|
||||
|
||||
const isCollapsed = isRowCollapsed({
|
||||
collapsedPrefs: preferences?.fields?.[path]?.collapsed,
|
||||
@@ -365,11 +362,9 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
})
|
||||
|
||||
if (isCollapsed) {
|
||||
newRow.collapsed = true
|
||||
acc.rows[acc.rows.length - 1].collapsed = true
|
||||
}
|
||||
|
||||
acc.rows.push(newRow)
|
||||
|
||||
return acc
|
||||
},
|
||||
{
|
||||
|
||||
@@ -168,16 +168,12 @@ const DocumentInfo: React.FC<
|
||||
try {
|
||||
const isGlobal = slug === globalSlug
|
||||
|
||||
const request = await requests.get(`${serverURL}${api}/payload-locked-documents`, {
|
||||
const query = isGlobal
|
||||
? `where[globalSlug][equals]=${slug}`
|
||||
: `where[document.value][equals]=${docID}&where[document.relationTo][equals]=${slug}`
|
||||
|
||||
const request = await requests.get(`${serverURL}${api}/payload-locked-documents?${query}`, {
|
||||
credentials: 'include',
|
||||
params: isGlobal
|
||||
? {
|
||||
'where[globalSlug][equals]': slug,
|
||||
}
|
||||
: {
|
||||
'where[document.relationTo][equals]': slug,
|
||||
'where[document.value][equals]': docID,
|
||||
},
|
||||
})
|
||||
|
||||
const { docs } = await request.json()
|
||||
@@ -205,17 +201,13 @@ const DocumentInfo: React.FC<
|
||||
try {
|
||||
const isGlobal = slug === globalSlug
|
||||
|
||||
const query = isGlobal
|
||||
? `where[globalSlug][equals]=${slug}`
|
||||
: `where[document.value][equals]=${docID}&where[document.relationTo][equals]=${slug}`
|
||||
|
||||
// Check if the document is already locked
|
||||
const request = await requests.get(`${serverURL}${api}/payload-locked-documents`, {
|
||||
const request = await requests.get(`${serverURL}${api}/payload-locked-documents?${query}`, {
|
||||
credentials: 'include',
|
||||
params: isGlobal
|
||||
? {
|
||||
'where[globalSlug][equals]': slug,
|
||||
}
|
||||
: {
|
||||
'where[document.relationTo][equals]': slug,
|
||||
'where[document.value][equals]': docID,
|
||||
},
|
||||
})
|
||||
|
||||
const { docs } = await request.json()
|
||||
|
||||
@@ -191,11 +191,7 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
}
|
||||
}, [modified])
|
||||
|
||||
const showAuthBlock = enableFields
|
||||
const showAPIKeyBlock = useAPIKey && canReadApiKey
|
||||
const showVerifyBlock = verify && isEditing
|
||||
|
||||
if (!(showAuthBlock || showAPIKeyBlock || showVerifyBlock)) {
|
||||
if (disableLocalStrategy && !enableFields && !useAPIKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ export default async function Post({ params: paramsPromise }: Args) {
|
||||
const url = '/posts/' + slug
|
||||
const post = await queryPostBySlug({ slug })
|
||||
|
||||
console.log('post', post)
|
||||
|
||||
if (!post) return <PayloadRedirects url={url} />
|
||||
|
||||
return (
|
||||
@@ -91,6 +93,7 @@ const queryPostBySlug = cache(async ({ slug }: { slug: string }) => {
|
||||
collection: 'posts',
|
||||
draft,
|
||||
limit: 1,
|
||||
// depth: 0, // IF YOU UNCOMMENT THIS, THE BLOCKS WILL NOT BE POPULATED
|
||||
overrideAccess: draft,
|
||||
pagination: false,
|
||||
where: {
|
||||
|
||||
13
templates/website/src/blocks/Relationship/config.ts
Normal file
13
templates/website/src/blocks/Relationship/config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const RelationshipBlock: Block = {
|
||||
slug: 'relationshipBlock',
|
||||
interfaceName: 'RelationshipBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
import { slugField } from '@/fields/slug'
|
||||
import { RelationshipBlock } from '@/blocks/Relationship/config'
|
||||
|
||||
export const Posts: CollectionConfig<'posts'> = {
|
||||
slug: 'posts',
|
||||
@@ -92,7 +93,9 @@ export const Posts: CollectionConfig<'posts'> = {
|
||||
return [
|
||||
...rootFeatures,
|
||||
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
|
||||
BlocksFeature({ blocks: [Banner, Code, MediaBlock] }),
|
||||
BlocksFeature({
|
||||
blocks: [Banner, Code, MediaBlock, RelationshipBlock],
|
||||
}),
|
||||
FixedToolbarFeature(),
|
||||
InlineToolbarFeature(),
|
||||
HorizontalRuleFeature(),
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
BannerBlock as BannerBlockProps,
|
||||
CallToActionBlock as CTABlockProps,
|
||||
MediaBlock as MediaBlockProps,
|
||||
RelationshipBlock,
|
||||
} from '@/payload-types'
|
||||
import { BannerBlock } from '@/blocks/Banner/Component'
|
||||
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||
@@ -24,7 +25,9 @@ import { cn } from '@/utilities/ui'
|
||||
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps>
|
||||
| SerializedBlockNode<
|
||||
CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps | RelationshipBlock
|
||||
>
|
||||
|
||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||
const { value, relationTo } = linkNode.fields.doc!
|
||||
@@ -52,6 +55,13 @@ const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters })
|
||||
),
|
||||
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
|
||||
cta: ({ node }) => <CallToActionBlock {...node.fields} />,
|
||||
relationshipBlock: ({ node }) => {
|
||||
console.log('relationshipBlock', node)
|
||||
const relationship = node.fields.relationship
|
||||
if (typeof relationship !== 'object')
|
||||
return <pre>{JSON.stringify(relationship, null, 2)}</pre>
|
||||
return <pre>{JSON.stringify(relationship?.title, null, 2)}</pre>
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1708,6 +1708,16 @@ export interface CodeBlock {
|
||||
blockName?: string | null;
|
||||
blockType: 'code';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "RelationshipBlock".
|
||||
*/
|
||||
export interface RelationshipBlock {
|
||||
relationship?: (string | null) | Post;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'relationshipBlock';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -261,33 +261,6 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'api-keys-with-field-read-access',
|
||||
auth: {
|
||||
disableLocalStrategy: true,
|
||||
useAPIKey: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'enableAPIKey',
|
||||
type: 'checkbox',
|
||||
access: {
|
||||
read: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
access: {
|
||||
read: () => false,
|
||||
},
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'API Keys With Field Read Access',
|
||||
singular: 'API Key With Field Read Access',
|
||||
},
|
||||
},
|
||||
],
|
||||
onInit: seed,
|
||||
typescript: {
|
||||
|
||||
@@ -335,30 +335,5 @@ describe('Auth', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('api-keys-with-field-read-access', () => {
|
||||
let user
|
||||
|
||||
beforeAll(async () => {
|
||||
url = new AdminUrlUtil(serverURL, 'api-keys-with-field-read-access')
|
||||
|
||||
user = await payload.create({
|
||||
collection: apiKeysSlug,
|
||||
data: {
|
||||
apiKey: uuid(),
|
||||
enableAPIKey: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('should hide auth parent container if api keys enabled but no read access', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
// assert that the auth parent container is hidden
|
||||
await expect(page.locator('.auth-fields')).toBeHidden()
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -68,7 +68,6 @@ export interface Config {
|
||||
'disable-local-strategy-password': DisableLocalStrategyPasswordAuthOperations;
|
||||
'api-keys': ApiKeyAuthOperations;
|
||||
'public-users': PublicUserAuthOperations;
|
||||
'api-keys-with-field-read-access': ApiKeysWithFieldReadAccessAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
@@ -78,7 +77,6 @@ export interface Config {
|
||||
'api-keys': ApiKey;
|
||||
'public-users': PublicUser;
|
||||
relationsCollection: RelationsCollection;
|
||||
'api-keys-with-field-read-access': ApiKeysWithFieldReadAccess;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
@@ -91,7 +89,6 @@ export interface Config {
|
||||
'api-keys': ApiKeysSelect<false> | ApiKeysSelect<true>;
|
||||
'public-users': PublicUsersSelect<false> | PublicUsersSelect<true>;
|
||||
relationsCollection: RelationsCollectionSelect<false> | RelationsCollectionSelect<true>;
|
||||
'api-keys-with-field-read-access': ApiKeysWithFieldReadAccessSelect<false> | ApiKeysWithFieldReadAccessSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -117,9 +114,6 @@ export interface Config {
|
||||
})
|
||||
| (PublicUser & {
|
||||
collection: 'public-users';
|
||||
})
|
||||
| (ApiKeysWithFieldReadAccess & {
|
||||
collection: 'api-keys-with-field-read-access';
|
||||
});
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
@@ -216,24 +210,6 @@ export interface PublicUserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
export interface ApiKeysWithFieldReadAccessAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
@@ -364,18 +340,6 @@ export interface RelationsCollection {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "api-keys-with-field-read-access".
|
||||
*/
|
||||
export interface ApiKeysWithFieldReadAccess {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
enableAPIKey?: boolean | null;
|
||||
apiKey?: string | null;
|
||||
apiKeyIndex?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
@@ -406,10 +370,6 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'relationsCollection';
|
||||
value: string | RelationsCollection;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'api-keys-with-field-read-access';
|
||||
value: string | ApiKeysWithFieldReadAccess;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user:
|
||||
@@ -432,10 +392,6 @@ export interface PayloadLockedDocument {
|
||||
| {
|
||||
relationTo: 'public-users';
|
||||
value: string | PublicUser;
|
||||
}
|
||||
| {
|
||||
relationTo: 'api-keys-with-field-read-access';
|
||||
value: string | ApiKeysWithFieldReadAccess;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -466,10 +422,6 @@ export interface PayloadPreference {
|
||||
| {
|
||||
relationTo: 'public-users';
|
||||
value: string | PublicUser;
|
||||
}
|
||||
| {
|
||||
relationTo: 'api-keys-with-field-read-access';
|
||||
value: string | ApiKeysWithFieldReadAccess;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -624,17 +576,6 @@ export interface RelationsCollectionSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "api-keys-with-field-read-access_select".
|
||||
*/
|
||||
export interface ApiKeysWithFieldReadAccessSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
enableAPIKey?: T;
|
||||
apiKey?: T;
|
||||
apiKeyIndex?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export const ArrayRowLabel = () => {
|
||||
return <p id="custom-array-row-label">This is a custom component</p>
|
||||
return <p>This is a custom component</p>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { FieldState, FormState, Payload, User } from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import path from 'path'
|
||||
@@ -11,7 +10,6 @@ import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { postsSlug } from './collections/Posts/index.js'
|
||||
|
||||
// eslint-disable-next-line payload/no-relative-monorepo-imports
|
||||
import { mergeServerFormState } from '../../packages/ui/src/forms/Form/mergeServerFormState.js'
|
||||
|
||||
@@ -23,14 +21,6 @@ const { email, password } = devUser
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const DummyReactComponent: React.ReactNode = {
|
||||
// @ts-expect-error - can ignore, needs to satisfy `typeof value.$$typeof === 'symbol'`
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type: 'div',
|
||||
props: {},
|
||||
key: null,
|
||||
}
|
||||
|
||||
describe('Form State', () => {
|
||||
beforeAll(async () => {
|
||||
;({ payload, restClient } = await initPayloadInt(dirname, undefined, true))
|
||||
@@ -576,7 +566,7 @@ describe('Form State', () => {
|
||||
expect(newState === currentState).toBe(true)
|
||||
})
|
||||
|
||||
it('should accept all values from the server regardless of local modifications, e.g. `acceptAllValues` on submit', () => {
|
||||
it('should accept all values from the server regardless of local modifications, e.g. on submit', () => {
|
||||
const title: FieldState = {
|
||||
value: 'Test Post (modified on the client)',
|
||||
initialValue: 'Test Post',
|
||||
@@ -587,7 +577,7 @@ describe('Form State', () => {
|
||||
const currentState: Record<string, FieldState> = {
|
||||
title: {
|
||||
...title,
|
||||
isModified: true, // This is critical, this is what we're testing
|
||||
isModified: true,
|
||||
},
|
||||
computedTitle: {
|
||||
value: 'Test Post (computed on the client)',
|
||||
@@ -595,31 +585,6 @@ describe('Form State', () => {
|
||||
valid: true,
|
||||
passesCondition: true,
|
||||
},
|
||||
array: {
|
||||
rows: [
|
||||
{
|
||||
id: '1',
|
||||
customComponents: {
|
||||
RowLabel: DummyReactComponent,
|
||||
},
|
||||
lastRenderedPath: 'array.0.customTextField',
|
||||
},
|
||||
],
|
||||
valid: true,
|
||||
passesCondition: true,
|
||||
},
|
||||
'array.0.id': {
|
||||
value: '1',
|
||||
initialValue: '1',
|
||||
valid: true,
|
||||
passesCondition: true,
|
||||
},
|
||||
'array.0.customTextField': {
|
||||
value: 'Test Post (modified on the client)',
|
||||
initialValue: 'Test Post',
|
||||
valid: true,
|
||||
passesCondition: true,
|
||||
},
|
||||
}
|
||||
|
||||
const incomingStateFromServer: Record<string, FieldState> = {
|
||||
@@ -635,29 +600,6 @@ describe('Form State', () => {
|
||||
valid: true,
|
||||
passesCondition: true,
|
||||
},
|
||||
array: {
|
||||
rows: [
|
||||
{
|
||||
id: '1',
|
||||
lastRenderedPath: 'array.0.customTextField',
|
||||
// Omit `customComponents` because the server did not re-render this row
|
||||
},
|
||||
],
|
||||
passesCondition: true,
|
||||
valid: true,
|
||||
},
|
||||
'array.0.id': {
|
||||
value: '1',
|
||||
initialValue: '1',
|
||||
valid: true,
|
||||
passesCondition: true,
|
||||
},
|
||||
'array.0.customTextField': {
|
||||
value: 'Test Post (modified on the client)',
|
||||
initialValue: 'Test Post',
|
||||
valid: true,
|
||||
passesCondition: true,
|
||||
},
|
||||
}
|
||||
|
||||
const newState = mergeServerFormState({
|
||||
@@ -672,14 +614,10 @@ describe('Form State', () => {
|
||||
...incomingStateFromServer.title,
|
||||
isModified: true,
|
||||
},
|
||||
array: {
|
||||
...incomingStateFromServer.array,
|
||||
rows: currentState?.array?.rows,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept values from the server if they have been modified locally since the request was made, e.g. `overrideLocalChanges: false` on autosave', () => {
|
||||
it('should not accept values from the server if they have been modified locally since the request was made, e.g. on autosave', () => {
|
||||
const title: FieldState = {
|
||||
value: 'Test Post (modified on the client 1)',
|
||||
initialValue: 'Test Post',
|
||||
|
||||
@@ -145,19 +145,6 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'spaceBottom',
|
||||
type: 'select',
|
||||
required: false,
|
||||
defaultValue: 'mb-0',
|
||||
options: [
|
||||
{ label: 'None', value: 'mb-0' },
|
||||
{ label: 'Small', value: 'mb-8' },
|
||||
{ label: 'Medium', value: 'mb-16' },
|
||||
{ label: 'Large', value: 'mb-24' },
|
||||
{ label: 'Extra Large', value: 'mb-[150px]' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,26 +96,22 @@ async function selectOption({
|
||||
type GetSelectInputValueFunction = <TMultiSelect = true>(args: {
|
||||
multiSelect: TMultiSelect
|
||||
selectLocator: Locator
|
||||
valueLabelClass?: string
|
||||
}) => Promise<TMultiSelect extends true ? string[] : string | undefined>
|
||||
|
||||
export const getSelectInputValue: GetSelectInputValueFunction = async ({
|
||||
selectLocator,
|
||||
multiSelect = false,
|
||||
valueLabelClass,
|
||||
}) => {
|
||||
if (multiSelect) {
|
||||
// For multi-select, get all selected options
|
||||
const selectedOptions = await selectLocator
|
||||
.locator(valueLabelClass || '.multi-value-label__text')
|
||||
.locator('.multi-value-label__text')
|
||||
.allTextContents()
|
||||
return selectedOptions || []
|
||||
}
|
||||
|
||||
// For single-select, get the selected value
|
||||
const singleValue = await selectLocator
|
||||
.locator(valueLabelClass || '.react-select--single-value')
|
||||
.textContent()
|
||||
const singleValue = await selectLocator.locator('.react-select--single-value').textContent()
|
||||
return (singleValue ?? undefined) as any
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ export const PostsCollection: CollectionConfig = {
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'documentLoaded',
|
||||
label: 'Document loaded',
|
||||
|
||||
@@ -755,22 +755,6 @@ describe('Locked Documents', () => {
|
||||
|
||||
// fields should be readOnly / disabled
|
||||
await expect(page.locator('#field-text')).toBeDisabled()
|
||||
|
||||
const richTextRoot = page
|
||||
.locator('.rich-text-lexical .ContentEditable__root[data-lexical-editor="true"]')
|
||||
.first()
|
||||
|
||||
// ensure editor is present
|
||||
await expect(richTextRoot).toBeVisible()
|
||||
|
||||
// core read-only checks
|
||||
await expect(richTextRoot).toHaveAttribute('contenteditable', 'false')
|
||||
await expect(richTextRoot).toHaveAttribute('aria-readonly', 'true')
|
||||
|
||||
// wrapper has read-only class (nice-to-have)
|
||||
await expect(page.locator('.rich-text-lexical').first()).toHaveClass(
|
||||
/rich-text-lexical--read-only/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -141,21 +141,6 @@ export interface Page {
|
||||
export interface Post {
|
||||
id: string;
|
||||
text?: string | null;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
documentLoaded?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -279,7 +264,6 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
richText?: T;
|
||||
documentLoaded?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { autosaveGlobalSlug } from '../shared.js'
|
||||
|
||||
export const AutosaveGlobal: CollectionConfig = {
|
||||
slug: autosaveGlobalSlug,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
group: 'Tenant Globals',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: {
|
||||
interval: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -7,16 +7,15 @@ const dirname = path.dirname(filename)
|
||||
import type { Config as ConfigType } from './payload-types.js'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { AutosaveGlobal } from './collections/AutosaveGlobal.js'
|
||||
import { Menu } from './collections/Menu.js'
|
||||
import { MenuItems } from './collections/MenuItems.js'
|
||||
import { Tenants } from './collections/Tenants.js'
|
||||
import { Users } from './collections/Users/index.js'
|
||||
import { seed } from './seed/index.js'
|
||||
import { autosaveGlobalSlug, menuItemsSlug, menuSlug } from './shared.js'
|
||||
import { menuItemsSlug, menuSlug } from './shared.js'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [Tenants, Users, MenuItems, Menu, AutosaveGlobal],
|
||||
collections: [Tenants, Users, MenuItems, Menu],
|
||||
admin: {
|
||||
autoLogin: false,
|
||||
importMap: {
|
||||
@@ -45,9 +44,6 @@ export default buildConfigWithDefaults({
|
||||
[menuSlug]: {
|
||||
isGlobal: true,
|
||||
},
|
||||
[autosaveGlobalSlug]: {
|
||||
isGlobal: true,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
translations: {
|
||||
|
||||
@@ -28,7 +28,7 @@ import { reInitializeDB } from '../helpers/reInitializeDB.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { credentials } from './credentials.js'
|
||||
import { seed } from './seed/index.js'
|
||||
import { autosaveGlobalSlug, menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from './shared.js'
|
||||
import { menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -37,7 +37,6 @@ test.describe('Multi Tenant', () => {
|
||||
let page: Page
|
||||
let serverURL: string
|
||||
let globalMenuURL: AdminUrlUtil
|
||||
let autosaveGlobalURL: AdminUrlUtil
|
||||
let menuItemsURL: AdminUrlUtil
|
||||
let usersURL: AdminUrlUtil
|
||||
let tenantsURL: AdminUrlUtil
|
||||
@@ -51,7 +50,6 @@ test.describe('Multi Tenant', () => {
|
||||
menuItemsURL = new AdminUrlUtil(serverURL, menuItemsSlug)
|
||||
usersURL = new AdminUrlUtil(serverURL, usersSlug)
|
||||
tenantsURL = new AdminUrlUtil(serverURL, tenantsSlug)
|
||||
autosaveGlobalURL = new AdminUrlUtil(serverURL, autosaveGlobalSlug)
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -76,7 +74,7 @@ test.describe('Multi Tenant', () => {
|
||||
data: credentials.admin,
|
||||
})
|
||||
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await page.goto(tenantsURL.list)
|
||||
|
||||
@@ -131,7 +129,7 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
|
||||
await page.goto(menuItemsURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('.collection-list .table .cell-name', {
|
||||
@@ -176,7 +174,7 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
|
||||
await page.goto(menuItemsURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('.collection-list .table .cell-name', {
|
||||
@@ -192,7 +190,7 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
|
||||
await page.goto(menuItemsURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('.collection-list .table .cell-name', {
|
||||
@@ -211,7 +209,7 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
|
||||
await page.goto(usersURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('.collection-list .table .cell-email', {
|
||||
@@ -271,7 +269,7 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
|
||||
await page.goto(menuItemsURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await goToListDoc({
|
||||
page,
|
||||
@@ -299,7 +297,7 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
|
||||
await page.goto(menuItemsURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await goToListDoc({
|
||||
page,
|
||||
@@ -308,7 +306,7 @@ test.describe('Multi Tenant', () => {
|
||||
urlUtil: menuItemsURL,
|
||||
})
|
||||
|
||||
await selectDocumentTenant({
|
||||
await selecteDocumentTenant({
|
||||
page,
|
||||
tenant: 'Steel Cat',
|
||||
})
|
||||
@@ -326,7 +324,7 @@ test.describe('Multi Tenant', () => {
|
||||
data: credentials.admin,
|
||||
})
|
||||
await page.goto(menuItemsURL.create)
|
||||
await selectDocumentTenant({
|
||||
await selecteDocumentTenant({
|
||||
page,
|
||||
tenant: 'Blue Dog',
|
||||
})
|
||||
@@ -420,24 +418,6 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
.toBe('Steel Cat')
|
||||
})
|
||||
|
||||
test('should navigate to globals with autosave enabled', async () => {
|
||||
await loginClientSide({
|
||||
page,
|
||||
serverURL,
|
||||
data: credentials.admin,
|
||||
})
|
||||
await page.goto(tenantsURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await page.goto(autosaveGlobalURL.list)
|
||||
await expect(page.locator('.doc-header__title')).toBeVisible()
|
||||
const globalTenant = await getGlobalTenant({ page })
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await getDocumentTenant({ page })
|
||||
})
|
||||
.toBe(globalTenant)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Tenant Selector', () => {
|
||||
@@ -519,7 +499,7 @@ test.describe('Multi Tenant', () => {
|
||||
})
|
||||
|
||||
await page.goto(tenantsURL.list)
|
||||
await clearGlobalTenant({ page })
|
||||
await clearTenant({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('.collection-list .table .cell-name', {
|
||||
@@ -606,24 +586,6 @@ test.describe('Multi Tenant', () => {
|
||||
/**
|
||||
* Helper Functions
|
||||
*/
|
||||
|
||||
async function getGlobalTenant({ page }: { page: Page }): Promise<string | undefined> {
|
||||
await openNav(page)
|
||||
return await getSelectInputValue<false>({
|
||||
selectLocator: page.locator('.tenant-selector'),
|
||||
multiSelect: false,
|
||||
})
|
||||
}
|
||||
|
||||
async function getDocumentTenant({ page }: { page: Page }): Promise<string | undefined> {
|
||||
await openNav(page)
|
||||
return await getSelectInputValue<false>({
|
||||
selectLocator: page.locator('#field-tenant'),
|
||||
multiSelect: false,
|
||||
valueLabelClass: '.relationship--single-value',
|
||||
})
|
||||
}
|
||||
|
||||
async function getTenantOptions({ page }: { page: Page }): Promise<string[]> {
|
||||
await openNav(page)
|
||||
return await getSelectInputOptions({
|
||||
@@ -631,7 +593,7 @@ async function getTenantOptions({ page }: { page: Page }): Promise<string[]> {
|
||||
})
|
||||
}
|
||||
|
||||
async function selectDocumentTenant({
|
||||
async function selecteDocumentTenant({
|
||||
page,
|
||||
tenant,
|
||||
}: {
|
||||
@@ -655,7 +617,7 @@ async function selectTenant({ page, tenant }: { page: Page; tenant: string }): P
|
||||
})
|
||||
}
|
||||
|
||||
async function clearGlobalTenant({ page }: { page: Page }): Promise<void> {
|
||||
async function clearTenant({ page }: { page: Page }): Promise<void> {
|
||||
await openNav(page)
|
||||
return clearSelectInput({
|
||||
selectLocator: page.locator('.tenant-selector'),
|
||||
|
||||
@@ -71,7 +71,6 @@ export interface Config {
|
||||
users: User;
|
||||
'food-items': FoodItem;
|
||||
'food-menu': FoodMenu;
|
||||
'autosave-global': AutosaveGlobal;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
@@ -86,7 +85,6 @@ export interface Config {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'food-items': FoodItemsSelect<false> | FoodItemsSelect<true>;
|
||||
'food-menu': FoodMenuSelect<false> | FoodMenuSelect<true>;
|
||||
'autosave-global': AutosaveGlobalSelect<false> | AutosaveGlobalSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -177,7 +175,7 @@ export interface User {
|
||||
*/
|
||||
export interface FoodItem {
|
||||
id: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
tenant: string | Tenant;
|
||||
name: string;
|
||||
content?: {
|
||||
root: {
|
||||
@@ -203,7 +201,7 @@ export interface FoodItem {
|
||||
*/
|
||||
export interface FoodMenu {
|
||||
id: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
tenant: string | Tenant;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
menuItems?:
|
||||
@@ -219,19 +217,6 @@ export interface FoodMenu {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-global".
|
||||
*/
|
||||
export interface AutosaveGlobal {
|
||||
id: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
@@ -254,10 +239,6 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'food-menu';
|
||||
value: string | FoodMenu;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'autosave-global';
|
||||
value: string | AutosaveGlobal;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -371,18 +352,6 @@ export interface FoodMenuSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-global_select".
|
||||
*/
|
||||
export interface AutosaveGlobalSelect<T extends boolean = true> {
|
||||
tenant?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
@@ -5,5 +5,3 @@ export const usersSlug = 'users'
|
||||
export const menuItemsSlug = 'food-items'
|
||||
|
||||
export const menuSlug = 'food-menu'
|
||||
|
||||
export const autosaveGlobalSlug = 'autosave-global'
|
||||
|
||||
Reference in New Issue
Block a user