diff --git a/packages/drupal/gutenberg_blocks/src/blocks/conditional.tsx b/packages/drupal/gutenberg_blocks/src/blocks/conditional.tsx new file mode 100644 index 000000000..131c59b2f --- /dev/null +++ b/packages/drupal/gutenberg_blocks/src/blocks/conditional.tsx @@ -0,0 +1,171 @@ +import clsx from 'clsx'; +import { InnerBlocks, InspectorControls } from 'wordpress__block-editor'; +import { registerBlockType } from 'wordpress__blocks'; +import { BaseControl, PanelBody } from 'wordpress__components'; + +// @ts-ignore +const { t: __ } = Drupal; + +registerBlockType(`custom/conditional`, { + title: __('Conditional content'), + category: 'layout', + icon: 'category', + // Allow the block only at the root level to avoid GraphQL fragment recursion. + parent: ['custom/content'], + attributes: { + displayFrom: { + type: 'string', + default: '', + }, + displayTo: { + type: 'string', + default: '', + }, + purpose: { + type: 'string', + default: '', + }, + }, + edit(props) { + const { attributes, setAttributes } = props; + + const displayFrom = attributes.displayFrom as string | undefined; + const displayTo = attributes.displayTo as string | undefined; + const purpose = ((attributes.purpose as string) || '').trim(); + + // Same logic as in BlockConditional.tsx + const active = { + scheduledDisplay: [ + displayFrom + ? new Date(displayFrom).getTime() <= new Date().getTime() + : true, + displayTo ? new Date(displayTo).getTime() > new Date().getTime() : true, + ].every(Boolean), + }; + const isActive = Object.values(active).every(Boolean); + + const conditions = { + scheduledDisplay: + displayFrom || displayTo + ? '🕒 ' + + __('Scheduled display') + + ': ' + + [ + displayFrom + ? __('From') + ' ' + new Date(displayFrom).toLocaleString() + : '', + displayTo + ? __('To') + ' ' + new Date(displayTo).toLocaleString() + : '', + ] + .filter(Boolean) + .join(' ') + : '', + }; + const hasConditions = Object.values(conditions).some(Boolean); + const summary = hasConditions ? ( + Object.entries(conditions) + .filter(([, value]) => !!value) + .map(([key, value]) =>
{value}
) + ) : ( +
{'ℹī¸ ' + __('No conditions set')}
+ ); + + return ( +
+
{__('Conditional content')}
+
{summary}
+
+ + {purpose || __('Content')} + + +
+ + + + { + setAttributes({ purpose: event.target.value }); + }} + /> + + +
+ + + + +
+ { + setAttributes({ + displayFrom: event.target.value + ? localToIsoTime(event.target.value) + : '', + }); + }} + /> +
+ +
+ { + setAttributes({ + displayTo: event.target.value + ? localToIsoTime(event.target.value) + : '', + }); + }} + /> +
+ +
+ + + +
+ ); + }, + + save() { + return ; + }, +}); + +function localToIsoTime(localTime: string) { + return new Date(localTime).toISOString(); +} + +function isoToLocalTime(isoTime: string) { + const date = new Date(isoTime); + date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); + return date.toISOString().slice(0, 16); +} diff --git a/packages/drupal/gutenberg_blocks/src/index.ts b/packages/drupal/gutenberg_blocks/src/index.ts index 9a846e2b3..256fb2d0c 100644 --- a/packages/drupal/gutenberg_blocks/src/index.ts +++ b/packages/drupal/gutenberg_blocks/src/index.ts @@ -10,6 +10,7 @@ import './blocks/image-with-text'; import './filters/list'; import './blocks/cta'; import './blocks/quote'; +import './blocks/conditional'; import './blocks/horizontal-separator'; import './blocks/accordion'; import './blocks/accordion-item-text'; diff --git a/packages/drupal/test_content/content/node/52ee5cc7-0ac5-49b5-8550-ce59476bd4ac.yml b/packages/drupal/test_content/content/node/52ee5cc7-0ac5-49b5-8550-ce59476bd4ac.yml new file mode 100644 index 000000000..d59c3dc67 --- /dev/null +++ b/packages/drupal/test_content/content/node/52ee5cc7-0ac5-49b5-8550-ce59476bd4ac.yml @@ -0,0 +1,62 @@ +_meta: + version: '1.0' + entity_type: node + uuid: 52ee5cc7-0ac5-49b5-8550-ce59476bd4ac + bundle: page + default_langcode: en +default: + revision_uid: + - + target_id: 1 + status: + - + value: true + uid: + - + target_id: 1 + title: + - + value: 'Conditional blocks' + created: + - + value: 1715684657 + promote: + - + value: false + sticky: + - + value: false + moderation_state: + - + value: published + path: + - + alias: /conditional-blocks + langcode: en + pathauto: 0 + content_translation_source: + - + value: und + content_translation_outdated: + - + value: false + body: + - + value: |- + + + + + +

