ashbuilds / payload-ai

AI Plugin is a powerful extension for the Payload CMS, integrating advanced AI capabilities to enhance content creation and management.
MIT License
47 stars 10 forks source link

TypeError: Cannot read properties of undefined (reading 'toString') #51

Open usmanmaqbool636 opened 16 hours ago

usmanmaqbool636 commented 16 hours ago

3.0.0-beta.111

I encountered a TypeError while using Payload CMS version 3.0.0-beta.111. The error appears in the useHistory.js hook, specifically when calling toString on an undefined property.

TypeError: Cannot read properties of undefined (reading 'toString') at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@ai-stack+payloadcms@3.0.0-beta.111_kbmvfilg3g3w6q46teqh3p4tfu/node_modules/@ai-stack/payloadcms/dist/ui/Compose/hooks/useHistory.js:44:34) at Array.forEach (<anonymous>) at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@ai-stack+payloadcms@3.0.0-beta.111_kbmvfilg3g3w6q46teqh3p4tfu/node_modules/@ai-stack/payloadcms/dist/ui/Compose/hooks/useHistory.js:43:36) at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@ai-stack+payloadcms@3.0.0-beta.111_kbmvfilg3g3w6q46teqh3p4tfu/node_modules/@ai-stack/payloadcms/dist/ui/Compose/hooks/useHistory.js:57:9) at react-stack-bottom-frame (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0-canary.186_@opentelemetry+api@1.9.0_react-dom@19.0.0-rc-3edc000d-20240926_react@1_eik4yb4jmqrehd77rh7hgfk6nu/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:22442:20)

payload.config.ts
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { uploadthingStorage } from '@payloadcms/storage-uploadthing';
import { seoPlugin } from '@payloadcms/plugin-seo'
import { payloadAiPlugin, PayloadAiPluginLexicalEditorFeature } from '@ai-stack/payloadcms';
import {
  BoldFeature,
  ItalicFeature,
  LinkFeature,
  lexicalEditor,
  OrderedListFeature,
  UnorderedListFeature,
} from '@payloadcms/richtext-lexical'
import sharp from 'sharp' // editor-import
import { UnderlineFeature } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'

import Categories from './collections/Categories'
import { Media } from './collections/Media'
import { Posts } from './collections/Posts'
import Users from './collections/Users'
import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
import { Post } from 'src/payload-types'

import SubscribeEmails from './collections/SubscribeEmails'
import { Banners } from './collections/banner';

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

const generateTitle: GenerateTitle<Post> = ({ doc }) => {
  return doc?.title ? `${doc.title} | EZ Launch` : 'EZ Launch'
}

const generateURL: GenerateURL<Post> = ({ doc }) => {
  return doc?.slug
    ? `${process.env.NEXT_PUBLIC_SERVER_URL!}/${doc.slug}`
    : process.env.NEXT_PUBLIC_SERVER_URL!
}

export default buildConfig({
  admin: {
    components: {},
    importMap: {
      baseDir: path.resolve(dirname),
    },
    user: Users.slug,
    livePreview: {
      breakpoints: [
        {
          label: 'Mobile',
          name: 'mobile',
          width: 375,
          height: 667,
        },
        {
          label: 'Tablet',
          name: 'tablet',
          width: 768,
          height: 1024,
        },
        {
          label: 'Desktop',
          name: 'desktop',
          width: 1440,
          height: 900,
        },
      ],
    },
  },
  // This config helps us configure global or default features that the other editors can inherit
  editor: lexicalEditor({
    features: ({rootFeatures, defaultFeatures}) => {
      return [
        ...defaultFeatures,
        UnderlineFeature(),
        BoldFeature(),
        ItalicFeature(),
        OrderedListFeature(),
        UnorderedListFeature(),
        LinkFeature({
          enabledCollections: ['posts'],
          fields: ({ defaultFields }) => {
            const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
              if ('name' in field && field.name === 'url') return false
              return true
            })

            return [
              ...defaultFieldsWithoutUrl,
              {
                name: 'url',
                type: 'text',
                admin: {
                  condition: ({ linkType }) => linkType !== 'internal',
                },
                label: ({ t }) => t('fields:enterURL'),
                required: true,
              },
            ]
          },
        }),
      ]
    },
  }),
  db: mongooseAdapter({
    url: process.env.DATABASE_URI || '',
  }),
  collections: [Posts, Media, Categories, Banners, Users, SubscribeEmails],
  cors: [process.env.PAYLOAD_PUBLIC_SERVER_URL || ''].filter(Boolean),
  csrf: [process.env.PAYLOAD_PUBLIC_SERVER_URL || ''].filter(Boolean),
  endpoints: [],
  plugins: [
    seoPlugin({
      generateTitle,
      generateURL,
    }),
    uploadthingStorage({
      collections: {
        media: true,
      },
      options: {
        apiKey: process.env.UPLOADTHING_SECRET,
        logLevel: "error",
        // appId: process.env.UPLOADTHING_APP_ID,
        acl: 'public-read',
      },
    }),
    payloadAiPlugin({
      collections: {
        [Posts.slug]: true,
      },
      debugging: false,
    }),
  ],
  secret: process.env.PAYLOAD_SECRET!,
  sharp,
  typescript: {
    outputFile: path.resolve(dirname, 'payload-types.ts'),
  },
})

