diff --git a/src/collections/blocks/ImageCards.ts b/src/collections/blocks/ImageCards.ts index 706f8bb..6da28b6 100644 --- a/src/collections/blocks/ImageCards.ts +++ b/src/collections/blocks/ImageCards.ts @@ -36,6 +36,14 @@ export const ImageCardsBlock: Block = { admin: { condition: (_, siblingData) => !siblingData?.customLink, }, + hooks: { + beforeChange: [ + // Write null instead of undefined so the Postgres adapter removes stale + // _rels rows left behind by block reordering, otherwise they resurface + // as this card's link: https://github.com/payloadcms/payload/issues/16647 + ({ value, siblingData }) => (siblingData?.customLink ? null : (value ?? null)), + ], + }, }, { name: 'customLink', @@ -43,9 +51,8 @@ export const ImageCardsBlock: Block = { type: 'text', required: false, admin: { - condition: (_, siblingData) => !siblingData?.link, description: - 'Alternative zu "Verknüpfung". Nur eins von beiden kann gesetzt werden.', + 'Alternative zu "Verknüpfung", z.B. für externe Seiten oder PDF-Dokumente.', }, }, ], diff --git a/src/compositions/Blocks/ImageCardsBlock.tsx b/src/compositions/Blocks/ImageCardsBlock.tsx index 775b39f..acedfbe 100644 --- a/src/compositions/Blocks/ImageCardsBlock.tsx +++ b/src/compositions/Blocks/ImageCardsBlock.tsx @@ -1,18 +1,14 @@ -import { Group, Media, Page } from '@/payload-types' +import { Media } from '@/payload-types' import { Container } from '@/components/Container/Container' import { Section } from '@/components/Section/Section' import { ImageCard } from '@/components/ImageCard/ImageCard' import { getPhoto } from '@/utils/dto/gallery' +import { ImageCardLinkFields, resolveImageCardHref } from '@/utils/dto/imageCardHref' import styles from './imageCardsBlock.module.scss' -export type ImageCardsBlockItem = { +export type ImageCardsBlockItem = ImageCardLinkFields & { title: string image: string | Media - link?: - | { relationTo: 'pages'; value: string | Page } - | { relationTo: 'group'; value: string | Group } - | null - customLink?: string | null id?: string | null } @@ -20,18 +16,6 @@ type ImageCardsBlockProps = { items: ImageCardsBlockItem[] } -const resolveHref = (item: ImageCardsBlockItem): string | undefined => { - if (item.link && typeof item.link.value === 'object') { - if (item.link.relationTo === 'pages' && item.link.value.slug) { - return `/${item.link.value.slug}` - } - if (item.link.relationTo === 'group' && item.link.value.slug) { - return `/gruppe/${item.link.value.slug}` - } - } - return item.customLink ?? undefined -} - export const ImageCardsBlock = ({ items }: ImageCardsBlockProps) => { return ( @@ -46,7 +30,7 @@ export const ImageCardsBlock = ({ items }: ImageCardsBlockProps) => { key={item.id ?? item.title} src={photo.src} title={item.title} - href={resolveHref(item)} + href={resolveImageCardHref(item)} /> ) })} diff --git a/src/utils/dto/imageCardHref.test.ts b/src/utils/dto/imageCardHref.test.ts new file mode 100644 index 0000000..b0055e1 --- /dev/null +++ b/src/utils/dto/imageCardHref.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { resolveImageCardHref } from './imageCardHref' + +const page = { slug: 'chronik' } as any +const group = { slug: 'chor' } as any + +describe('resolveImageCardHref', () => { + it('links to a page relationship', () => { + expect(resolveImageCardHref({ link: { relationTo: 'pages', value: page } })).toBe('/chronik') + }) + + it('links to a group relationship', () => { + expect(resolveImageCardHref({ link: { relationTo: 'group', value: group } })).toBe('/gruppe/chor') + }) + + it('returns nothing for an unpopulated relationship', () => { + expect(resolveImageCardHref({ link: { relationTo: 'group', value: 'id-only' } })).toBeUndefined() + }) + + it('uses the custom link when no relationship is set', () => { + expect(resolveImageCardHref({ link: null, customLink: 'https://example.org/x.pdf' })) + .toBe('https://example.org/x.pdf') + }) + + it('prefers the custom link when a stale relationship is also present', () => { + expect(resolveImageCardHref({ link: { relationTo: 'group', value: group }, customLink: 'https://example.org/x.pdf' })) + .toBe('https://example.org/x.pdf') + }) + + it('ignores a blank custom link', () => { + expect(resolveImageCardHref({ link: { relationTo: 'group', value: group }, customLink: ' ' })).toBe('/gruppe/chor') + expect(resolveImageCardHref({ customLink: '' })).toBeUndefined() + }) +}) diff --git a/src/utils/dto/imageCardHref.ts b/src/utils/dto/imageCardHref.ts new file mode 100644 index 0000000..7147a9d --- /dev/null +++ b/src/utils/dto/imageCardHref.ts @@ -0,0 +1,30 @@ +import type { Group, Page } from '@/payload-types' + +export type ImageCardLink = + | { relationTo: 'pages'; value: string | Page } + | { relationTo: 'group'; value: string | Group } + | null + +export type ImageCardLinkFields = { + link?: ImageCardLink + customLink?: string | null +} + +// A custom link wins over the relationship. Both can be present at once when a +// stale relationship row survives in the database (see the beforeChange hook in +// src/collections/blocks/ImageCards.ts), and the custom link is what the editor +// actually sees and intends. +export const resolveImageCardHref = (item: ImageCardLinkFields): string | undefined => { + const customLink = item.customLink?.trim() + if (customLink) return customLink + + if (item.link && typeof item.link.value === 'object') { + if (item.link.relationTo === 'pages' && item.link.value.slug) { + return `/${item.link.value.slug}` + } + if (item.link.relationTo === 'group' && item.link.value.slug) { + return `/gruppe/${item.link.value.slug}` + } + } + return undefined +}