feat(templates): add new slug component to the website (#7895)

https://github.com/user-attachments/assets/1ba125d3-9c65-4bab-98df-fb80c70eeb71
This commit is contained in:
Paul
2024-08-27 16:26:56 -06:00
committed by GitHub
parent de7ff1f8c6
commit 5d97d57e70
10 changed files with 177 additions and 55 deletions

View File

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

View File

@@ -236,6 +236,7 @@ export interface Page {
}; };
publishedAt?: string | null; publishedAt?: string | null;
slug?: string | null; slug?: string | null;
slugLock?: boolean | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
_status?: ('draft' | 'published') | null; _status?: ('draft' | 'published') | null;
@@ -331,6 +332,7 @@ export interface Post {
}[] }[]
| null; | null;
slug?: string | null; slug?: string | null;
slugLock?: boolean | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
_status?: ('draft' | 'published') | null; _status?: ('draft' | 'published') | null;

View File

@@ -102,7 +102,7 @@ export const Pages: CollectionConfig = {
position: 'sidebar', position: 'sidebar',
}, },
}, },
slugField(), ...slugField(),
], ],
hooks: { hooks: {
afterChange: [revalidatePage], afterChange: [revalidatePage],

View File

@@ -14,7 +14,6 @@ import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
import { Banner } from '../../blocks/Banner' import { Banner } from '../../blocks/Banner'
import { Code } from '../../blocks/Code' import { Code } from '../../blocks/Code'
import { MediaBlock } from '../../blocks/MediaBlock' import { MediaBlock } from '../../blocks/MediaBlock'
import { slugField } from '../../fields/slug'
import { generatePreviewPath } from '../../utilities/generatePreviewPath' import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { populateAuthors } from './hooks/populateAuthors' import { populateAuthors } from './hooks/populateAuthors'
import { revalidatePost } from './hooks/revalidatePost' import { revalidatePost } from './hooks/revalidatePost'
@@ -26,6 +25,7 @@ import {
OverviewField, OverviewField,
PreviewField, PreviewField,
} from '@payloadcms/plugin-seo/fields' } from '@payloadcms/plugin-seo/fields'
import { slugField } from '@/payload/fields/slug'
export const Posts: CollectionConfig = { export const Posts: CollectionConfig = {
slug: 'posts', slug: 'posts',
@@ -193,7 +193,7 @@ export const Posts: CollectionConfig = {
}, },
], ],
}, },
slugField(), ...slugField(),
], ],
hooks: { hooks: {
afterChange: [revalidatePost], afterChange: [revalidatePost],

View File

@@ -1,23 +0,0 @@
import type { Field } from 'payload'
import deepMerge from '../utilities/deepMerge'
import formatSlug from '../utilities/formatSlug'
type Slug = (fieldToUse?: string, overrides?: Partial<Field>) => Field
export const slugField: Slug = (fieldToUse = 'title', overrides = {}) =>
deepMerge<Field, Partial<Field>>(
{
name: 'slug',
type: 'text',
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [formatSlug(fieldToUse)],
},
index: true,
label: 'Slug',
},
overrides,
)

View File

@@ -0,0 +1,73 @@
'use client'
import React, { useCallback, useEffect } from 'react'
import {
useField,
useFieldProps,
Button,
TextInput,
FieldLabel,
useFormFields,
} from '@payloadcms/ui'
import type { TextFieldProps } from 'payload'
import { formatSlug } from './formatSlug'
import './index.scss'
type SlugComponentProps = {
fieldToUse: string
checkboxFieldPath: string
} & TextFieldProps
export const SlugComponent: React.FC<SlugComponentProps> = ({
field,
fieldToUse,
checkboxFieldPath: checkboxFieldPathFromProps,
}) => {
const { label } = field
const { path, readOnly: readOnlyFromProps } = useFieldProps()
const checkboxFieldPath = path.includes('.')
? `${path}.${checkboxFieldPathFromProps}`
: checkboxFieldPathFromProps
const { value, setValue } = useField<string>({ path })
const { value: checkboxValue, setValue: setCheckboxValue } = useField<boolean>({
path: checkboxFieldPath,
})
const fieldToUseValue = useFormFields(([fields, dispatch]) => {
return fields[fieldToUse].value as string
})
useEffect(() => {
if (checkboxValue) setValue(formatSlug(fieldToUseValue))
}, [fieldToUseValue, checkboxValue])
const handleLock = useCallback(
(e) => {
e.preventDefault()
setCheckboxValue(!checkboxValue)
},
[checkboxValue, setCheckboxValue],
)
const readOnly = readOnlyFromProps || checkboxValue
return (
<div className="field-type slug-field-component">
<div className="label-wrapper">
<FieldLabel field={field} htmlFor={`field-${path}`} label={label} />
<Button className="lock-button" buttonStyle="none" onClick={handleLock}>
{checkboxValue ? 'Unlock' : 'Lock'}
</Button>
</div>
<TextInput label={''} value={value} onChange={setValue} path={path} readOnly={readOnly} />
</div>
)
}

View File

@@ -1,27 +1,25 @@
import type { FieldHook } from 'payload' import type { FieldHook } from 'payload'
const format = (val: string): string => export const formatSlug = (val: string): string =>
val val
.replace(/ /g, '-') .replace(/ /g, '-')
.replace(/[^\w-]+/g, '') .replace(/[^\w-]+/g, '')
.toLowerCase() .toLowerCase()
const formatSlug = export const formatSlugHook =
(fallback: string): FieldHook => (fallback: string): FieldHook =>
({ data, operation, originalDoc, value }) => { ({ data, operation, originalDoc, value }) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return format(value) return formatSlug(value)
} }
if (operation === 'create' || !data?.slug) { if (operation === 'create' || !data?.slug) {
const fallbackData = data?.[fallback] || data?.[fallback] const fallbackData = data?.[fallback] || data?.[fallback]
if (fallbackData && typeof fallbackData === 'string') { if (fallbackData && typeof fallbackData === 'string') {
return format(fallbackData) return formatSlug(fallbackData)
} }
} }
return value return value
} }
export default formatSlug

