diff --git a/templates/website/src/app/(payload)/admin/importMap.js b/templates/website/src/app/(payload)/admin/importMap.js index 19c7e2ce2..47d1b140f 100644 --- a/templates/website/src/app/(payload)/admin/importMap.js +++ b/templates/website/src/app/(payload)/admin/importMap.js @@ -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, } diff --git a/templates/website/src/payload-types.ts b/templates/website/src/payload-types.ts index a5e004195..47adc1fdb 100644 --- a/templates/website/src/payload-types.ts +++ b/templates/website/src/payload-types.ts @@ -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; diff --git a/templates/website/src/payload/collections/Pages/index.ts b/templates/website/src/payload/collections/Pages/index.ts index df201877a..3576566be 100644 --- a/templates/website/src/payload/collections/Pages/index.ts +++ b/templates/website/src/payload/collections/Pages/index.ts @@ -102,7 +102,7 @@ export const Pages: CollectionConfig = { position: 'sidebar', }, }, - slugField(), + ...slugField(), ], hooks: { afterChange: [revalidatePage], diff --git a/templates/website/src/payload/collections/Posts/index.ts b/templates/website/src/payload/collections/Posts/index.ts index 80927033f..c3f6b02ab 100644 --- a/templates/website/src/payload/collections/Posts/index.ts +++ b/templates/website/src/payload/collections/Posts/index.ts @@ -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], diff --git a/templates/website/src/payload/fields/slug.ts b/templates/website/src/payload/fields/slug.ts deleted file mode 100644 index b73ecba4f..000000000 --- a/templates/website/src/payload/fields/slug.ts +++ /dev/null @@ -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 - -export const slugField: Slug = (fieldToUse = 'title', overrides = {}) => - deepMerge>( - { - name: 'slug', - type: 'text', - admin: { - position: 'sidebar', - }, - hooks: { - beforeValidate: [formatSlug(fieldToUse)], - }, - index: true, - label: 'Slug', - }, - overrides, - ) diff --git a/templates/website/src/payload/fields/slug/SlugComponent.tsx b/templates/website/src/payload/fields/slug/SlugComponent.tsx new file mode 100644 index 000000000..3567790df --- /dev/null +++ b/templates/website/src/payload/fields/slug/SlugComponent.tsx @@ -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 = ({ + field, + fieldToUse, + checkboxFieldPath: checkboxFieldPathFromProps, +}) => { + const { label } = field + const { path, readOnly: readOnlyFromProps } = useFieldProps() + + const checkboxFieldPath = path.includes('.') + ? `${path}.${checkboxFieldPathFromProps}` + : checkboxFieldPathFromProps + + const { value, setValue } = useField({ path }) + + const { value: checkboxValue, setValue: setCheckboxValue } = useField({ + 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 ( +
+
+ + + +
+ + +
+ ) +} diff --git a/templates/website/src/payload/utilities/formatSlug.ts b/templates/website/src/payload/fields/slug/formatSlug.ts similarity index 75% rename from templates/website/src/payload/utilities/formatSlug.ts rename to templates/website/src/payload/fields/slug/formatSlug.ts index dea02cb24..004e9f2ed 100644 --- a/templates/website/src/payload/utilities/formatSlug.ts +++ b/templates/website/src/payload/fields/slug/formatSlug.ts @@ -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 diff --git a/templates/website/src/payload/fields/slug/index.scss b/templates/website/src/payload/fields/slug/index.scss new file mode 100644 index 000000000..e3dd2d836 --- /dev/null +++ b/templates/website/src/payload/fields/slug/index.scss @@ -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; + } +} diff --git a/templates/website/src/payload/fields/slug/index.ts b/templates/website/src/payload/fields/slug/index.ts new file mode 100644 index 000000000..32cd344f1 --- /dev/null +++ b/templates/website/src/payload/fields/slug/index.ts @@ -0,0 +1,54 @@ +import type { CheckboxField, TextField } from 'payload' + +import { formatSlugHook } from './formatSlug' + +type Overrides = { + slugOverrides?: Partial + checkboxOverrides?: Partial +} + +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 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] +} diff --git a/templates/website/tsconfig.json b/templates/website/tsconfig.json index d95492814..974f89b94 100644 --- a/templates/website/tsconfig.json +++ b/templates/website/tsconfig.json @@ -35,6 +35,9 @@ ], "@/*": [ "./src/app/*" + ], + "@/payload/*": [ + "./src/payload/*" ] } },