fix(next, payload): enable relationship & upload version tracking when localization enabled (#7508)

This commit is contained in:
Patrik
2024-08-06 12:28:06 -04:00
committed by GitHub
parent e8bed7b315
commit 62744e79ac
9 changed files with 237 additions and 43 deletions

7
.vscode/launch.json vendored
View File

@@ -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}",

View File

@@ -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}

View File

@@ -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,
} }

View File

@@ -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,

View File

@@ -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'

View File

@@ -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,
}

View File

@@ -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',

View 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,
)
})
})
})

View File

@@ -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".