diff --git a/apps/cms/config/sync/gutenberg.settings.yml b/apps/cms/config/sync/gutenberg.settings.yml index e58422162..ed9ef9d7d 100644 --- a/apps/cms/config/sync/gutenberg.settings.yml +++ b/apps/cms/config/sync/gutenberg.settings.yml @@ -4,12 +4,12 @@ page_template_lock: all page_allowed_blocks: core/paragraph: core/paragraph core/list: core/list - core/quote: core/quote core/table: core/table core/all: 0 core/image: 0 core/heading: 0 core/gallery: 0 + core/quote: 0 core/audio: 0 core/button: 0 core/buttons: 0 @@ -75,6 +75,7 @@ page_allowed_drupal_blocks: drupalblock/all_core: 0 page_title_block: 0 drupalblock/all_forms: 0 + masquerade: 0 user_login_block: 0 drupalblock/all_lists_views_: 0 'views_block:content_recent-block_1': 0 @@ -91,3 +92,6 @@ page_allowed_drupal_blocks: system_branding_block: 0 node_syndicate_block: 0 drupalblock/all_user: 0 + drupalblock/all_webform: 0 + webform_block: 0 + webform_submission_limit_block: 0 diff --git a/packages/drupal/gutenberg_blocks/css/edit.css b/packages/drupal/gutenberg_blocks/css/edit.css index f4a49c277..1c603451e 100644 --- a/packages/drupal/gutenberg_blocks/css/edit.css +++ b/packages/drupal/gutenberg_blocks/css/edit.css @@ -12,6 +12,11 @@ content: ''; } +.gutenberg__editor blockquote .quote-author, +.gutenberg__editor blockquote .quote-role { + text-align: right; +} + .gutenberg__editor .container-wrapper { display: block; position: relative; diff --git a/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php b/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php new file mode 100644 index 000000000..53df53041 --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/Plugin/Validation/GutenbergValidator/QuoteValidator.php @@ -0,0 +1,40 @@ + [ + 'field_label' => $this->t('Quote text'), + 'rules' => ['required'], + ], + 'author' => [ + 'field_label' => $this->t('Quote author'), + 'rules' => ['required'], + ], + ]; + } + +} diff --git a/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx new file mode 100644 index 000000000..1aea31b6a --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/blocks/quote.tsx @@ -0,0 +1,100 @@ +import { RichText } from 'wordpress__block-editor'; +import { registerBlockType } from 'wordpress__blocks'; +import { compose, withState } from 'wordpress__compose'; +import { dispatch } from 'wordpress__data'; + +import { cleanUpText } from '../utils/clean-up-text'; +import { DrupalMediaEntity } from '../utils/drupal-media'; + +declare const Drupal: { t: (s: string) => string }; + +const { t: __ } = Drupal; + +// @ts-ignore +const { setPlainTextAttribute } = silverbackGutenbergUtils; + +// @ts-ignore +registerBlockType(`custom/quote`, { + title: __('Quote'), + icon: 'format-quote', + category: 'text', + attributes: { + quote: { + type: 'string', + }, + author: { + tpye: 'string', + }, + role: { + type: 'string', + }, + mediaEntityIds: { + type: 'array', + }, + }, + // @ts-ignore + edit: compose(withState())((props) => { + return ( +
+ { + props.setAttributes({ + quote: cleanUpText(quote, ['strong']), + }); + }} + /> + { + setPlainTextAttribute(props, 'author', author); + }} + /> + { + setPlainTextAttribute(props, 'role', role); + }} + /> + { + // @ts-ignore + error = typeof error === 'string' ? error : error[2]; + dispatch('core/notices').createWarningNotice(error); + }} + /> +
+ ); + }), + save() { + return null; + }, +}); diff --git a/packages/drupal/gutenberg_blocks/src/index.ts b/packages/drupal/gutenberg_blocks/src/index.ts index 6eae568bf..2763bdad8 100644 --- a/packages/drupal/gutenberg_blocks/src/index.ts +++ b/packages/drupal/gutenberg_blocks/src/index.ts @@ -8,3 +8,4 @@ import './blocks/image-teasers'; import './blocks/image-with-text'; import './filters/list'; import './blocks/cta'; +import './blocks/quote'; diff --git a/packages/drupal/test_content/content/file/2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml b/packages/drupal/test_content/content/file/2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml new file mode 100644 index 000000000..8947e3bbc --- /dev/null +++ b/packages/drupal/test_content/content/file/2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7.yml @@ -0,0 +1,27 @@ +_meta: + version: '1.0' + entity_type: file + uuid: 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7 + default_langcode: en +default: + uid: + - + target_id: 1 + filename: + - + value: the_silverback.jpeg + uri: + - + value: 'public://2024-04/the_silverback.jpeg' + filemime: + - + value: image/jpeg + filesize: + - + value: 146269 + status: + - + value: true + created: + - + value: 1713785099 diff --git a/packages/drupal/test_content/content/file/the_silverback.jpeg b/packages/drupal/test_content/content/file/the_silverback.jpeg new file mode 100644 index 000000000..25cc96bce Binary files /dev/null and b/packages/drupal/test_content/content/file/the_silverback.jpeg differ diff --git a/packages/drupal/test_content/content/media/5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml b/packages/drupal/test_content/content/media/5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml new file mode 100644 index 000000000..5dbede2d2 --- /dev/null +++ b/packages/drupal/test_content/content/media/5dfc1856-e9e4-4f02-9cd6-9d888870ce1a.yml @@ -0,0 +1,74 @@ +_meta: + version: '1.0' + entity_type: media + uuid: 5dfc1856-e9e4-4f02-9cd6-9d888870ce1a + bundle: image + default_langcode: en + depends: + 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7: file +default: + revision_user: + - + target_id: 1 + status: + - + value: true + uid: + - + target_id: 1 + name: + - + value: the_silverback.jpeg + created: + - + value: 1713785099 + path: + - + alias: '' + langcode: en + pathauto: 0 + content_translation_source: + - + value: und + content_translation_outdated: + - + value: false + field_media_image: + - + entity: 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7 + alt: 'The silverback' + title: '' + width: 1280 + height: 720 +translations: + de: + status: + - + value: true + uid: + - + target_id: 1 + name: + - + value: the_silverback.jpeg + created: + - + value: 1713785205 + path: + - + alias: '' + langcode: de + pathauto: 0 + content_translation_source: + - + value: en + content_translation_outdated: + - + value: false + field_media_image: + - + entity: 2f1be18f-dc18-4b56-8ff0-ce24d8ff7df7 + alt: 'The silverback DE' + title: '' + width: 1280 + height: 720 diff --git a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml index 43e5fe62d..eb002424c 100644 --- a/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml +++ b/packages/drupal/test_content/content/node/a397ca48-8fad-411e-8901-0eba2feb989c.yml @@ -8,6 +8,7 @@ _meta: 3a0fe860-a6d6-428a-9474-365bd57509aa: media 478c4289-961d-4ce8-85d6-578ae05f3019: media 72187a1f-3e48-4b45-a9b7-189c6fd7ee26: media + 5dfc1856-e9e4-4f02-9cd6-9d888870ce1a: media default: revision_uid: - @@ -102,10 +103,6 @@ default:

