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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -102,7 +102,7 @@ export const Pages: CollectionConfig = {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
slugField(),
|
||||
...slugField(),
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [revalidatePage],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
73
templates/website/src/payload/fields/slug/SlugComponent.tsx
Normal file
73
templates/website/src/payload/fields/slug/SlugComponent.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
12
templates/website/src/payload/fields/slug/index.scss
Normal file
12
templates/website/src/payload/fields/slug/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
54
templates/website/src/payload/fields/slug/index.ts
Normal file
54
templates/website/src/payload/fields/slug/index.ts
Normal 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]
|
||||
}
|
||||
@@ -35,6 +35,9 @@
|
||||
],
|
||||
"@/*": [
|
||||
"./src/app/*"
|
||||
],
|
||||
"@/payload/*": [
|
||||
"./src/payload/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user