Skip to content

Commit 5d97d57

Browse files
authored
feat(templates): add new slug component to the website (#7895)
https://github.com/user-attachments/assets/1ba125d3-9c65-4bab-98df-fb80c70eeb71
1 parent de7ff1f commit 5d97d57

File tree

10 files changed

+177
-55
lines changed

10 files changed

+177
-55
lines changed

templates/website/src/app/(payload)/admin/importMap.js

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,32 @@ import { MetaTitleComponent as MetaTitleComponent_11 } from '@payloadcms/plugin-
1313
import { MetaImageComponent as MetaImageComponent_12 } from '@payloadcms/plugin-seo/client'
1414
import { MetaDescriptionComponent as MetaDescriptionComponent_13 } from '@payloadcms/plugin-seo/client'
1515
import { PreviewComponent as PreviewComponent_14 } from '@payloadcms/plugin-seo/client'
16-
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_15 } from '@payloadcms/richtext-lexical/client'
17-
import { BlocksFeatureClient as BlocksFeatureClient_16 } from '@payloadcms/richtext-lexical/client'
18-
import { default as default_17 } from 'src/payload/components/BeforeDashboard'
19-
import { default as default_18 } from 'src/payload/components/BeforeLogin'
16+
import { SlugComponent as SlugComponent_15 } from '@/payload/fields/slug/SlugComponent'
17+
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_16 } from '@payloadcms/richtext-lexical/client'
18+
import { BlocksFeatureClient as BlocksFeatureClient_17 } from '@payloadcms/richtext-lexical/client'
19+
import { default as default_18 } from 'src/payload/components/BeforeDashboard'
20+
import { default as default_19 } from 'src/payload/components/BeforeLogin'
2021

2122
export const importMap = {
22-
"@payloadcms/richtext-lexical/client#RichTextCell": RichTextCell_0,
23-
"@payloadcms/richtext-lexical/client#RichTextField": RichTextField_1,
24-
"@payloadcms/richtext-lexical/generateComponentMap#getGenerateComponentMap": getGenerateComponentMap_2,
25-
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_3,
26-
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_4,
27-
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_5,
28-
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_6,
29-
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_7,
30-
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_8,
31-
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_9,
32-
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_10,
33-
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_11,
34-
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_12,
35-
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_13,
36-
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_14,
37-
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_15,
38-
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_16,
39-
"/payload/components/BeforeDashboard#default": default_17,
40-
"/payload/components/BeforeLogin#default": default_18
23+
'@payloadcms/richtext-lexical/client#RichTextCell': RichTextCell_0,
24+
'@payloadcms/richtext-lexical/client#RichTextField': RichTextField_1,
25+
'@payloadcms/richtext-lexical/generateComponentMap#getGenerateComponentMap':
26+
getGenerateComponentMap_2,
27+
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient': InlineToolbarFeatureClient_3,
28+
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient': FixedToolbarFeatureClient_4,
29+
'@payloadcms/richtext-lexical/client#HeadingFeatureClient': HeadingFeatureClient_5,
30+
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient': UnderlineFeatureClient_6,
31+
'@payloadcms/richtext-lexical/client#BoldFeatureClient': BoldFeatureClient_7,
32+
'@payloadcms/richtext-lexical/client#ItalicFeatureClient': ItalicFeatureClient_8,
33+
'@payloadcms/richtext-lexical/client#LinkFeatureClient': LinkFeatureClient_9,
34+
'@payloadcms/plugin-seo/client#OverviewComponent': OverviewComponent_10,
35+
'@payloadcms/plugin-seo/client#MetaTitleComponent': MetaTitleComponent_11,
36+
'@payloadcms/plugin-seo/client#MetaImageComponent': MetaImageComponent_12,
37+
'@payloadcms/plugin-seo/client#MetaDescriptionComponent': MetaDescriptionComponent_13,
38+
'@payloadcms/plugin-seo/client#PreviewComponent': PreviewComponent_14,
39+
'@/payload/fields/slug/SlugComponent#SlugComponent': SlugComponent_15,
40+
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient': HorizontalRuleFeatureClient_16,
41+
'@payloadcms/richtext-lexical/client#BlocksFeatureClient': BlocksFeatureClient_17,
42+
'/payload/components/BeforeDashboard#default': default_18,
43+
'/payload/components/BeforeLogin#default': default_19,
4144
}