Heading 3

- -

Quote

Citation
- - @@ -117,6 +114,9 @@ default: + + +

@@ -189,15 +189,13 @@ translations:

Heading 3 DE

- -

Quote DE

Citation DE
- - + + format: gutenberg summary: '' diff --git a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml index 53ade0b64..a367f61b2 100644 --- a/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml +++ b/packages/drupal/test_content/content/node/ceb9b2a7-4c4c-4084-ada9-d5f6505d466b.yml @@ -58,10 +58,6 @@ default:
- -

- -

@@ -73,6 +69,7 @@ default:

+ format: gutenberg summary: '' diff --git a/packages/schema/src/fragments/Page.gql b/packages/schema/src/fragments/Page.gql index 0a78d9dea..cbf2227eb 100644 --- a/packages/schema/src/fragments/Page.gql +++ b/packages/schema/src/fragments/Page.gql @@ -34,6 +34,7 @@ fragment Page on Page { ...BlockImageTeasers ...BlockCta ...BlockImageWithText + ...BlockQuote } metaTags { tag diff --git a/packages/schema/src/fragments/PageContent/BlockQuote.gql b/packages/schema/src/fragments/PageContent/BlockQuote.gql new file mode 100644 index 000000000..785efb620 --- /dev/null +++ b/packages/schema/src/fragments/PageContent/BlockQuote.gql @@ -0,0 +1,9 @@ +fragment BlockQuote on BlockQuote { + quote + author + role + image { + source(width: 1536, sizes: [[768, 768], [1536, 1536]]) + alt + } +} diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 3d86b6850..1b2d3fb19 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -200,6 +200,7 @@ union PageContent @resolveEditorBlockType = | BlockImageTeasers | BlockCta | BlockImageWithText + | BlockQuote type BlockForm @type(id: "custom/form") { url: Url @resolveEditorBlockAttribute(key: "formId") @webformIdToUrl(id: "$") @@ -250,6 +251,13 @@ type BlockImageWithText @type(id: "custom/image-with-text") { textContent: BlockMarkup @resolveEditorBlockChildren @seek(pos: 0) } +type BlockQuote @type(id: "custom/quote") { + quote: Markup @resolveEditorBlockAttribute(key: "quote") + author: String @resolveEditorBlockAttribute(key: "author") + role: String @resolveEditorBlockAttribute(key: "role") + image: MediaImage @resolveEditorBlockMedia +} + input PaginationInput { limit: Int! offset: Int! diff --git a/packages/ui/src/components/Atoms/Container.css b/packages/ui/src/components/Atoms/Container.css new file mode 100644 index 000000000..8985bc6dc --- /dev/null +++ b/packages/ui/src/components/Atoms/Container.css @@ -0,0 +1,40 @@ +.container-page { + @apply max-w-full px-[1.25rem] md:px-[3.75rem]; +} + +.container-content { + @apply mx-auto max-w-7xl; +} + +.container-text { + @apply max-w-[40.75rem] xl:ml-[11rem] lg:ml-[7rem]; +} + +.nested-container .container-page { + @apply px-0 max-w-none; +} + +.nested-container .container-content { + @apply max-w-none; +} + +.nested-container .container-text { + @apply max-w-none ml-0 my-2.5 md:my-5; +} + +.container-content-wrapper { + @apply mx-auto max-w-[62rem]; +} + +.nested-container .container-page:first-child .container-text, +.nested-container .container-page:first-child .container-text *:first-child{ + @apply mt-0; +} +.nested-container .container-page:last-child .container-text, +.nested-container .container-page:last-child .container-text *:last-child { + @apply mb-0; +} + +.container-nested .prose ul:not(ul ul), .container-nested .prose ol:not(ol ol) { + @apply mt-0; +} diff --git a/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx b/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx index 54f29a9fb..a83f81b79 100644 --- a/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx +++ b/packages/ui/src/components/Organisms/PageContent/BlockMarkup.tsx @@ -18,7 +18,6 @@ export function BlockMarkup(props: BlockMarkupFragment) { className={clsx([ 'mx-auto max-w-3xl prose lg:prose-xl mt-10', 'prose-a:text-indigo-600', - 'prose-blockquote:border-indigo-200', 'prose-em:text-indigo-600', 'prose-strong:text-indigo-600', 'marker:text-indigo-600 marker:font-bold', @@ -49,6 +48,25 @@ export function BlockMarkup(props: BlockMarkupFragment) { ); }, + blockquote: ({ children }: PropsWithChildren<{}>) => { + return ( +
+ + + + {children} +
+ ); + }, }} markup={props.markup} /> diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts new file mode 100644 index 000000000..ffe6090f1 --- /dev/null +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.stories.ts @@ -0,0 +1,23 @@ +import { Markup } from '@custom/schema'; +import Avatar from '@stories/avatar.jpg?as=metadata'; +import { Meta, StoryObj } from '@storybook/react'; + +import { image } from '../../../helpers/image'; +import { BlockQuote } from './BlockQuote'; + +export default { + component: BlockQuote, +} satisfies Meta; + +export const Quote = { + args: { + role: 'test role', + author: 'Author name', + image: { + source: image(Avatar), + alt: 'Portrait', + }, + quote: + '

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

' as Markup, + }, +} satisfies StoryObj; diff --git a/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx new file mode 100644 index 000000000..6c1abaad9 --- /dev/null +++ b/packages/ui/src/components/Organisms/PageContent/BlockQuote.tsx @@ -0,0 +1,47 @@ +import { BlockQuoteFragment, Html, Image } from '@custom/schema'; +import React from 'react'; + +export function BlockQuote(props: BlockQuoteFragment) { + return ( +
+
+ + Quote Symbol + + + {props.quote && } +
+ {props.image && ( + {props.image.alt + )} +
+ {props.author &&

{props.author}

} +
+ {props.role && ( +

+ / + + {props.role} + +

+ )} +
+
+
+ ); +} diff --git a/packages/ui/src/components/Organisms/PageDisplay.tsx b/packages/ui/src/components/Organisms/PageDisplay.tsx index 975d447c0..a39c57ec4 100644 --- a/packages/ui/src/components/Organisms/PageDisplay.tsx +++ b/packages/ui/src/components/Organisms/PageDisplay.tsx @@ -8,6 +8,7 @@ import { BlockCta } from './PageContent/BlockCta'; import { BlockForm } from './PageContent/BlockForm'; import { BlockMarkup } from './PageContent/BlockMarkup'; import { BlockMedia } from './PageContent/BlockMedia'; +import { BlockQuote } from './PageContent/BlockQuote'; import { PageHero } from './PageHero'; export function PageDisplay(page: PageFragment) { @@ -58,6 +59,8 @@ export function PageDisplay(page: PageFragment) { BlockImageWithText goes here ); + case 'BlockQuote': + return
; default: throw new UnreachableCaseError(block); } diff --git a/packages/ui/src/components/Organisms/PageHero.tsx b/packages/ui/src/components/Organisms/PageHero.tsx index c043a10c0..7eea65402 100644 --- a/packages/ui/src/components/Organisms/PageHero.tsx +++ b/packages/ui/src/components/Organisms/PageHero.tsx @@ -15,7 +15,7 @@ export function PageHero(props: NonNullable) { function DefaultHero(props: NonNullable) { return ( -
+
{props.image ? ( {props.image.alt}) { ) : null}
-

+

{props.headline}

{props.lead ? ( @@ -74,7 +74,7 @@ function FormHero(props: NonNullable) { ) : null}
-

+

{props.headline}

{props.lead ? ( diff --git a/packages/ui/src/tailwind.css b/packages/ui/src/tailwind.css index bd6213e1d..2d464b850 100644 --- a/packages/ui/src/tailwind.css +++ b/packages/ui/src/tailwind.css @@ -1,3 +1,31 @@ +/* Import all atom stylesheets. */ +@import './components/Atoms/Container.css'; + @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +/* Prose overrides */ +.lg\:prose-xl + :where(blockquote):not(:where([class~='not-prose'], [class~='not-prose'] *)) { + padding-left: 0 !important; +} + +.prose :where(blockquote p):not( :where([class~='not-prose']) ) { + margin-top: 12px !important; + margin-bottom: 12px !important; +} + +.prose + :where(blockquote p:first-of-type):not( + :where([class~='not-prose'], [class~='not-prose'] *) + )::before { + content: '' !important; +} + +.prose + :where(blockquote p:first-of-type):not( + :where([class~='not-prose'], [class~='not-prose'] *) + )::after { + content: '' !important; +} diff --git a/packages/ui/static/stories/avatar.jpg b/packages/ui/static/stories/avatar.jpg new file mode 100644 index 000000000..e796b545e Binary files /dev/null and b/packages/ui/static/stories/avatar.jpg differ diff --git a/tests/e2e/specs/drupal/blocks.spec.ts b/tests/e2e/specs/drupal/blocks.spec.ts index ec7dc6a1b..1e9e881dc 100644 --- a/tests/e2e/specs/drupal/blocks.spec.ts +++ b/tests/e2e/specs/drupal/blocks.spec.ts @@ -50,10 +50,20 @@ test('All blocks are rendered', async ({ page }) => { await expect(page.locator('h3:text("Heading 3")')).toHaveCount(1); // Quote - await expect(page.locator('blockquote > p:text("Quote")')).toHaveCount(1); - await expect(page.locator('blockquote > cite:text("Citation")')).toHaveCount( - 1, - ); + await expect( + page.locator( + 'blockquote:text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.")', + ), + ).toHaveCount(1); + await expect( + page.locator('blockquote p.not-prose:text("John Doe")'), + ).toHaveCount(1); + await expect( + page.locator('blockquote p.not-prose span:text("Project manager")'), + ).toHaveCount(1); + await expect( + page.locator('blockquote img[alt="The silverback"]'), + ).toHaveCount(1); // Form await expect( diff --git a/tests/schema/specs/blocks.spec.ts b/tests/schema/specs/blocks.spec.ts index 4b86529d4..22d2ef1f9 100644 --- a/tests/schema/specs/blocks.spec.ts +++ b/tests/schema/specs/blocks.spec.ts @@ -62,6 +62,14 @@ test('Blocks', async () => { markup } } + ... on BlockQuote { + quote + author + role + image { + __typename + } + } } } { @@ -147,8 +155,6 @@ test('Blocks', async () => {
  • list 1
  • list 2
    1. list 2.2

Heading 3

- -

Quote

Citation
", }, { @@ -192,6 +198,15 @@ test('Blocks', async () => { "text": "CTA with link to media", "url": "/media/[numeric]", }, + { + "__typename": "BlockQuote", + "author": "John Doe", + "image": { + "__typename": "MediaImage", + }, + "quote": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sagittis nisi nec neque porta, a ornare ligula efficitur.", + "role": "Project manager", + }, { "__typename": "BlockMarkup", "markup": " @@ -229,8 +244,6 @@ test('Blocks', async () => {
-

-

", }, @@ -248,7 +261,13 @@ test('Blocks', async () => { "markup": "

", - }, + }, + { + "__typename": "BlockQuote", + "author": "Jane Doe", + "image": null, + "quote": "In vitae diam quis odio tincidunt faucibus eget ut libero", + "role": null, }, ], "hero": {