View File

@@ -0,0 +1,12 @@
.slug-field-component {
.label-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
.lock-button {
margin: 0;
padding-bottom: 0.3125rem;
}
}

View File

@@ -0,0 +1,54 @@
import type { CheckboxField, TextField } from 'payload'
import { formatSlugHook } from './formatSlug'
type Overrides = {
slugOverrides?: Partial<TextField>
checkboxOverrides?: Partial<CheckboxField>
}
type Slug = (fieldToUse?: string, overrides?: Overrides) => [TextField, CheckboxField]
export const slugField: Slug = (fieldToUse = 'title', overrides = {}) => {
const { slugOverrides, checkboxOverrides } = overrides
const checkBoxField: CheckboxField = {
name: 'slugLock',
type: 'checkbox',
defaultValue: true,
admin: {
hidden: true,
position: 'sidebar',
},
...checkboxOverrides,
}
// Expect ts error here because of typescript mismatching Partial<TextField> with TextField
// @ts-expect-error
const slugField: TextField = {
name: 'slug',
type: 'text',
index: true,
label: 'Slug',
...(slugOverrides || {}),
hooks: {
// Kept this in for hook or API based updates
beforeValidate: [formatSlugHook(fieldToUse)],
},
admin: {
position: 'sidebar',
...(slugOverrides?.admin || {}),
components: {
Field: {
path: '@/payload/fields/slug/SlugComponent#SlugComponent',
clientProps: {
fieldToUse,
checkboxFieldPath: checkBoxField.name,
},
},
},
},
}
return [slugField, checkBoxField]
}

View File

@@ -35,6 +35,9 @@
], ],
"@/*": [ "@/*": [
"./src/app/*" "./src/app/*"
],
"@/payload/*": [
"./src/payload/*"
] ]
} }
}, },