templates/website/src/payload-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export interface Page {
236236
};
237237
publishedAt?: string | null;
238238
slug?: string | null;
239+
slugLock?: boolean | null;
239240
updatedAt: string;
240241
createdAt: string;
241242
_status?: ('draft' | 'published') | null;
@@ -331,6 +332,7 @@ export interface Post {
331332
}[]
332333
| null;
333334
slug?: string | null;
335+
slugLock?: boolean | null;
334336
updatedAt: string;
335337
createdAt: string;
336338
_status?: ('draft' | 'published') | null;

templates/website/src/payload/collections/Pages/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const Pages: CollectionConfig = {
102102
position: 'sidebar',
103103
},
104104
},
105-
slugField(),
105+
...slugField(),
106106
],
107107
hooks: {
108108
afterChange: [revalidatePage],

templates/website/src/payload/collections/Posts/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
1414
import { Banner } from '../../blocks/Banner'
1515
import { Code } from '../../blocks/Code'
1616
import { MediaBlock } from '../../blocks/MediaBlock'
17-
import { slugField } from '../../fields/slug'
1817
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
1918
import { populateAuthors } from './hooks/populateAuthors'
2019
import { revalidatePost } from './hooks/revalidatePost'
@@ -26,6 +25,7 @@ import {
2625
OverviewField,
2726
PreviewField,
2827
} from '@payloadcms/plugin-seo/fields'
28+
import { slugField } from '@/payload/fields/slug'
2929

3030
export const Posts: CollectionConfig = {
3131
slug: 'posts',
@@ -193,7 +193,7 @@ export const Posts: CollectionConfig = {
193193
},
194194
],
195195
},
196-
slugField(),
196+
...slugField(),
197197
],
198198
hooks: {
199199
afterChange: [revalidatePost],

templates/website/src/payload/fields/slug.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use client'
2+
import React, { useCallback, useEffect } from 'react'
3+
4+
import {
5+
useField,
6+
useFieldProps,
7+
Button,
8+
TextInput,
9+
FieldLabel,
10+
useFormFields,
11+
} from '@payloadcms/ui'
12+
13+
import type { TextFieldProps } from 'payload'
14+
15+
import { formatSlug } from './formatSlug'
16+
import './index.scss'
17+
18+
type SlugComponentProps = {
19+
fieldToUse: string
20+
checkboxFieldPath: string
21+
} & TextFieldProps
22+
23+
export const SlugComponent: React.FC<SlugComponentProps> = ({
24+
field,
25+
fieldToUse,
26+
checkboxFieldPath: checkboxFieldPathFromProps,
27+
}) => {
28+
const { label } = field
29+
const { path, readOnly: readOnlyFromProps } = useFieldProps()
30+
31+
const checkboxFieldPath = path.includes('.')
32+
? `${path}.${checkboxFieldPathFromProps}`
33+
: checkboxFieldPathFromProps
34+
35+
const { value, setValue } = useField<string>({ path })
36+
37+
const { value: checkboxValue, setValue: setCheckboxValue } = useField<boolean>({
38+
path: checkboxFieldPath,
39+
})
40+
41+
const fieldToUseValue = useFormFields(([fields, dispatch]) => {
42+
return fields[fieldToUse].value as string
43+
})
44+
45+
useEffect(() => {
46+
if (checkboxValue) setValue(formatSlug(fieldToUseValue))
47+
}, [fieldToUseValue, checkboxValue])
48+
49+
const handleLock = useCallback(
50+
(e) => {
51+
e.preventDefault()
52+
53+
setCheckboxValue(!checkboxValue)
54+
},
55+
[checkboxValue, setCheckboxValue],
56+
)
57+
58+
const readOnly = readOnlyFromProps || checkboxValue
59+
60+
return (
61+
<div className="field-type slug-field-component">
62+
<div className="label-wrapper">
63+
<FieldLabel field={field} htmlFor={`field-${path}`} label={label} />
64+
65+
<Button className="lock-button" buttonStyle="none" onClick={handleLock}>
66+
{checkboxValue ? 'Unlock' : 'Lock'}
67+
</Button>
68+
</div>
69+
70+
<TextInput label={''} value={value} onChange={setValue} path={path} readOnly={readOnly} />
71+
</div>
72+
)
73+
}
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
import type { FieldHook } from 'payload'
22

3-
const format = (val: string): string =>
3+
export const formatSlug = (val: string): string =>
44
val
55
.replace(/ /g, '-')
66
.replace(/[^\w-]+/g, '')
77
.toLowerCase()
88

9-
const formatSlug =
9+
export const formatSlugHook =
1010
(fallback: string): FieldHook =>
1111
({ data, operation, originalDoc, value }) => {
1212
if (typeof value === 'string') {
13-
return format(value)
13+
return formatSlug(value)
1414
}
1515

1616
if (operation === 'create' || !data?.slug) {
1717
const fallbackData = data?.[fallback] || data?.[fallback]
1818

1919
if (fallbackData && typeof fallbackData === 'string') {
20-
return format(fallbackData)
20+
return formatSlug(fallbackData)
2121
}
2222
}
2323

2424
return value
2525
}
26-
27-
export default formatSlug
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.slug-field-component {
2+
.label-wrapper {
3+
display: flex;
4+
justify-content: space-between;
5+
align-items: center;
6+
}
7+
8+
.lock-button {
9+
margin: 0;
10+
padding-bottom: 0.3125rem;
11+
}
12+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { CheckboxField, TextField } from 'payload'
2+
3+
import { formatSlugHook } from './formatSlug'
4+
5+
type Overrides = {
6+
slugOverrides?: Partial<TextField>
7+
checkboxOverrides?: Partial<CheckboxField>
8+
}
9+
10+
type Slug = (fieldToUse?: string, overrides?: Overrides) => [TextField, CheckboxField]
11+
12+
export const slugField: Slug = (fieldToUse = 'title', overrides = {}) => {
13+
const { slugOverrides, checkboxOverrides } = overrides
14+
15+
const checkBoxField: CheckboxField = {
16+
name: 'slugLock',
17+
type: 'checkbox',
18+
defaultValue: true,
19+
admin: {
20+
hidden: true,
21+
position: 'sidebar',
22+
},
23+
...checkboxOverrides,
24+
}
25+
26+
// Expect ts error here because of typescript mismatching Partial<TextField> with TextField
27+
// @ts-expect-error
28+
const slugField: TextField = {
29+
name: 'slug',
30+
type: 'text',
31+
index: true,
32+
label: 'Slug',
33+
...(slugOverrides || {}),
34+
hooks: {
35+
// Kept this in for hook or API based updates
36+
beforeValidate: [formatSlugHook(fieldToUse)],
37+
},
38+
admin: {
39+
position: 'sidebar',
40+
...(slugOverrides?.admin || {}),
41+
components: {
42+
Field: {
43+
path: '@/payload/fields/slug/SlugComponent#SlugComponent',
44+
clientProps: {
45+
fieldToUse,
46+
checkboxFieldPath: checkBoxField.name,
47+
},
48+
},
49+
},
50+
},
51+
}
52+
53+
return [slugField, checkBoxField]
54+
}

templates/website/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
],
3636
"@/*": [
3737
"./src/app/*"
38+
],
39+
"@/payload/*": [
40+
"./src/payload/*"
3841
]
3942
}
4043
},

0 commit comments

Comments
 (0)