fix(templates): website bug fixes for slug generation and form builder and adds support for strictNullChecks: true (#7877)

This commit is contained in:
Paul
2024-08-26 16:10:09 -06:00
committed by GitHub
parent fed7f2fa5b
commit 05bf52aac3
32 changed files with 10390 additions and 131 deletions

View File

@@ -18,16 +18,16 @@
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/db-mongodb": "3.0.0-beta.84",
"@payloadcms/live-preview-react": "3.0.0-beta.84",
"@payloadcms/next": "3.0.0-beta.84",
"@payloadcms/plugin-cloud": "3.0.0-beta.84",
"@payloadcms/plugin-form-builder": "3.0.0-beta.84",
"@payloadcms/plugin-nested-docs": "3.0.0-beta.84",
"@payloadcms/plugin-redirects": "3.0.0-beta.84",
"@payloadcms/plugin-seo": "3.0.0-beta.84",
"@payloadcms/richtext-lexical": "3.0.0-beta.84",
"@payloadcms/ui": "3.0.0-beta.84",
"@payloadcms/db-mongodb": "3.0.0-beta.91",
"@payloadcms/live-preview-react": "3.0.0-beta.91",
"@payloadcms/next": "3.0.0-beta.91",
"@payloadcms/plugin-cloud": "3.0.0-beta.91",
"@payloadcms/plugin-form-builder": "3.0.0-beta.91",
"@payloadcms/plugin-nested-docs": "3.0.0-beta.91",
"@payloadcms/plugin-redirects": "3.0.0-beta.91",
"@payloadcms/plugin-seo": "3.0.0-beta.91",
"@payloadcms/richtext-lexical": "3.0.0-beta.91",
"@payloadcms/ui": "3.0.0-beta.91",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
@@ -41,7 +41,7 @@
"lexical": "0.17.0",
"lucide-react": "^0.378.0",
"next": "15.0.0-canary.104",
"payload": "3.0.0-beta.84",
"payload": "3.0.0-beta.91",
"payload-admin-bar": "^1.0.6",
"prism-react-renderer": "^2.3.1",
"react": "19.0.0-rc-06d0b89e-20240801",
@@ -56,6 +56,7 @@
"@payloadcms/eslint-config": "^1.1.1",
"@tailwindcss/typography": "^0.5.13",
"@types/escape-html": "^1.0.2",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "18.11.3",
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",

10230
templates/website/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -50,16 +50,22 @@ export default async function Post({ params: { slug = '' } }) {
/>
</div>
{post.relatedPosts && post.relatedPosts.length > 0 && (
<RelatedPosts
className="mt-12"
docs={post.relatedPosts.filter((post) => typeof post === 'object')}
/>
)}
</div>
</article>
)
}
export async function generateMetadata({ params: { slug } }): Promise<Metadata> {
export async function generateMetadata({
params: { slug },
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await queryPostBySlug({ slug })
return generateMeta({ doc: post })

View File

@@ -39,7 +39,9 @@ export default async function Page() {
<CollectionArchive posts={posts.docs} />
<div className="container">
{posts.totalPages > 1 && <Pagination page={posts.page} totalPages={posts.totalPages} />}
{posts.totalPages > 1 && posts.page && (
<Pagination page={posts.page} totalPages={posts.totalPages} />
)}
</div>
</div>
)

View File

@@ -40,7 +40,9 @@ export default async function Page({ params: { pageNumber = 2 } }) {
<CollectionArchive posts={posts.docs} />
<div className="container">
{posts.totalPages > 1 && <Pagination page={posts.page} totalPages={posts.totalPages} />}
{posts.totalPages > 1 && posts.page && (
<Pagination page={posts.page} totalPages={posts.totalPages} />
)}
</div>
</div>
)
@@ -60,7 +62,7 @@ export async function generateStaticParams() {
limit: 10,
})
const pages = []
const pages: number[] = []
for (let i = 1; i <= posts.totalPages; i++) {
pages.push(i)

View File

@@ -19,24 +19,23 @@ import { default as default_17 } from 'src/payload/components/BeforeDashboard'
import { default as default_18 } from 'src/payload/components/BeforeLogin'
export const importMap = {
'@payloadcms/richtext-lexical/client#RichTextCell': RichTextCell_0,
'@payloadcms/richtext-lexical/client#RichTextField': RichTextField_1,
'@payloadcms/richtext-lexical/generateComponentMap#getGenerateComponentMap':
getGenerateComponentMap_2,
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient': InlineToolbarFeatureClient_3,
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient': FixedToolbarFeatureClient_4,
'@payloadcms/richtext-lexical/client#HeadingFeatureClient': HeadingFeatureClient_5,
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient': UnderlineFeatureClient_6,
'@payloadcms/richtext-lexical/client#BoldFeatureClient': BoldFeatureClient_7,
'@payloadcms/richtext-lexical/client#ItalicFeatureClient': ItalicFeatureClient_8,
'@payloadcms/richtext-lexical/client#LinkFeatureClient': LinkFeatureClient_9,
'@payloadcms/plugin-seo/client#OverviewComponent': OverviewComponent_10,
'@payloadcms/plugin-seo/client#MetaTitleComponent': MetaTitleComponent_11,
'@payloadcms/plugin-seo/client#MetaImageComponent': MetaImageComponent_12,
'@payloadcms/plugin-seo/client#MetaDescriptionComponent': MetaDescriptionComponent_13,
'@payloadcms/plugin-seo/client#PreviewComponent': PreviewComponent_14,
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient': HorizontalRuleFeatureClient_15,
'@payloadcms/richtext-lexical/client#BlocksFeatureClient': BlocksFeatureClient_16,
'/payload/components/BeforeDashboard#default': default_17,
'/payload/components/BeforeLogin#default': default_18,
"@payloadcms/richtext-lexical/client#RichTextCell": RichTextCell_0,
"@payloadcms/richtext-lexical/client#RichTextField": RichTextField_1,
"@payloadcms/richtext-lexical/generateComponentMap#getGenerateComponentMap": getGenerateComponentMap_2,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_3,
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_4,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_5,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_6,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_7,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_8,
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_9,
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_10,
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_11,
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_12,
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_13,
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_14,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_15,
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_16,
"/payload/components/BeforeDashboard#default": default_17,
"/payload/components/BeforeLogin#default": default_18
}

View File

@@ -14,14 +14,16 @@ export const ArchiveBlock: React.FC<
id?: string
}
> = async (props) => {
const { id, categories, introContent, limit = 3, populateBy, selectedDocs } = props
const { id, categories, introContent, limit: limitFromProps, populateBy, selectedDocs } = props
const limit = limitFromProps || 3
let posts: Post[] = []
if (populateBy === 'collection') {
const payload = await getPayloadHMR({ config: configPromise })
const flattenedCategories = categories.map((category) => {
const flattenedCategories = categories?.map((category) => {
if (typeof category === 'object') return category.id
else return category
})
@@ -43,9 +45,13 @@ export const ArchiveBlock: React.FC<
posts = fetchedPosts.docs
} else {
posts = selectedDocs.map((post) => {
if (selectedDocs?.length) {
const filteredSelectedPosts = selectedDocs.map((post) => {
if (typeof post.value === 'object') return post.value
})
}) as Post[]
posts = filteredSelectedPosts
}
}
return (

View File

@@ -16,7 +16,7 @@ export const CallToActionBlock: React.FC<
<div className="container">
<div className="bg-card rounded border-border border p-4 flex flex-col gap-8 md:flex-row md:justify-between md:items-center">
<div className="max-w-[48rem] flex items-center">
<RichText className="" content={richText} enableGutter={false} />
{richText && <RichText className="mb-0" content={richText} enableGutter={false} />}
</div>
<div className="flex flex-col gap-8">
{(links || []).map(({ link }, i) => {

View File

@@ -7,7 +7,7 @@ type Props = {
language?: string
}
export const Code: React.FC<Props> = ({ code, language }) => {
export const Code: React.FC<Props> = ({ code, language = '' }) => {
return (
<Highlight code={code} language={language} theme={themes.vsDark}>
{({ getLineProps, getTokenProps, tokens }) => (

View File

@@ -32,12 +32,13 @@ export const ContentBlock: React.FC<
return (
<div
className={cn(`col-span-4 lg:col-span-${colsSpanClasses[size]}`, {
className={cn(`col-span-4 lg:col-span-${colsSpanClasses[size!]}`, {
'md:col-span-2': size !== 'full',
})}
key={index}
>
<RichText content={richText} enableGutter={false} />
{richText && <RichText content={richText} enableGutter={false} />}
{enableLink && <CMSLink {...link} />}
</div>
)

View File

@@ -1,6 +1,8 @@
import type { CheckboxField } from '@payloadcms/plugin-form-builder/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import { useFormContext } from 'react-hook-form'
import { Checkbox as CheckboxUi } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import React from 'react'
@@ -20,13 +22,19 @@ export const Checkbox: React.FC<
setValue: any
}
> = ({ name, defaultValue, errors, label, register, required: requiredFromProps, width }) => {
const props = register(name, { required: requiredFromProps })
const { setValue } = useFormContext()
return (
<Width width={width}>
<div className="flex items-center gap-2">
<CheckboxUi
defaultChecked={defaultValue}
id={name}
{...register(name, { required: requiredFromProps })}
{...props}
onCheckedChange={(checked) => {
setValue(props.name, checked)
}}
/>
<Label htmlFor={name}>{label}</Label>
</div>

View File

@@ -1,14 +1,12 @@
import type { MessageField } from '@payloadcms/plugin-form-builder/types'
import RichText from '@/components/RichText'
import React from 'react'
import { Width } from '../Width'
export const Message: React.FC<MessageField> = ({ message }) => {
export const Message: React.FC = ({ message }: { message: Record<string, any> }) => {
return (
<Width className="my-12" width="100">
<RichText content={message} />
{message && <RichText content={message} />}
</Width>
)
}

View File

@@ -5,7 +5,7 @@ export const buildInitialFormState = (fields: FormFieldBlock[]) => {
if (field.blockType === 'checkbox') {
return {
...initialSchema,
[field.name]: false,
[field.name]: field.defaultValue,
}
}
if (field.blockType === 'country') {

View File

@@ -3,7 +3,7 @@ import type { Form as FormType } from '@payloadcms/plugin-form-builder/types'
import { useRouter } from 'next/navigation'
import React, { useCallback, useState } from 'react'
import { useForm } from 'react-hook-form'
import { useForm, FormProvider } from 'react-hook-form'
import RichText from 'src/app/components/RichText'
import { Button } from 'src/app/components/ui/button'
@@ -48,10 +48,8 @@ export const FormBlock: React.FC<
const {
control,
formState: { errors },
getValues,
handleSubmit,
register,
setValue,
} = formMethods
const [isLoading, setIsLoading] = useState(false)
@@ -128,6 +126,7 @@ export const FormBlock: React.FC<
return (
<div className="container max-w-[48rem] pb-20">
<FormProvider {...formMethods}>
{enableIntro && introContent && !hasSubmitted && (
<RichText className="mb-8" content={introContent} enableGutter={false} />
)}
@@ -166,6 +165,7 @@ export const FormBlock: React.FC<
</Button>
</form>
)}
</FormProvider>
</div>
)
}

View File

@@ -15,7 +15,7 @@ interface HeaderClientProps {
export const HeaderClient: React.FC<HeaderClientProps> = ({ header }) => {
/* Storing the value in a useState to avoid hydration errors */
const [theme, setTheme] = useState(null)
const [theme, setTheme] = useState<string | null>(null)
const { headerTheme, setHeaderTheme } = useHeaderTheme()
const pathname = usePathname()
@@ -25,7 +25,7 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ header }) => {
}, [pathname])
useEffect(() => {
if (headerTheme !== theme) setTheme(headerTheme)
if (headerTheme && headerTheme !== theme) setTheme(headerTheme)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [headerTheme])

View File

@@ -9,15 +9,15 @@ type CMSLinkType = {
appearance?: 'inline' | ButtonProps['variant']
children?: React.ReactNode
className?: string
label?: string
newTab?: boolean
label?: string | null
newTab?: boolean | null
reference?: {
relationTo: 'pages' | 'posts'
value: Page | Post | string | number
}
size?: ButtonProps['size']
type?: 'custom' | 'reference'
url?: string
} | null
size?: ButtonProps['size'] | null
type?: 'custom' | 'reference' | null
url?: string | null
}
export const CMSLink: React.FC<CMSLinkType> = (props) => {
@@ -48,7 +48,7 @@ export const CMSLink: React.FC<CMSLinkType> = (props) => {
/* Ensure we don't break any styles set by richText */
if (appearance === 'inline') {
return (
<Link className={cn(className)} href={href || url} {...newTabProps}>
<Link className={cn(className)} href={href || url || ''} {...newTabProps}>
{label && label}
{children && children}
</Link>
@@ -57,7 +57,7 @@ export const CMSLink: React.FC<CMSLinkType> = (props) => {
return (
<Button asChild className={className} size={size} variant={appearance}>
<Link className={cn(className)} href={href || url} {...newTabProps}>
<Link className={cn(className)} href={href || url || ''} {...newTabProps}>
{label && label}
{children && children}
</Link>

View File

@@ -6,6 +6,6 @@ import React from 'react'
export const LivePreviewListener: React.FC = () => {
const router = useRouter()
return (
<PayloadLivePreview refresh={router.refresh} serverURL={process.env.NEXT_PUBLIC_SERVER_URL} />
<PayloadLivePreview refresh={router.refresh} serverURL={process.env.NEXT_PUBLIC_SERVER_URL!} />
)
}

View File

@@ -41,8 +41,8 @@ export const ImageMedia: React.FC<MediaProps> = (props) => {
width: fullWidth,
} = resource
width = fullWidth
height = fullHeight
width = fullWidth!
height = fullHeight!
alt = altFromResource
src = `${process.env.NEXT_PUBLIC_SERVER_URL}${url}`

View File

@@ -19,7 +19,7 @@ export const HighImpactHero: React.FC<Page['hero']> = ({ links, media, richText
<div className="relative -mt-[10.4rem] flex items-end text-white" data-theme="dark">
<div className="container mb-8 z-10 relative">
<div className="max-w-[34rem]">
<RichText className="mb-6" content={richText} enableGutter={false} />
{richText && <RichText className="mb-6" content={richText} enableGutter={false} />}
{Array.isArray(links) && links.length > 0 && (
<ul className="flex gap-4">
{links.map(({ link }, i) => {
@@ -34,7 +34,7 @@ export const HighImpactHero: React.FC<Page['hero']> = ({ links, media, richText
</div>
</div>
<div className="min-h-[80vh] select-none">
{typeof media === 'object' && (
{media && typeof media === 'object' && (
<React.Fragment>
<Media fill imgClassName="-z-10 object-cover" priority resource={media} />
<div className="absolute pointer-events-none left-0 bottom-0 w-full h-1/2 bg-gradient-to-t from-black to-transparent" />

View File

@@ -18,7 +18,7 @@ export const LowImpactHero: React.FC<LowImpactHeroType> = ({ children, richText
return (
<div className="container mt-16">
<div className="max-w-[48rem]">
{children || <RichText content={richText} enableGutter={false} />}
{children || (richText && <RichText content={richText} enableGutter={false} />)}
</div>
</div>
)

View File

@@ -10,7 +10,8 @@ export const MediumImpactHero: React.FC<Page['hero']> = ({ links, media, richTex
return (
<div className="">
<div className="container mb-8">
<RichText className="mb-6" content={richText} enableGutter={false} />
{richText && <RichText className="mb-6" content={richText} enableGutter={false} />}
{Array.isArray(links) && links.length > 0 && (
<ul className="flex gap-4">
{links.map(({ link }, i) => {
@@ -24,7 +25,7 @@ export const MediumImpactHero: React.FC<Page['hero']> = ({ links, media, richTex
)}
</div>
<div className="container ">
{typeof media === 'object' && (
{media && typeof media === 'object' && (
<div>
<Media
className="-mx-4 md:-mx-8 2xl:-mx-16"

View File

@@ -1,6 +1,8 @@
import jwt from 'jsonwebtoken'
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayloadHMR } from '@payloadcms/next/utilities'
import configPromise from '@payload-config'
const payloadToken = 'payload-token'
@@ -13,6 +15,7 @@ export async function GET(
}
},
): Promise<Response> {
const payload = await getPayloadHMR({ config: configPromise })
const token = req.cookies.get(payloadToken)?.value
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
@@ -25,8 +28,9 @@ export async function GET(
new Response('You are not allowed to preview this page', { status: 403 })
}
const user = jwt.decode(token, process.env.PAYLOAD_SECRET)
const user = jwt.verify(token, payload.config.secret)
// You can add additional checks here to see if the user is allowed to preview this page
if (!user) {
draftMode().disable()
return new Response('You are not allowed to preview this page', { status: 403 })

View File

@@ -19,7 +19,7 @@ const initialContext: ContextType = {
const HeaderThemeContext = createContext(initialContext)
export const HeaderThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [headerTheme, setThemeState] = useState<Theme | undefined>(
const [headerTheme, setThemeState] = useState<Theme | undefined | null>(
canUseDOM ? (document.documentElement.getAttribute('data-theme') as Theme) : undefined,
)

View File

@@ -20,7 +20,7 @@ export const generateMeta = async (args: { doc: Page | Post }): Promise<Metadata
return {
description: doc?.meta?.description,
openGraph: mergeOpenGraph({
description: doc?.meta?.description,
description: doc?.meta?.description || '',
images: ogImage
? [
{

View File

@@ -34,8 +34,9 @@ export const getMeUser = async (args?: {
redirect(nullUserRedirect)
}
// Token will exist here because if it doesn't the user will be redirected
return {
token,
token: token!,
user,
}
}

View File

@@ -6,10 +6,10 @@ import { useCallback, useEffect, useRef } from 'react'
type UseClickableCardType<T extends HTMLElement> = {
card: {
ref: RefObject<T>
ref: RefObject<T | null>
}
link: {
ref: RefObject<HTMLAnchorElement>
ref: RefObject<HTMLAnchorElement | null>
}
}

View File

@@ -92,7 +92,7 @@ export interface Page {
id?: string | null;
}[]
| null;
media?: string | Media | null;
media?: (string | null) | Media;
};
layout: (
| {
@@ -231,7 +231,7 @@ export interface Page {
)[];
meta?: {
title?: string | null;
image?: string | Media | null;
image?: (string | null) | Media;
description?: string | null;
};
publishedAt?: string | null;
@@ -319,7 +319,7 @@ export interface Post {
categories?: (string | Category)[] | null;
meta?: {
title?: string | null;
image?: string | Media | null;
image?: (string | null) | Media;
description?: string | null;
};
publishedAt?: string | null;

View File

@@ -41,8 +41,8 @@ const generateTitle: GenerateTitle<Post | Page> = ({ doc }) => {
const generateURL: GenerateURL<Post | Page> = ({ doc }) => {
return doc?.slug
? `${process.env.NEXT_PUBLIC_SERVER_URL}/${doc.slug}`
: process.env.NEXT_PUBLIC_SERVER_URL
? `${process.env.NEXT_PUBLIC_SERVER_URL!}/${doc.slug}`
: process.env.NEXT_PUBLIC_SERVER_URL!
}
export default buildConfig({
@@ -116,7 +116,7 @@ export default buildConfig({
}),
// database-adapter-config-start
db: mongooseAdapter({
url: process.env.DATABASE_URI,
url: process.env.DATABASE_URI!,
}),
// database-adapter-config-end
collections: [Pages, Posts, Media, Categories, Users],
@@ -190,7 +190,7 @@ export default buildConfig({
}),
payloadCloudPlugin(), // storage-adapter-placeholder
],
secret: process.env.PAYLOAD_SECRET,
secret: process.env.PAYLOAD_SECRET!,
sharp,
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),

View File

@@ -5,7 +5,5 @@ import type { User } from '../../payload-types'
type isAuthenticated = (args: AccessArgs<User>) => boolean
export const authenticated: isAuthenticated = ({ req: { user } }) => {
if (user) {
return true
}
return Boolean(user)
}

View File

@@ -1,4 +1,5 @@
import type { CollectionAfterReadHook } from 'payload'
import { User } from 'src/payload-types'
// The `user` collection has access control locked so that users are not publicly accessible
// This means that we need to populate the authors manually here to protect user privacy
@@ -6,7 +7,7 @@ import type { CollectionAfterReadHook } from 'payload'
// So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI
export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => {
if (doc?.authors) {
const authorDocs = []
const authorDocs: User[] = []
for (const author of doc.authors) {
const authorDoc = await payload.findByID({

View File

@@ -13,8 +13,8 @@ const formatSlug =
return format(value)
}
if (operation === 'create') {
const fallbackData = data?.[fallback] || originalDoc?.[fallback]
if (operation === 'create' || !data?.slug) {
const fallbackData = data?.[fallback] || data?.[fallback]
if (fallbackData && typeof fallbackData === 'string') {
return format(fallbackData)

View File

@@ -11,6 +11,7 @@
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,