diff --git a/docs/fields/rich-text.mdx b/docs/fields/rich-text.mdx index 338981a525..6d3f210707 100644 --- a/docs/fields/rich-text.mdx +++ b/docs/fields/rich-text.mdx @@ -55,6 +55,7 @@ The default `elements` available in Payload are: - `h4` - `h5` - `h6` +- `blockquote` - `link` - `ol` - `ul` @@ -154,6 +155,7 @@ const ExampleCollection: CollectionConfig = { 'h3', 'h4', 'link', + 'blockquote', { name: 'cta', Button: CustomCallToActionButton, @@ -271,7 +273,7 @@ const serialize = (children) => children.map((node, i) => { {serialize(node.children)} ); - case 'quote': + case 'blockquote': return (
{serialize(node.children)} diff --git a/examples/auth/nextjs/components/RichText/serialize.tsx b/examples/auth/nextjs/components/RichText/serialize.tsx index e134b11f46..3b14fc9f19 100644 --- a/examples/auth/nextjs/components/RichText/serialize.tsx +++ b/examples/auth/nextjs/components/RichText/serialize.tsx @@ -69,7 +69,7 @@ const serialize = (children: Children): React.ReactElement[] => return{serialize(node.children)}
case 'h6': return{serialize(node.children)}
- case 'quote': + case 'blockquote': return{serialize(node.children)}case 'ul': return{serialize(node.children)}
diff --git a/examples/form-builder/nextjs/components/RichText/serialize.tsx b/examples/form-builder/nextjs/components/RichText/serialize.tsx index 045dde6e45..cfb4dc443c 100644 --- a/examples/form-builder/nextjs/components/RichText/serialize.tsx +++ b/examples/form-builder/nextjs/components/RichText/serialize.tsx @@ -114,7 +114,7 @@ const serialize = (children: Children): React.ReactElement[] => children.map((no {serialize(node.children)} ); - case 'quote': + case 'blockquote': return ({serialize(node.children)} diff --git a/examples/preview/nextjs/components/RichText/serialize.tsx b/examples/preview/nextjs/components/RichText/serialize.tsx index e134b11f46..3b14fc9f19 100644 --- a/examples/preview/nextjs/components/RichText/serialize.tsx +++ b/examples/preview/nextjs/components/RichText/serialize.tsx @@ -69,7 +69,7 @@ const serialize = (children: Children): React.ReactElement[] => return{serialize(node.children)}
case 'h6': return{serialize(node.children)}
- case 'quote': + case 'blockquote': return{serialize(node.children)}case 'ul': return{serialize(node.children)}
diff --git a/examples/redirects/nextjs/components/RichText/serialize.tsx b/examples/redirects/nextjs/components/RichText/serialize.tsx index fdf7efeeac..9e4febd7bb 100644 --- a/examples/redirects/nextjs/components/RichText/serialize.tsx +++ b/examples/redirects/nextjs/components/RichText/serialize.tsx @@ -67,7 +67,7 @@ const serialize = (children: Children): React.ReactElement[] => return{serialize(node.children)}
case 'h6': return{serialize(node.children)}
- case 'quote': + case 'blockquote': return{serialize(node.children)}case 'ul': return{serialize(node.children)}
diff --git a/src/admin/components/forms/field-types/RichText/elements/blockquote/index.scss b/src/admin/components/forms/field-types/RichText/elements/blockquote/index.scss new file mode 100644 index 0000000000..b0cc5424e2 --- /dev/null +++ b/src/admin/components/forms/field-types/RichText/elements/blockquote/index.scss @@ -0,0 +1,9 @@ +@import '../../../../../../scss/styles.scss'; + +.rich-text-blockquote { + &[data-slate-node=element] { + margin: base(.625) 0; + padding-left: base(0.625); + border-left: 1px solid var(--theme-elevation-200); + } +} diff --git a/src/admin/components/forms/field-types/RichText/elements/blockquote/index.tsx b/src/admin/components/forms/field-types/RichText/elements/blockquote/index.tsx new file mode 100644 index 0000000000..58c0ac68c9 --- /dev/null +++ b/src/admin/components/forms/field-types/RichText/elements/blockquote/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import ElementButton from '../Button'; +import BlockquoteIcon from '../../../../../icons/Blockquote'; + +import './index.scss'; + +const Blockquote = ({ attributes, children }) => ( ++ {children} ++); + +const blockquote = { + Button: () => ( ++ + ), + Element: Blockquote, +}; + +export default blockquote; diff --git a/src/admin/components/forms/field-types/RichText/elements/index.tsx b/src/admin/components/forms/field-types/RichText/elements/index.tsx index dcb884ec1e..72eed50f60 100644 --- a/src/admin/components/forms/field-types/RichText/elements/index.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/index.tsx @@ -5,6 +5,7 @@ import h4 from './h4'; import h5 from './h5'; import h6 from './h6'; import link from './link'; +import blockquote from './blockquote'; import ol from './ol'; import ul from './ul'; import li from './li'; @@ -20,6 +21,7 @@ const elements = { h5, h6, link, + blockquote, ol, ul, li, diff --git a/src/admin/components/forms/field-types/RichText/index.scss b/src/admin/components/forms/field-types/RichText/index.scss index 27ff876bab..bbf25b4906 100644 --- a/src/admin/components/forms/field-types/RichText/index.scss +++ b/src/admin/components/forms/field-types/RichText/index.scss @@ -102,7 +102,8 @@ &--read-only { .rich-text__editor { - background-color: var(--theme-elevation-150); + background: var(--theme-elevation-200); + color: var(--theme-elevation-450); padding: base(.5); .popup button { @@ -122,7 +123,7 @@ left: 0; right: 0; bottom: 0; - background-color: var(--theme-elevation-150); + background-color: var(--theme-elevation-200); opacity: .85; z-index: 2; backdrop-filter: unset; diff --git a/src/admin/components/forms/field-types/Textarea/index.scss b/src/admin/components/forms/field-types/Textarea/index.scss index ee065331f1..5d976ea7c7 100644 --- a/src/admin/components/forms/field-types/Textarea/index.scss +++ b/src/admin/components/forms/field-types/Textarea/index.scss @@ -28,6 +28,17 @@ } } + &.read-only { + .textarea-outer { + background: var(--theme-elevation-200); + color: var(--theme-elevation-450); + &:hover { + border-color: var(--theme-elevation-150); + @include shadow-sm; + } + } + } + // This element takes exactly the same dimensions as the clone .textarea-inner { display: block; diff --git a/src/admin/components/forms/field-types/Upload/Input.tsx b/src/admin/components/forms/field-types/Upload/Input.tsx index c353051bca..cf3627d189 100644 --- a/src/admin/components/forms/field-types/Upload/Input.tsx +++ b/src/admin/components/forms/field-types/Upload/Input.tsx @@ -174,7 +174,7 @@ const UploadInput: React.FC+ = (props) => { { + handleRemove={readOnly ? undefined : () => { onChange(null); }} /> @@ -184,20 +184,24 @@ const UploadInput: React.FC = (props) => { ); }; diff --git a/src/admin/components/views/Version/RenderFieldsToDiff/fields/Text/richTextToHTML.ts b/src/admin/components/views/Version/RenderFieldsToDiff/fields/Text/richTextToHTML.ts index 869b583014..e913ab0d2e 100644 --- a/src/admin/components/views/Version/RenderFieldsToDiff/fields/Text/richTextToHTML.ts +++ b/src/admin/components/views/Version/RenderFieldsToDiff/fields/Text/richTextToHTML.ts @@ -70,6 +70,10 @@ export const richTextToHTML = (content: unknown): string => { nodeHTML = `@@ -211,8 +215,8 @@ const UploadInput: React.FC = (props) => { /> )} - - + {!readOnly && } + {!readOnly && } ${richTextToHTML(node.children)}
`; break; + case 'blockquote': + nodeHTML = `${richTextToHTML(node.children)}`; + break; + case 'ul': nodeHTML = `${richTextToHTML(node.children)}
`; break; diff --git a/src/collections/config/schema.ts b/src/collections/config/schema.ts index d72e140b83..de03bba879 100644 --- a/src/collections/config/schema.ts +++ b/src/collections/config/schema.ts @@ -184,6 +184,14 @@ const collectionSchema = joi.object().keys({ format: joi.string(), options: joi.object(), }), + trimOptions: joi.alternatives().try( + joi.object().keys({ + format: joi.string(), + options: joi.object(), + }), + joi.string(), + joi.number(), + ), }), joi.boolean(), ), diff --git a/src/uploads/generateFileData.ts b/src/uploads/generateFileData.ts index 70376dd4bf..5a7a2eaf9b 100644 --- a/src/uploads/generateFileData.ts +++ b/src/uploads/generateFileData.ts @@ -56,7 +56,7 @@ export const generateFileData = async({ }; } - const { staticDir, imageSizes, disableLocalStorage, resizeOptions, formatOptions } = collectionConfig.upload; + const { staticDir, imageSizes, disableLocalStorage, resizeOptions, formatOptions, trimOptions } = collectionConfig.upload; let staticPath = staticDir; if (staticDir.indexOf('/') !== 0) { @@ -85,7 +85,7 @@ export const generateFileData = async ({ if (fileIsAnimated) sharpOptions.animated = true; - if (fileSupportsResize && (resizeOptions || formatOptions)) { + if (fileSupportsResize && (resizeOptions || formatOptions || trimOptions)) { if (file.tempFilePath) { sharpFile = sharp(file.tempFilePath, sharpOptions); } else { @@ -99,6 +99,9 @@ export const generateFileData = async ({ if (formatOptions) { sharpFile = sharpFile.toFormat(formatOptions.format, formatOptions.options); } + if (trimOptions) { + sharpFile = sharpFile.trim(trimOptions); + } } if (isImage(file.mimetype)) { diff --git a/src/uploads/imageResizer.ts b/src/uploads/imageResizer.ts index d7701bb1b6..4e9b667faf 100644 --- a/src/uploads/imageResizer.ts +++ b/src/uploads/imageResizer.ts @@ -77,6 +77,10 @@ export default async function resizeAndSave({ resized = resized.toFormat(desiredSize.formatOptions.format, desiredSize.formatOptions.options); } + if (desiredSize.trimOptions) { + resized = resized.trim(desiredSize.trimOptions); + } + const bufferObject = await resized.toBuffer({ resolveWithObject: true, }); diff --git a/src/uploads/types.ts b/src/uploads/types.ts index c5f1480b85..a118ddfb6c 100644 --- a/src/uploads/types.ts +++ b/src/uploads/types.ts @@ -41,9 +41,16 @@ export type ImageUploadFormatOptions = { options?: Parameters [1] } +/** + * Params sent to the sharp trim() function + * @link https://sharp.pixelplumbing.com/api-resize#trim + */ +export type ImageUploadTrimOptions = Parameters [0] + export type ImageSize = ResizeOptions & { name: string formatOptions?: ImageUploadFormatOptions + trimOptions?: ImageUploadTrimOptions /** * @deprecated prefer position */ @@ -64,6 +71,7 @@ export type IncomingUploadType = { resizeOptions?: ResizeOptions /** Options for original upload file only. For sizes, set each formatOptions individually. */ formatOptions?: ImageUploadFormatOptions + trimOptions?: ImageUploadTrimOptions } export type Upload = { @@ -77,6 +85,7 @@ export type Upload = { handlers?: any[] resizeOptions?: ResizeOptions; formatOptions?: ImageUploadFormatOptions + trimOptions?: ImageUploadTrimOptions } export type File = { diff --git a/test/_community/collections/Media/index.ts b/test/_community/collections/Media/index.ts new file mode 100644 index 0000000000..7f5d8d0781 --- /dev/null +++ b/test/_community/collections/Media/index.ts @@ -0,0 +1,13 @@ +import type { CollectionConfig } from '../../../../src/collections/config/types'; + +export const mediaSlug = 'media'; + +export const MediaCollection: CollectionConfig = { + slug: mediaSlug, + upload: true, + access: { + read: () => true, + create: () => true, + }, + fields: [], +}; diff --git a/test/_community/collections/Posts/index.ts b/test/_community/collections/Posts/index.ts index 3432846bb3..61487d8ff3 100644 --- a/test/_community/collections/Posts/index.ts +++ b/test/_community/collections/Posts/index.ts @@ -1,4 +1,5 @@ import type { CollectionConfig } from '../../../../src/collections/config/types'; +import { mediaSlug } from '../Media'; export const postsSlug = 'posts'; @@ -9,5 +10,14 @@ export const PostsCollection: CollectionConfig = { name: 'text', type: 'text', }, + { + name: 'associatedMedia', + type: 'upload', + relationTo: mediaSlug, + access: { + create: () => true, + update: () => false, + }, + }, ], }; diff --git a/test/_community/config.ts b/test/_community/config.ts index 1fe86e512c..f6f28dc3d1 100644 --- a/test/_community/config.ts +++ b/test/_community/config.ts @@ -2,11 +2,13 @@ import { buildConfig } from '../buildConfig'; import { PostsCollection, postsSlug } from './collections/Posts'; import { MenuGlobal } from './globals/Menu'; import { devUser } from '../credentials'; +import { MediaCollection } from './collections/Media'; export default buildConfig({ // ...extend config here collections: [ PostsCollection, + MediaCollection, // ...add more collections here ], globals: [ diff --git a/test/uploads/config.ts b/test/uploads/config.ts index b5867c0203..c9c1ce4277 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -135,6 +135,39 @@ export default buildConfig({ }, fields: [], }, + { + slug: 'media-trim', + upload: { + staticURL: '/media-trim', + staticDir: './media-trim', + mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'], + trimOptions: 0, + imageSizes: [ + { + name: 'trimNumber', + width: 1024, + height: undefined, + trimOptions: 0, + }, + { + name: 'trimString', + width: 1024, + height: undefined, + trimOptions: 0, + }, + { + name: 'trimOptions', + width: 1024, + height: undefined, + trimOptions: { + background: '#000000', + threshold: 50, + }, + }, + ], + }, + fields: [], + }, { slug: 'unstored-media', upload: {