Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
useFormFields,
useTranslation,
} from '@payloadcms/ui'
import { formatDocTitle } from '@payloadcms/ui/shared'
import { fieldAffectsData, getObjectDotNotation } from 'payload/shared'
import React, { useState, useTransition } from 'react'

Expand All @@ -25,6 +24,7 @@ import type {
import type { ImportPreviewResponse } from '../../types.js'

import { DEFAULT_PREVIEW_LIMIT, PREVIEW_LIMIT_OPTIONS } from '../../constants.js'
import { RelationshipCell } from '../RelationshipCell/index.js'
import './index.css'

const baseClass = 'import-preview'
Expand Down Expand Up @@ -239,7 +239,7 @@ export const ImportPreview: React.FC = () => {
active: true,
field,
Heading: label,
renderedCells: docs.map((doc) => {
renderedCells: docs.map((doc, rowIndex) => {
const value = getObjectDotNotation(doc, fieldPath)

if (value === undefined || value === null) {
Expand All @@ -248,62 +248,13 @@ export const ImportPreview: React.FC = () => {

// Format based on field type
if (field.type === 'relationship' || field.type === 'upload') {
// Handle relationships
if (typeof value === 'object' && !Array.isArray(value)) {
// Single relationship
const relationTo = Array.isArray(field.relationTo)
? (value as any).relationTo
: field.relationTo

const relatedConfig = config.collections.find((c) => c.slug === relationTo)
if (relatedConfig && relatedConfig.admin?.useAsTitle) {
const titleValue = (value as any)[relatedConfig.admin.useAsTitle]
if (titleValue) {
return formatDocTitle({
collectionConfig: relatedConfig,
data: value as any,
dateFormat: config.admin.dateFormat,
i18n,
})
}
}

// Fallback to ID
const id = (value as any).id || value
return `${getTranslation(relatedConfig?.labels?.singular || relationTo, i18n)}: ${id}`
} else if (Array.isArray(value)) {
// Multiple relationships
return value
.map((item) => {
if (typeof item === 'object') {
const relationTo = Array.isArray(field.relationTo)
? item.relationTo
: field.relationTo
const relatedConfig = config.collections.find(
(c) => c.slug === relationTo,
)

if (relatedConfig && relatedConfig.admin?.useAsTitle) {
const titleValue = item[relatedConfig.admin.useAsTitle]
if (titleValue) {
return formatDocTitle({
collectionConfig: relatedConfig,
data: item,
dateFormat: config.admin.dateFormat,
i18n,
})
}
}

return item.id || item
}
return item
})
.join(', ')
}

// Just an ID
return String(value)
return (
<RelationshipCell
key={`${fieldPath}-${rowIndex}`}
relationTo={field.relationTo}
value={value}
/>
)
} else if (field.type === 'date') {
// Display date as string to avoid wrong locale/timezone conversion
return String(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import type { ClientCollectionConfig } from 'payload'

import { describe, expect, it } from 'vitest'

import { getRelationshipGroups } from './getRelationshipGroups.js'

const collections = [
{
slug: 'posts',
admin: { useAsTitle: 'title' },
fields: [{ name: 'title', type: 'text' }],
labels: { plural: 'Posts', singular: 'Post' },
},
{
slug: 'users',
admin: { useAsTitle: 'email' },
fields: [{ name: 'email', type: 'email' }],
labels: { plural: 'Users', singular: 'User' },
},
] as unknown as ClientCollectionConfig[]

const i18n = { t: (key: string) => key } as any

const group = ({ relationTo, value }: { relationTo: string | string[]; value: unknown }) =>
getRelationshipGroups({ collections, dateFormat: 'MMMM do yyyy', i18n, relationTo, value })

/** Collapses each group to `Label: value, value (+N)` so assertions stay readable. */
const summarize = (groups: ReturnType<typeof group>) =>
groups.map(({ label, options, remaining }) => {
const values = options.join(', ')

return remaining ? `${label}: ${values} (+${remaining})` : `${label}: ${values}`
})

describe('getRelationshipGroups', () => {
describe('monomorphic', () => {
it('should build a single group labeled with the plural collection label', () => {
expect(group({ relationTo: 'posts', value: 3 })).toEqual([
{
label: 'Posts',
options: ['3'],
remaining: 0,
},
])
})

it('should collect a list of bare IDs into one group', () => {
expect(summarize(group({ relationTo: 'posts', value: [1, 2] }))).toEqual(['Posts: 1, 2'])
})

it('should title a populated document using useAsTitle', () => {
expect(
summarize(group({ relationTo: 'posts', value: { id: 3, title: 'The Wall' } })),
).toEqual(['Posts: The Wall'])
})

it('should fall back to untitled and ID for a document with no useAsTitle value', () => {
expect(summarize(group({ relationTo: 'posts', value: { id: 3 } }))).toEqual([
'Posts: general:untitled - ID: 3',
])
})
})

describe('polymorphic', () => {
it('should label a wrapped ID with its collection', () => {
expect(
summarize(
group({ relationTo: ['posts', 'users'], value: { relationTo: 'posts', value: 3 } }),
),
).toEqual(['Posts: 3'])
})

it('should title a wrapped populated document using useAsTitle', () => {
expect(
summarize(
group({
relationTo: ['posts', 'users'],
value: { relationTo: 'posts', value: { id: 3, title: 'The Wall' } },
}),
),
).toEqual(['Posts: The Wall'])
})

it('should group entries that share a collection', () => {
expect(
summarize(
group({
relationTo: ['posts', 'users'],
value: [
{ relationTo: 'users', value: 7 },
{ relationTo: 'posts', value: 3 },
{ relationTo: 'users', value: 9 },
],
}),
),
).toEqual(['Users: 7, 9', 'Posts: 3'])
})

it('should order groups by where their collection first appears', () => {
expect(
group({
relationTo: ['posts', 'users'],
value: [
{ relationTo: 'posts', value: 3 },
{ relationTo: 'users', value: 7 },
],
}).map(({ label }) => label),
).toEqual(['Posts', 'Users'])
})

it('should fall back to the collection slug when the collection has no labels', () => {
expect(
summarize(group({ relationTo: ['pages'], value: { relationTo: 'pages', value: 4 } })),
).toEqual(['pages: 4'])
})

it('should keep entries whose collection is unknown in their own unlabeled group', () => {
expect(
summarize(
group({
relationTo: ['posts', 'users'],
value: [{ relationTo: 'posts', value: 3 }, 4],
}),
),
).toEqual(['Posts: 3', ': 4'])
})
})

describe('capping', () => {
it('should not report remaining options when the group fits', () => {
expect(summarize(group({ relationTo: 'posts', value: [1, 2, 3] }))).toEqual([
'Posts: 1, 2, 3',
])
})

it('should cap a group at three options and count the rest', () => {
expect(summarize(group({ relationTo: 'posts', value: [1, 2, 3, 4, 5] }))).toEqual([
'Posts: 1, 2, 3 (+2)',
])
})

it('should cap each collection independently rather than across the cell', () => {
expect(
summarize(
group({
relationTo: ['posts', 'users'],
value: [
{ relationTo: 'users', value: 7 },
{ relationTo: 'posts', value: 3 },
{ relationTo: 'users', value: 9 },
{ relationTo: 'posts', value: 4 },
],
}),
),
).toEqual(['Users: 7, 9', 'Posts: 3, 4'])
})

it('should count overflow per collection', () => {
expect(
summarize(
group({
relationTo: ['posts', 'users'],
value: [
...[1, 2, 3, 4, 5].map((value) => ({ relationTo: 'posts', value })),
...[7, 8].map((value) => ({ relationTo: 'users', value })),
],
}),
),
).toEqual(['Posts: 1, 2, 3 (+2)', 'Users: 7, 8'])
})

it('should not let a leading collection crowd out a later one', () => {
expect(
summarize(
group({
relationTo: ['posts', 'users'],
value: [
...[1, 2, 3, 4].map((value) => ({ relationTo: 'posts', value })),
{ relationTo: 'users', value: 7 },
],
}),
),
).toEqual(['Posts: 1, 2, 3 (+1)', 'Users: 7'])
})
})

describe('unresolvable values', () => {
it('should render an object carrying no ID as JSON rather than [object Object]', () => {
expect(summarize(group({ relationTo: 'posts', value: { slug: 'no-id-here' } }))).toEqual([
'Posts: {"slug":"no-id-here"}',
])
})

it('should never render [object Object] for a wrapped value', () => {
const groups = group({
relationTo: ['posts', 'users'],
value: { relationTo: 'posts', value: { title: 'No ID' } },
})

expect(groups[0]?.options[0]).not.toContain('[object Object]')
})
})
})
Loading
Loading