posts.ts (collection)

import React from 'react'
import type { CollectionConfig } from 'payload'

import {
  FixedToolbarFeature,
  HeadingFeature,
  HorizontalRuleFeature,
  HTMLConverterFeature,
  InlineToolbarFeature,
  lexicalEditor,
  lexicalHTML,
} from '@payloadcms/richtext-lexical'

import { authenticated } from '../../access/authenticated'
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
import { populateAuthors } from './hooks/populateAuthors'
import { revalidatePost } from './hooks/revalidatePost'

import {
  MetaDescriptionField,
  MetaImageField,
  MetaTitleField,
  OverviewField,
  PreviewField,
} from '@payloadcms/plugin-seo/fields'
import { slugField } from '@/fields/slug'
import { PayloadAiPluginLexicalEditorFeature } from '@ai-stack/payloadcms'

export const Posts: CollectionConfig = {
  slug: 'posts',
  access: {
    create: authenticated,
    delete: authenticated,
    read: authenticatedOrPublished,
    update: authenticated,
  },
  admin: {
    defaultColumns: ['title', 'slug', 'updatedAt'],
    useAsTitle: 'title',
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
      unique: true
    },
    {
      name: "template",
      label: "Select Template",
      type: "radio",
      options: [
        { label: "One", value: "one" },
        { label: "Two", value: "two" },
        { label: "Three", value: "three" },
      ],
      defaultValue: "one",
      required: true
    },
    {
      type: 'ui',
      name: 'templateSkeleton',
      admin: {
        components: {
          Field: {
            path: '@/fields/post/Preview',
            clientProps: {
            },
          },
        },
      },
    },
    {
      name: 'coverPhoto',
      type: 'upload',
      relationTo: 'media',
      required: true,
    },
    {
      type: 'tabs',
      tabs: [
        {
          fields: [
            {
              name: 'content',
              type: 'richText',
              editor: lexicalEditor({
                features: ({ rootFeatures }) => {
                  return [
                    ...rootFeatures,
                    HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
                    FixedToolbarFeature(),
                    InlineToolbarFeature(),
                    HorizontalRuleFeature(),
                    HTMLConverterFeature(),
                    PayloadAiPluginLexicalEditorFeature(),
                  ]
                },
              }),
              label: false,
              required: true,
            },
            lexicalHTML('content', { name: 'content_html', storeInDB: false }),
            {
              name: 'mediaType',
              type: 'select',
              options: [
                { label: 'YouTube Video', value: 'youtube-video' },
                { label: 'Image Slider', value: 'image-slider' },
              ],
              required: true,
            },
            {
              name: 'youtubeVideoUrl', // YouTube video URL field
              type: 'text',
              required: true,
              admin: {
                condition: (_, { mediaType }) => mediaType === 'youtube-video', // Show only if 'youtube-video' is selected
              },
              validate: (value) => {
                const youtubeRegex = /^(https?\:\/\/)?(www\.youtube\.com|youtu\.be)\/.+$/;
                if (!youtubeRegex.test(value)) {
                  return 'Please enter a valid YouTube URL.';
                }
                return true;
              },
            },
            {
              name: 'imageSlider', // Image slider field
              type: 'array',
              minRows: 1,
              maxRows: 10,
              fields: [
                {
                  name: 'image',
                  type: 'upload',
                  relationTo: 'media',
                  required: true,
                },
                {
                  name: 'caption',
                  type: 'text',
                },
              ],
              admin: {
                condition: (_, { mediaType }) => mediaType === 'image-slider', // Show only if 'image-slider' is selected
              },
            },
            {
              name: 'midImage', // Mid image field (only required for template "two")
              type: 'upload',
              relationTo: 'media',
              admin: {
                condition: (_, { template }) => template === 'two', // Only show if "two" is selected
              },
              required: true, // Only required if "two" is selected
            },
            {
              name: 'tags',
              type: 'array',
              label: 'Tags',
              labels: {
                singular: 'Tag',
                plural: 'Tags',
              },
              fields: [
                {
                  name: 'tag',
                  type: 'text',
                  label: 'Tag',
                  required: true,
                },
              ],
            },
          ],
          label: 'Content',
        },
        {
          name: 'meta',
          label: 'SEO',
          fields: [
            OverviewField({
              titlePath: 'meta.title',
              descriptionPath: 'meta.description',
              imagePath: 'meta.image',
            }),
            MetaTitleField({
              hasGenerateFn: true,
            }),
            MetaImageField({
              relationTo: 'media',
            }),

            MetaDescriptionField({}),
            PreviewField({
              // if the `generateUrl` function is configured
              hasGenerateFn: true,

              // field paths to match the target field for data
              titlePath: 'meta.title',
              descriptionPath: 'meta.description',
            }),
          ],
        },
      ],
    },
    {
      name: 'categories',
      type: 'relationship',
      admin: {
        position: 'sidebar',
      },
      hasMany: true,
      relationTo: 'categories',
    },
    {
      name: 'classification',
      type: 'select',
      admin: { position: 'sidebar' },
      options: [
        {
          label: 'Trending',
          value: 'trending',
        },
        {
          label: 'Latest',
          value: 'new',
        }
      ],
      defaultValue: 'new',
      required: true,
    },
    {
      name: 'publishedAt',
      type: 'date',
      admin: {
        date: {
          pickerAppearance: 'dayAndTime',
        },
        position: 'sidebar',
      },
      hooks: {
        beforeChange: [
          ({ siblingData, value }) => {
            if (siblingData._status === 'published' && !value) {
              return new Date()
            }
            return value
          },
        ],
      },
    },
    {
      name: 'authors',
      type: 'relationship',
      admin: {
        position: 'sidebar',
      },
      relationTo: 'users',
    },
    {
      name: 'populatedAuthors',
      type: 'array',
      access: {
        update: () => false,
      },
      admin: {
        disabled: true,
        readOnly: true,
      },
      fields: [
        {
          name: 'id',
          type: 'text',
        },
        {
          name: 'name',
          type: 'text',
        },
      ],
    },
    ...slugField(),
  ],
  hooks: {
    afterChange: [revalidatePost],
    afterRead: [populateAuthors],
  },
  versions: {
    drafts: {
      // autosave: {
      //   interval: 100, // We set this interval for optimal live preview
      // },
    },
    maxPerDoc: 50,
  },
}
usmanmaqbool636 commented 16 hours ago

node_modules/@ai-stack/payloadcms/dist/ui/Compose/hooks/useHistory.js

I was able to resolve the issue by adding the optional chaining operator id?.toString() in the following block of code from useHistory.js:

        Object.keys(latestHistory).forEach((k)=>{
            if (!k.startsWith(id?.toString())) {
                delete latestHistory[k];
            }
        });
ashbuilds commented 5 hours ago

Hi @usmanmaqbool636! Thanks for reporting, would you like to submit a PR?

usmanmaqbool636 commented 4 hours ago

sure

usmanmaqbool636 commented 3 hours ago

Hey @ashbuilds , I’ve created the PR. Please take a look when you get a chance https://github.com/ashbuilds/payload-ai/pull/52