fix(next, payload): enable relationship & upload version tracking when localization enabled (#7508)
This commit is contained in:
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -56,6 +56,13 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal"
|
"type": "node-terminal"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "node --no-deprecation test/dev.js fields-relationship",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"name": "Run Dev Fields-Relationship",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "node --no-deprecation test/dev.js login-with-username",
|
"command": "node --no-deprecation test/dev.js login-with-username",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ const generateLabelFromValue = (
|
|||||||
locale: string,
|
locale: string,
|
||||||
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
|
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
|
||||||
): string => {
|
): string => {
|
||||||
let relation: string
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map((v) => generateLabelFromValue(collections, field, locale, v))
|
||||||
|
.filter(Boolean) // Filters out any undefined or empty values
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
let relatedDoc: RelationshipValue
|
let relatedDoc: RelationshipValue
|
||||||
let valueToReturn = '' as any
|
let valueToReturn = '' as any
|
||||||
|
|
||||||
@@ -37,17 +43,20 @@ const generateLabelFromValue = (
|
|||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(relationTo)) {
|
if (typeof value === 'object' && 'relationTo' in value) {
|
||||||
if (typeof value === 'object') {
|
relatedDoc = value.value
|
||||||
relation = value.relationTo
|
|
||||||
relatedDoc = value.value
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
relation = relationTo
|
// Non-polymorphic relationship
|
||||||
relatedDoc = value
|
relatedDoc = value
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedCollection = collections.find((c) => c.slug === relation)
|
const relatedCollection = relationTo
|
||||||
|
? collections.find(
|
||||||
|
(c) =>
|
||||||
|
c.slug ===
|
||||||
|
(typeof value === 'object' && 'relationTo' in value ? value.relationTo : relationTo),
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
if (relatedCollection) {
|
if (relatedCollection) {
|
||||||
const useAsTitle = relatedCollection?.admin?.useAsTitle
|
const useAsTitle = relatedCollection?.admin?.useAsTitle
|
||||||
@@ -56,45 +65,65 @@ const generateLabelFromValue = (
|
|||||||
)
|
)
|
||||||
let titleFieldIsLocalized = false
|
let titleFieldIsLocalized = false
|
||||||
|
|
||||||
if (useAsTitleField && fieldAffectsData(useAsTitleField))
|
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
|
||||||
titleFieldIsLocalized = useAsTitleField.localized
|
titleFieldIsLocalized = useAsTitleField.localized
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
|
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
|
||||||
valueToReturn = relatedDoc[useAsTitle]
|
valueToReturn = relatedDoc[useAsTitle]
|
||||||
} else if (typeof relatedDoc?.id !== 'undefined') {
|
} else if (typeof relatedDoc?.id !== 'undefined') {
|
||||||
valueToReturn = relatedDoc.id
|
valueToReturn = relatedDoc.id
|
||||||
|
} else {
|
||||||
|
valueToReturn = relatedDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof valueToReturn === 'object' && titleFieldIsLocalized) {
|
if (typeof valueToReturn === 'object' && titleFieldIsLocalized) {
|
||||||
valueToReturn = valueToReturn[locale]
|
valueToReturn = valueToReturn[locale]
|
||||||
}
|
}
|
||||||
|
} else if (relatedDoc) {
|
||||||
|
// Handle non-polymorphic `hasMany` relationships or fallback
|
||||||
|
if (typeof relatedDoc.id !== 'undefined') {
|
||||||
|
valueToReturn = relatedDoc.id
|
||||||
|
} else {
|
||||||
|
valueToReturn = relatedDoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof valueToReturn === 'object' && valueToReturn !== null) {
|
||||||
|
valueToReturn = JSON.stringify(valueToReturn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return valueToReturn
|
return valueToReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
const Relationship: React.FC<Props> = ({ comparison, field, i18n, locale, version }) => {
|
const Relationship: React.FC<Props> = ({ comparison, field, i18n, locale, version }) => {
|
||||||
let placeholder = ''
|
const placeholder = `[${i18n.t('general:noValue')}]`
|
||||||
|
|
||||||
const { collections } = useConfig()
|
const { collections } = useConfig()
|
||||||
|
|
||||||
if (version === comparison) placeholder = `[${i18n.t('general:noValue')}]`
|
let versionToRender: string | undefined = placeholder
|
||||||
|
let comparisonToRender: string | undefined = placeholder
|
||||||
|
|
||||||
let versionToRender = version
|
if (version) {
|
||||||
let comparisonToRender = comparison
|
if ('hasMany' in field && field.hasMany && Array.isArray(version)) {
|
||||||
|
versionToRender =
|
||||||
|
version.map((val) => generateLabelFromValue(collections, field, locale, val)).join(', ') ||
|
||||||
|
placeholder
|
||||||
|
} else {
|
||||||
|
versionToRender = generateLabelFromValue(collections, field, locale, version) || placeholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ('hasMany' in field && field.hasMany) {
|
if (comparison) {
|
||||||
if (Array.isArray(version))
|
if ('hasMany' in field && field.hasMany && Array.isArray(comparison)) {
|
||||||
versionToRender = version
|
comparisonToRender =
|
||||||
.map((val) => generateLabelFromValue(collections, field, locale, val))
|
comparison
|
||||||
.join(', ')
|
.map((val) => generateLabelFromValue(collections, field, locale, val))
|
||||||
if (Array.isArray(comparison))
|
.join(', ') || placeholder
|
||||||
comparisonToRender = comparison
|
} else {
|
||||||
.map((val) => generateLabelFromValue(collections, field, locale, val))
|
comparisonToRender =
|
||||||
.join(', ')
|
generateLabelFromValue(collections, field, locale, comparison) || placeholder
|
||||||
} else {
|
}
|
||||||
versionToRender = generateLabelFromValue(collections, field, locale, version)
|
|
||||||
comparisonToRender = generateLabelFromValue(collections, field, locale, comparison)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const label =
|
const label =
|
||||||
@@ -112,10 +141,8 @@ const Relationship: React.FC<Props> = ({ comparison, field, i18n, locale, versio
|
|||||||
</Label>
|
</Label>
|
||||||
<ReactDiffViewer
|
<ReactDiffViewer
|
||||||
hideLineNumbers
|
hideLineNumbers
|
||||||
newValue={typeof versionToRender !== 'undefined' ? String(versionToRender) : placeholder}
|
newValue={versionToRender}
|
||||||
oldValue={
|
oldValue={comparisonToRender}
|
||||||
typeof comparisonToRender !== 'undefined' ? String(comparisonToRender) : placeholder
|
|
||||||
}
|
|
||||||
showDiffOnly={false}
|
showDiffOnly={false}
|
||||||
splitView
|
splitView
|
||||||
styles={diffStyles}
|
styles={diffStyles}
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ export default {
|
|||||||
number: Text,
|
number: Text,
|
||||||
point: Text,
|
point: Text,
|
||||||
radio: Select,
|
radio: Select,
|
||||||
relationship: null,
|
relationship: Relationship,
|
||||||
richText: Text,
|
richText: Text,
|
||||||
row: Nested,
|
row: Nested,
|
||||||
select: Select,
|
select: Select,
|
||||||
tabs: Tabs,
|
tabs: Tabs,
|
||||||
text: Text,
|
text: Text,
|
||||||
textarea: Text,
|
textarea: Text,
|
||||||
upload: null,
|
upload: Relationship,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,24 +126,25 @@ export const relationshipPopulationPromise = async ({
|
|||||||
|
|
||||||
if (fieldSupportsMany(field) && field.hasMany) {
|
if (fieldSupportsMany(field) && field.hasMany) {
|
||||||
if (
|
if (
|
||||||
|
field.localized &&
|
||||||
locale === 'all' &&
|
locale === 'all' &&
|
||||||
typeof siblingDoc[field.name] === 'object' &&
|
typeof siblingDoc[field.name] === 'object' &&
|
||||||
siblingDoc[field.name] !== null
|
siblingDoc[field.name] !== null
|
||||||
) {
|
) {
|
||||||
Object.keys(siblingDoc[field.name]).forEach((key) => {
|
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
|
||||||
if (Array.isArray(siblingDoc[field.name][key])) {
|
if (Array.isArray(siblingDoc[field.name][localeKey])) {
|
||||||
siblingDoc[field.name][key].forEach((relatedDoc, index) => {
|
siblingDoc[field.name][localeKey].forEach((relatedDoc, index) => {
|
||||||
const rowPromise = async () => {
|
const rowPromise = async () => {
|
||||||
await populate({
|
await populate({
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: siblingDoc[field.name][key][index],
|
data: siblingDoc[field.name][localeKey][index],
|
||||||
dataReference: resultingDoc,
|
dataReference: resultingDoc,
|
||||||
depth: populateDepth,
|
depth: populateDepth,
|
||||||
draft,
|
draft,
|
||||||
fallbackLocale,
|
fallbackLocale,
|
||||||
field,
|
field,
|
||||||
index,
|
index,
|
||||||
key,
|
key: localeKey,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
req,
|
req,
|
||||||
@@ -179,21 +180,22 @@ export const relationshipPopulationPromise = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
field.localized &&
|
||||||
|
locale === 'all' &&
|
||||||
typeof siblingDoc[field.name] === 'object' &&
|
typeof siblingDoc[field.name] === 'object' &&
|
||||||
siblingDoc[field.name] !== null &&
|
siblingDoc[field.name] !== null
|
||||||
locale === 'all'
|
|
||||||
) {
|
) {
|
||||||
Object.keys(siblingDoc[field.name]).forEach((key) => {
|
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
|
||||||
const rowPromise = async () => {
|
const rowPromise = async () => {
|
||||||
await populate({
|
await populate({
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: siblingDoc[field.name][key],
|
data: siblingDoc[field.name][localeKey],
|
||||||
dataReference: resultingDoc,
|
dataReference: resultingDoc,
|
||||||
depth: populateDepth,
|
depth: populateDepth,
|
||||||
draft,
|
draft,
|
||||||
fallbackLocale,
|
fallbackLocale,
|
||||||
field,
|
field,
|
||||||
key,
|
key: localeKey,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
req,
|
req,
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export const collection2Slug = 'collection-2'
|
|||||||
export const videoCollectionSlug = 'videos'
|
export const videoCollectionSlug = 'videos'
|
||||||
export const podcastCollectionSlug = 'podcasts'
|
export const podcastCollectionSlug = 'podcasts'
|
||||||
export const mixedMediaCollectionSlug = 'mixed-media'
|
export const mixedMediaCollectionSlug = 'mixed-media'
|
||||||
|
export const versionedRelationshipFieldSlug = 'versioned-relationship-field'
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
import { collection1Slug, versionedRelationshipFieldSlug } from '../../collectionSlugs.js'
|
||||||
|
|
||||||
|
export const VersionedRelationshipFieldCollection: CollectionConfig = {
|
||||||
|
slug: versionedRelationshipFieldSlug,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'relationshipField',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: [collection1Slug],
|
||||||
|
hasMany: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
versions: true,
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
slug,
|
slug,
|
||||||
videoCollectionSlug,
|
videoCollectionSlug,
|
||||||
} from './collectionSlugs.js'
|
} from './collectionSlugs.js'
|
||||||
|
import { VersionedRelationshipFieldCollection } from './collections/VersionedRelationshipField/index.js'
|
||||||
|
|
||||||
export interface FieldsRelationship {
|
export interface FieldsRelationship {
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@@ -321,6 +322,9 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
slug: collection1Slug,
|
slug: collection1Slug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'name',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fields: [
|
fields: [
|
||||||
@@ -376,7 +380,13 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
VersionedRelationshipFieldCollection,
|
||||||
],
|
],
|
||||||
|
localization: {
|
||||||
|
locales: ['en'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
fallback: true,
|
||||||
|
},
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: 'users',
|
collection: 'users',
|
||||||
|
|||||||
101
test/fields-relationship/int.spec.ts
Normal file
101
test/fields-relationship/int.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||||
|
import type { Collection1 } from './payload-types.js'
|
||||||
|
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
|
import { collection1Slug, versionedRelationshipFieldSlug } from './collectionSlugs.js'
|
||||||
|
import configPromise from './config.js'
|
||||||
|
|
||||||
|
let payload: Payload
|
||||||
|
let restClient: NextRESTClient
|
||||||
|
|
||||||
|
const { email, password } = devUser
|
||||||
|
|
||||||
|
describe('Relationship Fields', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const initialized = await initPayloadInt(configPromise)
|
||||||
|
;({ payload, restClient } = initialized)
|
||||||
|
|
||||||
|
await restClient.login({
|
||||||
|
slug: 'users',
|
||||||
|
credentials: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (typeof payload.db.destroy === 'function') {
|
||||||
|
await payload.db.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Versioned Relationship Field', () => {
|
||||||
|
let version2ID: string
|
||||||
|
const relatedDocName = 'Related Doc'
|
||||||
|
beforeAll(async () => {
|
||||||
|
const relatedDoc = await payload.create({
|
||||||
|
collection: collection1Slug,
|
||||||
|
data: {
|
||||||
|
name: relatedDocName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const version1 = await payload.create({
|
||||||
|
collection: versionedRelationshipFieldSlug,
|
||||||
|
data: {
|
||||||
|
title: 'Version 1 Title',
|
||||||
|
relationshipField: {
|
||||||
|
value: relatedDoc.id,
|
||||||
|
relationTo: collection1Slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const version2 = await payload.update({
|
||||||
|
collection: versionedRelationshipFieldSlug,
|
||||||
|
id: version1.id,
|
||||||
|
data: {
|
||||||
|
title: 'Version 2 Title',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const versions = await payload.findVersions({
|
||||||
|
collection: versionedRelationshipFieldSlug,
|
||||||
|
where: {
|
||||||
|
parent: {
|
||||||
|
equals: version2.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: '-updatedAt',
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
version2ID = versions.docs[0].id
|
||||||
|
})
|
||||||
|
it('should return the correct versioned relationship field via REST', async () => {
|
||||||
|
const version2Data = await restClient
|
||||||
|
.GET(`/${versionedRelationshipFieldSlug}/versions/${version2ID}?locale=all`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
expect(version2Data.version.title).toEqual('Version 2 Title')
|
||||||
|
expect(version2Data.version.relationshipField[0].value.name).toEqual(relatedDocName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the correct versioned relationship field via LocalAPI', async () => {
|
||||||
|
const version2Data = await payload.findVersionByID({
|
||||||
|
collection: versionedRelationshipFieldSlug,
|
||||||
|
id: version2ID,
|
||||||
|
locale: 'all',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(version2Data.version.title).toEqual('Version 2 Title')
|
||||||
|
expect((version2Data.version.relationshipField[0].value as Collection1).name).toEqual(
|
||||||
|
relatedDocName,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -24,12 +24,16 @@ export interface Config {
|
|||||||
videos: Video;
|
videos: Video;
|
||||||
podcasts: Podcast;
|
podcasts: Podcast;
|
||||||
'mixed-media': MixedMedia;
|
'mixed-media': MixedMedia;
|
||||||
|
'versioned-relationship-field': VersionedRelationshipField;
|
||||||
users: User;
|
users: User;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: string;
|
||||||
|
};
|
||||||
globals: {};
|
globals: {};
|
||||||
locale: null;
|
locale: 'en';
|
||||||
user: User & {
|
user: User & {
|
||||||
collection: 'users';
|
collection: 'users';
|
||||||
};
|
};
|
||||||
@@ -37,15 +41,20 @@ export interface Config {
|
|||||||
export interface UserAuthOperations {
|
export interface UserAuthOperations {
|
||||||
forgotPassword: {
|
forgotPassword: {
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
login: {
|
login: {
|
||||||
password: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
|
password: string;
|
||||||
};
|
};
|
||||||
registerFirstUser: {
|
registerFirstUser: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
unlock: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -252,6 +261,22 @@ export interface MixedMedia {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "versioned-relationship-field".
|
||||||
|
*/
|
||||||
|
export interface VersionedRelationshipField {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
relationshipField?:
|
||||||
|
| {
|
||||||
|
relationTo: 'collection-1';
|
||||||
|
value: string | Collection1;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
|
|||||||
Reference in New Issue
Block a user