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 { MetaDescriptionComponent as MetaDescriptionComponent_13 } 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 { BlocksFeatureClient as BlocksFeatureClient_16 } from '@payloadcms/richtext-lexical/client'
import { default as default_17 } from 'src/payload/components/BeforeDashboard'
import { default as default_18 } from 'src/payload/components/BeforeLogin'
import { SlugComponent as SlugComponent_15 } from '@/payload/fields/slug/SlugComponent'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_16 } from '@payloadcms/richtext-lexical/client'
import { BlocksFeatureClient as BlocksFeatureClient_17 } from '@payloadcms/richtext-lexical/client'
import { default as default_18 } from 'src/payload/components/BeforeDashboard'
import { default as default_19 } 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,
'@/payload/fields/slug/SlugComponent#SlugComponent': SlugComponent_15,
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient': HorizontalRuleFeatureClient_16,
'@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;
slug?: string | null;
slugLock?: boolean | null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
@@ -331,6 +332,7 @@ export interface Post {
}[]
| null;
slug?: string | null;
slugLock?: boolean | null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;

View File

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

View File

@@ -14,7 +14,6 @@ import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
import { Banner } from '../../blocks/Banner'
import { Code } from '../../blocks/Code'
import { MediaBlock } from '../../blocks/MediaBlock'
import { slugField } from '../../fields/slug'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { populateAuthors } from './hooks/populateAuthors'
import { revalidatePost } from './hooks/revalidatePost'
@@ -26,6 +25,7 @@ import {
OverviewField,
PreviewField,
} from '@payloadcms/plugin-seo/fields'
import { slugField } from '@/payload/fields/slug'
export const Posts: CollectionConfig = {
slug: 'posts',
@@ -193,7 +193,7 @@ export const Posts: CollectionConfig = {
},
],
},
slugField(),
...slugField(),
],
hooks: {
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'
const format = (val: string): string =>
export const formatSlug = (val: string): string =>
val
.replace(/ /g, '-')
.replace(/[^\w-]+/g, '')
.toLowerCase()
const formatSlug =
export const formatSlugHook =
(fallback: string): FieldHook =>
({ data, operation, originalDoc, value }) => {
if (typeof value === 'string') {
return format(value)
return formatSlug(value)
}
if (operation === 'create' || !data?.slug) {
const fallbackData = data?.[fallback] || data?.[fallback]
if (fallbackData && typeof fallbackData === 'string') {
return format(fallbackData)
return formatSlug(fallbackData)
}
}
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/*"
],
"@/payload/*": [
"./src/payload/*"
]
}
},