payloadcms / payload

Payload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.
https://payloadcms.com
MIT License
23.24k stars 1.46k forks source link

useLivePreview() breaks when collection has an object array #7662

Closed thoerr closed 2 weeks ago

thoerr commented 1 month ago

Link to reproduction

No response

Payload Version

3.0.0-beta.76

Node Version

v20.10.0

Next.js Version

15.0.0-canary.104

Describe the Bug

I have a collection with an object array field called slides:

      name: 'slides',
      type: 'array',
      label: 'Slides',
      minRows: 1,
      fields: [
        {
          name: 'slideTitle',
          type: 'text',
          label: 'Slide Title',
          required: true,
        },
        {
          name: 'content',
          label: 'Content',
          type: 'richText',
          editor: lexicalEditor({
            features: ({ defaultFeatures }) => [
              ...defaultFeatures,
              BlocksFeature({ blocks: [QuestionsBlock] }),
            ],
          }),
        },
      ],
    },

When using livePreview on this collection, the preview works when starting a new document. However, once there is data saved to the slides object array, the livePreview will fail on load with the following error:

image

This appears to be a problem with assigning the property value explicitly rather than spreading the result. This line in traverseFields.js works for shallow properties, but fails when assigning a sub-object property value, making the live preview non-functional.

I don't know if this is a beta 3.0 issue, or an existing issue in previous version of livePreview, as this is my first time using it.


I get a second issue in this situation where traverseRichText.js errors on line 45 when trying to read 'id'. This one is less clear to me, and I'm not sure what causes it to sometimes occur, but it happens rather often while adding/editing content to the lexical editor. This can happen with a new document or existing document.

image

Reproduction Steps

  1. Create a collection with an object array that contains it's own properties.
  2. Enable livePreview on the collection.
  3. Set up useLivePreview on the front-end.
  4. Create a new document in the collection and add data to the object array.
  5. Save the document.
  6. Reload payload.

Adapters and Plugins

No response

paulpopus commented 3 weeks ago

Testing this now on the latest beta and I can't reproduce it, could you provide a more full config please?

Does it error only when live preview is on? Could it be the way you're serialising rich text inside your slide? Does it error if you remove the richtext field?

thoerr commented 3 weeks ago

@paulpopus The error only happens when live preview is on, and removing the rich text does not stop the issue for me.

On the front-end, I'm not serializing the editor json until after it passes through useLivePreview and makes its way to a deeper component; I also confirmed the slides object fetched on the front-end matches 1:1 what I see in the api tab on the back-end.

The only way I get the live preview working is by sending an empty object through initialData instead of the fetched lesson data -- but of course the front-end doesn't work then, since it doesn't have data without being in live preview mode.

I've also tried playing with the depth prop in both useLivePreview() and in my front-end data fetching without success.

Here's some more snippets from my set-up:

Full collection config:


const Lessons: CollectionConfig = {
  slug: 'lessons',
  admin: {
    useAsTitle: 'title',
    group: 'Course Data',
    livePreview: {
      url: ({ data }) => `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/lesson/${data.id}?slide=1`,
    },
  },
  versions: {
    drafts: true,
    maxPerDoc: 5,
  },
  fields: [
    {
      type: 'row',
      fields: [
        {
          name: 'title',
          type: 'text',
          label: 'Title',
          required: true,
          admin: {
            width: '60%',
          },
        },
        {
          name: 'slug',
          hooks: {
            beforeValidate: [formatSlug('title')],
          },
          index: true,
          label: 'Slug',
          type: 'text',
          admin: {
            width: '30%',
          },
        },
      ],
    },
    {
      name: 'course',
      label: 'Course',
      relationTo: 'courses',
      type: 'relationship',
    },
    {
      name: 'slides',
      type: 'array',
      label: 'Slides',
      minRows: 1,
      fields: [
        {
          name: 'slideTitle',
          type: 'text',
          label: 'Slide Title',
          required: true,
        },
        {
          name: 'content',
          label: 'Content',
          type: 'richText',
          editor: lexicalEditor({
            features: () => [
              InlineToolbarFeature(),
              LinkFeature(),
              BoldFeature(),
              ItalicFeature(),
              UnderlineFeature(),
              IndentFeature(),
              BlockquoteFeature(),
              ParagraphFeature(),
              OrderedListFeature(),
              UnorderedListFeature(),
              UploadFeature(),
              HorizontalRuleFeature(),
              HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
              BlocksFeature({ blocks: [QuestionsBlock, VideoEmbedBlock] }),
            ],
          }),
        },
      ],
    },
    organizationsField(),
    createdByField(),
  ],
  hooks: organizationHooks(createdByHooks()),
  access: organizationAccess(),
};

admin.livePreview in payload config:

livePreview: {
      breakpoints: [
        {
          name: 'mobile',
          height: 667,
          label: 'Mobile',
          width: 375,
        },
      ],
    },

data fetch from front-end redux thunk:

export const getLesson = createAsyncThunk(
  'getLesson',
  async ({ lessonId }: { lessonId: string }): Promise<Lesson[]> =>
    await axiosRequest({
      url: `/api/lessons?where[id][equals]=${lessonId}&depth=1`,
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    })
      .then((response) => response.data)
      .then((response) => {
        const lessons = response.docs;
        return lessons;
      })
      .catch((error) => {
        throw error;
      })
);

Front-end page excerpt lesson/[lessonId]/page.tsx

const Page = () => {

  // ...
  const { lessonId }: { lessonId: string } = useParams();

const lessons = useSelector((state: any) => {
    return state.lessons.data;
  });

  const { data, isLoading }: any = useLivePreview({
    initialData: lessons[lessonId] ? lessons[lessonId] : {},
    serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '',
    depth: 1,
  });

  // ...
}
export default Page;
thoerr commented 2 weeks ago

@paulpopus I did some more experimentation, and it seems that if I create a new collection with identical configuration, apart from function name and collection slug, it does work. I can create new and edit existing Lessons and the live preview does not fail.

However, creating new or editing existing Lessons in the original collection will always fail. Not sure what would cause this, but I can just delete the existing old collection and use the new one; we're still in development so no live data has been saved yet. The original collection was created on a beta in the 70s and has undergone some changes along the way. I can't make out why it wouldn't work for new items in the collection if it's working in the copy of the collection if the configuration is the same though.

I'll go ahead an close this though.

paulpopus commented 2 weeks ago

@thoerr I would test with a fresh database, you may have abandoned/dead items or properties in your existing one. I assume you're using mongodb where this is more common

thoerr commented 2 weeks ago

@paulpopus I was actually mistaken -- I hadn't updated the data fetching on the front-end to reflect the "new" collection, so it was passing through an empty object (which of course works since there's no existing properties to modify).

BUT what I did find that works is putting the data in useLivePreview() inside an array:

  const { data, isLoading }: any = useLivePreview({
    initialData: [lesson],
    serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '',
    depth: 1,
  });

Where lesson is the document I'm editing. I didn't expect this since the live preview demo appears to only pass through the document object (https://github.com/payloadcms/payload/blob/main/examples/live-preview/next-app/app/_api/fetchPage.ts).

The type for initialData appears to be unknown, and the docs don't specify a type for theinitialData.

github-actions[bot] commented 6 days ago

This issue has been automatically locked. Please open a new issue if this issue persists with any additional detail.