Complete

+ + + + + +

No conditions

+ + + + format: gutenberg + summary: '' diff --git a/packages/schema/src/fragments/Page.gql b/packages/schema/src/fragments/Page.gql index 28f7127f2..33a67a4d5 100644 --- a/packages/schema/src/fragments/Page.gql +++ b/packages/schema/src/fragments/Page.gql @@ -38,6 +38,7 @@ fragment Page on Page { ...BlockHorizontalSeparator ...BlockAccordion ...BlockInfoGrid + ...BlockConditional } metaTags { tag diff --git a/packages/schema/src/fragments/PageContent/BlockConditional.gql b/packages/schema/src/fragments/PageContent/BlockConditional.gql new file mode 100644 index 000000000..5016b930f --- /dev/null +++ b/packages/schema/src/fragments/PageContent/BlockConditional.gql @@ -0,0 +1,17 @@ +fragment BlockConditional on BlockConditional { + displayFrom + displayTo + content { + __typename + ...BlockMarkup + ...BlockMedia + ...BlockForm + ...BlockImageTeasers + ...BlockCta + ...BlockImageWithText + ...BlockQuote + ...BlockHorizontalSeparator + ...BlockAccordion + ...BlockInfoGrid + } +} diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index 0ee5dc8b1..6f758c9c2 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -203,6 +203,18 @@ type Hero { @webformIdToUrl(id: "$") } +union CommonContent @resolveEditorBlockType = + | BlockMarkup + | BlockMedia + | BlockForm + | BlockImageTeasers + | BlockCta + | BlockImageWithText + | BlockQuote + | BlockHorizontalSeparator + | BlockAccordion + | BlockInfoGrid + union PageContent @resolveEditorBlockType = | BlockMarkup | BlockMedia @@ -214,6 +226,7 @@ union PageContent @resolveEditorBlockType = | BlockHorizontalSeparator | BlockAccordion | BlockInfoGrid + | BlockConditional type BlockForm @type(id: "custom/form") { url: Url @resolveEditorBlockAttribute(key: "formId") @webformIdToUrl(id: "$") @@ -335,6 +348,12 @@ type BlockHorizontalSeparator @type(id: "custom/horizontal-separator") { markup: Markup! @resolveEditorBlockMarkup } +type BlockConditional @type(id: "custom/conditional") { + displayFrom: String @resolveEditorBlockAttribute(key: "displayFrom") + displayTo: String @resolveEditorBlockAttribute(key: "displayTo") + content: [CommonContent] @resolveEditorBlockChildren +} + input PaginationInput { limit: Int! offset: Int! diff --git a/packages/ui/src/components/Organisms/PageContent/BlockConditional.tsx b/packages/ui/src/components/Organisms/PageContent/BlockConditional.tsx new file mode 100644 index 000000000..b401ead14 --- /dev/null +++ b/packages/ui/src/components/Organisms/PageContent/BlockConditional.tsx @@ -0,0 +1,30 @@ +import { BlockConditionalFragment } from '@custom/schema'; +import React, { useEffect, useState } from 'react'; + +import { isTruthy } from '../../../utils/isTruthy'; +import { CommonContent } from '../PageDisplay'; + +export function BlockConditional(props: BlockConditionalFragment) { + const [isActive, setIsActive] = useState(false); + useEffect(() => { + const active = { + scheduledDisplay: [ + props.displayFrom + ? new Date(props.displayFrom).getTime() <= new Date().getTime() + : true, + props.displayTo + ? new Date(props.displayTo).getTime() > new Date().getTime() + : true, + ].every(Boolean), + }; + setIsActive(Object.values(active).every(Boolean)); + }, []); + + return isActive ? ( + <> + {props.content?.filter(isTruthy).map((block, index) => { + return ; + })} + + ) : null; +} diff --git a/packages/ui/src/components/Organisms/PageDisplay.tsx b/packages/ui/src/components/Organisms/PageDisplay.tsx index c5999faf9..a97ef64ce 100644 --- a/packages/ui/src/components/Organisms/PageDisplay.tsx +++ b/packages/ui/src/components/Organisms/PageDisplay.tsx @@ -1,5 +1,5 @@ 'use client'; -import { PageFragment } from '@custom/schema'; +import { BlockConditionalFragment, PageFragment } from '@custom/schema'; import React from 'react'; import { isTruthy } from '../../utils/isTruthy'; @@ -7,6 +7,7 @@ import { UnreachableCaseError } from '../../utils/unreachable-case-error'; import { BreadCrumbs } from '../Molecules/Breadcrumbs'; import { PageTransition } from '../Molecules/PageTransition'; import { BlockAccordion } from './PageContent/BlockAccordion'; +import { BlockConditional } from './PageContent/BlockConditional'; import { BlockCta } from './PageContent/BlockCta'; import { BlockForm } from './PageContent/BlockForm'; import { BlockHorizontalSeparator } from './PageContent/BlockHorizontalSeparator'; @@ -25,32 +26,44 @@ export function PageDisplay(page: PageFragment) { {!page.hero && } {page.hero && } {page?.content?.filter(isTruthy).map((block, index) => { - switch (block.__typename) { - case 'BlockMedia': - return ; - case 'BlockMarkup': - return ; - case 'BlockForm': - return ; - case 'BlockImageTeasers': - return ; - case 'BlockCta': - return ; - case 'BlockImageWithText': - return ; - case 'BlockQuote': - return
; - case 'BlockHorizontalSeparator': - return ; - case 'BlockAccordion': - return ; - case 'BlockInfoGrid': - return ; - default: - throw new UnreachableCaseError(block); + if (block.__typename === 'BlockConditional') { + return ; + } else { + return ; } })}
); } + +type CommonContentBlock = NonNullable< + Required['content'][number] +>; + +export function CommonContent(props: CommonContentBlock) { + switch (props.__typename) { + case 'BlockMedia': + return ; + case 'BlockMarkup': + return ; + case 'BlockForm': + return ; + case 'BlockImageTeasers': + return ; + case 'BlockCta': + return ; + case 'BlockImageWithText': + return ; + case 'BlockQuote': + return
; + case 'BlockHorizontalSeparator': + return ; + case 'BlockAccordion': + return ; + case 'BlockInfoGrid': + return ; + default: + throw new UnreachableCaseError(props); + } +} diff --git a/tests/schema/specs/blocks.spec.ts b/tests/schema/specs/blocks.spec.ts index de9ae4ed0..b9a8d061c 100644 --- a/tests/schema/specs/blocks.spec.ts +++ b/tests/schema/specs/blocks.spec.ts @@ -655,3 +655,58 @@ test('Block - info grid', async () => { } `); }); + +test('Conditional', async () => { + const result = await fetch(gql` + { + _loadDrupalPage(id: "52ee5cc7-0ac5-49b5-8550-ce59476bd4ac") { + content { + __typename + ... on BlockConditional { + content { + ... on BlockMarkup { + markup + } + } + displayFrom + displayTo + } + } + } + } + `); + expect(result).toMatchInlineSnapshot(` + { + "data": { + "_loadDrupalPage": { + "content": [ + { + "__typename": "BlockConditional", + "content": [ + { + "markup": " +

Complete

+ ", + }, + ], + "displayFrom": "2024-05-16T11:05:00.000Z", + "displayTo": "2024-05-23T13:03:00.000Z", + }, + { + "__typename": "BlockConditional", + "content": [ + { + "markup": " +

No conditions

+ ", + }, + ], + "displayFrom": null, + "displayTo": null, + }, + ], + }, + }, + } + `); +});