A standalone paragraph with markup and link
+ + @@ -108,9 +98,9 @@ default: - + - + @@ -123,40 +113,29 @@ default: translations: de: status: - - - value: true + - value: true uid: - - - target_id: 1 + - target_id: 1 title: - - - value: 'Blocks: complete DE' + - value: 'Blocks: complete DE' created: - - - value: 1687338353 + - value: 1687338353 promote: - - - value: false + - value: false sticky: - - - value: false + - value: false moderation_state: - - - value: published + - value: published path: - - - alias: /blocks-complete + - alias: /blocks-complete langcode: de pathauto: 0 content_translation_source: - - - value: en + - value: en content_translation_outdated: - - - value: false + - value: false body: - - - value: |- + - value: |- diff --git a/packages/drupal/test_content/export.php b/packages/drupal/test_content/export.php index e7daf34b4..7572d38a0 100644 --- a/packages/drupal/test_content/export.php +++ b/packages/drupal/test_content/export.php @@ -13,6 +13,7 @@ // troubles if exported to the default content. 'path_alias', 'content_moderation_state', + 'oauth2_token', 'redirect', 'webform_submission', 'consumer', diff --git a/packages/schema/codegen.ts b/packages/schema/codegen.ts index 7dc41889a..7a0ea92c9 100644 --- a/packages/schema/codegen.ts +++ b/packages/schema/codegen.ts @@ -40,7 +40,7 @@ const config: CodegenConfig = { context: ['gatsby'], }, }, - // Directive autoloader for Drupla. + // Directive autoloader for Drupal. 'build/drupal-autoload.json': { plugins: ['@amazeelabs/codegen-autoloader'], config: { diff --git a/packages/schema/src/fragments/Page.gql b/packages/schema/src/fragments/Page.gql index cbf2227eb..3ecddb3c9 100644 --- a/packages/schema/src/fragments/Page.gql +++ b/packages/schema/src/fragments/Page.gql @@ -35,6 +35,7 @@ fragment Page on Page { ...BlockCta ...BlockImageWithText ...BlockQuote + ...BlockHorizontalSeparator } metaTags { tag diff --git a/packages/schema/src/fragments/PageContent/BlockCta.gql b/packages/schema/src/fragments/PageContent/BlockCta.gql index 2dae0fda2..db56f5580 100644 --- a/packages/schema/src/fragments/PageContent/BlockCta.gql +++ b/packages/schema/src/fragments/PageContent/BlockCta.gql @@ -2,4 +2,6 @@ fragment BlockCta on BlockCta { url text openInNewTab + icon + iconPosition } diff --git a/packages/schema/src/fragments/PageContent/BlockHorizontalSeparator.gql b/packages/schema/src/fragments/PageContent/BlockHorizontalSeparator.gql new file mode 100644 index 000000000..ef992a139 --- /dev/null +++ b/packages/schema/src/fragments/PageContent/BlockHorizontalSeparator.gql @@ -0,0 +1,5 @@ +fragment BlockHorizontalSeparator on BlockHorizontalSeparator { + # This is not really used, but until we have other real setting fields, we + # have to provide a dummy one. + markup +} diff --git a/packages/schema/src/operations/CurrentUser.gql b/packages/schema/src/operations/CurrentUser.gql new file mode 100644 index 000000000..56047b3d8 --- /dev/null +++ b/packages/schema/src/operations/CurrentUser.gql @@ -0,0 +1,8 @@ +query CurrentUser { + currentUser { + id + name + email + memberFor + } +} diff --git a/packages/schema/src/schema.graphql b/packages/schema/src/schema.graphql index ca52d0ef3..6ed19968b 100644 --- a/packages/schema/src/schema.graphql +++ b/packages/schema/src/schema.graphql @@ -73,6 +73,14 @@ directive @route( path: String! ) repeatable on FIELD_DEFINITION | SCALAR | UNION | ENUM | INTERFACE | OBJECT +""" +Retrieve the properties of the current user. + +Provided by the "silverback_gatsby" module. +implementation(drupal): custom.directives::currentUser +""" +directive @currentUser repeatable on FIELD_DEFINITION | SCALAR | UNION | ENUM | INTERFACE | OBJECT + enum Locale @default @value(string: "en") { en de @@ -206,6 +214,7 @@ union PageContent @resolveEditorBlockType = | BlockCta | BlockImageWithText | BlockQuote + | BlockHorizontalSeparator type BlockForm @type(id: "custom/form") { url: Url @resolveEditorBlockAttribute(key: "formId") @webformIdToUrl(id: "$") @@ -249,6 +258,31 @@ type BlockCta @type(id: "custom/cta") { url: Url @resolveEditorBlockAttribute(key: "url") text: String @resolveEditorBlockAttribute(key: "text") openInNewTab: Boolean @resolveEditorBlockAttribute(key: "openInNewTab") + icon: CTAIconType @resolveEditorBlockAttribute(key: "icon") + iconPosition: CTAIconPosition + @resolveEditorBlockAttribute(key: "iconPosition") +} + +enum CTAIconType { + NONE + ARROW +} + +enum CTAIconPosition { + AFTER + BEFORE +} + +type BlockImageWithText @type(id: "custom/image-with-text") { + image: MediaImage @resolveEditorBlockMedia + 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 } type BlockImageWithText @type(id: "custom/image-with-text") { @@ -270,6 +304,10 @@ type BlockQuote @type(id: "custom/quote") { image: MediaImage @resolveEditorBlockMedia } +type BlockHorizontalSeparator @type(id: "custom/horizontal-separator") { + markup: Markup! @resolveEditorBlockMarkup +} + input PaginationInput { limit: Int! offset: Int! @@ -298,6 +336,8 @@ type Query { stringTranslations: [TranslatableString!] @gatsbyNodes(type: "TranslatableString") + + currentUser: User @currentUser } type Mutation { @@ -322,6 +362,13 @@ type MutationError { field: String } +type User { + id: ID! + name: String! + email: String! + memberFor: String! +} + type MetaTag { tag: String! attributes: MetaTagAttributes! diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts index ca58622e6..c8db1415e 100644 --- a/packages/ui/.storybook/main.ts +++ b/packages/ui/.storybook/main.ts @@ -23,6 +23,10 @@ const config: StorybookConfig = { pluginTurbosnap({ rootDir: config.root ?? process.cwd() }), imagetools(), ], + // https://github.com/nextauthjs/next-auth/discussions/4566 + define: { + 'process.env': process.env, + }, } satisfies UserConfig), staticDirs: ['../static/public', '../static/stories'], stories: ['../src/**/*.stories.@(ts|tsx|mdx)'], diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index 28c5c4eb6..08121d802 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -5,6 +5,13 @@ import { Decorator } from '@storybook/react'; import React from 'react'; import { IntlProvider } from 'react-intl'; import { SWRConfig, useSWRConfig } from 'swr'; +import { + SessionContext as NextSessionContext, + SessionProvider, +} from 'next-auth/react'; +import { faker } from '@faker-js/faker'; +import { Session } from 'next-auth'; +import { useMemo } from 'react'; // Every story is wrapped in an IntlProvider by default. const IntlDecorator: Decorator = (Story) => ( @@ -21,6 +28,68 @@ const LocationDecorator: Decorator = (Story, ctx) => { ); }; +type AuthState = + | { data: Session; status: 'authenticated' } + | { data: null; status: 'unauthenticated' | 'loading' }; + +export const AUTH_STATES: Record< + string, + { title: string; session: AuthState | undefined } +> = { + unknown: { + title: 'Session Unknown', + session: undefined, + }, + loading: { + title: 'Session Loading', + session: { + data: null, + status: 'loading', + }, + }, + unauthenticated: { + title: 'Not Authenticated', + session: { + data: null, + status: 'unauthenticated', + }, + }, + authenticated: { + title: 'Authenticated', + session: { + data: { + user: { + name: faker.person.fullName(), + email: faker.internet.email(), + image: faker.image.avatar(), + }, + expires: faker.date.future().toString(), + }, + status: 'authenticated', + }, + }, +}; + +const SessionContext: React.FC<{ session: AuthState }> = ({ + session, + children, +}) => { + const value = useMemo((): AuthState => { + return session ? session : { data: undefined, status: 'unauthenticated' }; + }, [session]); + + return+ {intl.formatMessage( + { defaultMessage: 'Email: {email}', id: 'uPNlBw' }, + { + email: data?.currentUser?.email, + }, + )} +
++ {intl.formatMessage( + { + defaultMessage: 'Member for {member_for}', + id: 'OZlBcy', + }, + { + member_for: data?.currentUser?.memberFor, + }, + )} +
+ > + ) : ( ++ {intl.formatMessage({ + defaultMessage: 'Sign in to view your profile.', + id: 'K99M9A', + })} +
+ )} +A standalone paragraph with markup and link
", }, + { + "__typename": "BlockHorizontalSeparator", + }, { "__typename": "BlockMedia", "caption": "Media image", @@ -182,18 +207,24 @@ test('Blocks', async () => { }, { "__typename": "BlockCta", + "icon": null, + "iconPosition": null, "openInNewTab": null, "text": "Internal CTA", "url": "/en/drupal", }, { "__typename": "BlockCta", + "icon": "ARROW", + "iconPosition": null, "openInNewTab": true, "text": "External CTA", "url": "https://www.google.com", }, { "__typename": "BlockCta", + "icon": "ARROW", + "iconPosition": "BEFORE", "openInNewTab": null, "text": "CTA with link to media", "url": "/media/[numeric]", @@ -249,6 +280,8 @@ test('Blocks', async () => { }, { "__typename": "BlockCta", + "icon": null, + "iconPosition": null, "openInNewTab": null, "text": null, "url": null,