@iqual/nuxt-icms 1.6.1 → 1.6.2-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/__tests__/anchors.spec.ts +43 -0
- package/__tests__/contract/blokkliComponents.spec.ts +59 -0
- package/__tests__/contract/componentFragments.spec.ts +24 -0
- package/__tests__/contract/languages.spec.ts +63 -0
- package/__tests__/contract/project.ts +144 -0
- package/__tests__/hreflang.spec.ts +39 -0
- package/{tests → __tests__}/pages.spec.ts +14 -5
- package/app.config.ts +15 -0
- package/app.vue +3 -21
- package/assets/css/backend.css +56 -0
- package/assets/css/base/base.css +2 -2
- package/assets/css/base/typography.css +2 -2
- package/assets/css/ckeditor.css +3 -4
- package/assets/css/components/decorations.css +1 -1
- package/assets/css/components/editor.css +1 -1
- package/assets/css/components/grid-container.css +19 -19
- package/assets/css/fe-fixes.css +6 -12
- package/assets/css/theme/colors/color-primary.css +33 -11
- package/assets/css/theme/colors/color-secondary.css +33 -11
- package/assets/css/utilities/utilities.css +1 -1
- package/assets/symbols/anchor.svg +3 -0
- package/blokkli.editAdapter.ts +56 -0
- package/bundles.ts +6 -6
- package/cache/__tests__/pageCache.spec.ts +540 -0
- package/cache/cacheTagRegistry.ts +124 -0
- package/cache/enabledForRequest.ts +21 -0
- package/cache/multiCache.serverOptions.ts +58 -0
- package/cache/pageCache.ts +360 -0
- package/components/Blokkli/Anchor/icon.svg +3 -0
- package/components/Blokkli/Anchor/index.vue +13 -0
- package/components/Blokkli/AnchorNavigation/icon.svg +8 -0
- package/components/Blokkli/AnchorNavigation/index.vue +9 -0
- package/components/Blokkli/Card/Element/index.vue +0 -1
- package/components/Blokkli/Collapsible/Element/index.vue +5 -0
- package/components/Blokkli/FAQ/Element/index.vue +0 -1
- package/components/Blokkli/Fragment/Separator/icon.svg +4 -0
- package/components/Blokkli/Fragment/Separator/index.vue +87 -0
- package/components/Blokkli/Gallery/index.vue +1 -3
- package/components/Blokkli/IconText/Element/index.vue +0 -1
- package/components/Blokkli/Introduction/index.vue +0 -1
- package/components/Blokkli/Listing/icon.svg +9 -0
- package/components/Blokkli/Listing/index.vue +24 -0
- package/components/Blokkli/Logo/Element/index.vue +0 -1
- package/components/Blokkli/Table/index.vue +0 -1
- package/components/Blokkli/Team/Element/index.vue +0 -1
- package/components/Blokkli/Team/Layout/index.vue +0 -1
- package/components/Blokkli/Testimonial/Element/index.vue +0 -1
- package/components/Blokkli/Text/index.vue +0 -1
- package/components/DesignSystem/Atoms/Button.vue +3 -0
- package/components/DesignSystem/Atoms/GridRow.vue +3 -1
- package/components/DesignSystem/Molecules/ButtonGroup.vue +5 -4
- package/components/DesignSystem/Molecules/IconText.vue +6 -6
- package/components/DesignSystem/Molecules/MediaLightbox.vue +8 -8
- package/components/DesignSystem/Molecules/MediaSlider.vue +9 -11
- package/components/DesignSystem/Molecules/Partnership.vue +7 -4
- package/components/DesignSystem/Molecules/Table.vue +35 -19
- package/components/DesignSystem/Molecules/TitleComposition.vue +46 -15
- package/components/DesignSystem/Molecules/iFrame.vue +2 -2
- package/components/DesignSystem/Organisms/CallToAction.vue +8 -7
- package/components/DesignSystem/Organisms/CallToActionFeature.vue +8 -7
- package/components/DesignSystem/Organisms/CallToActionFullwidth.vue +9 -8
- package/components/DesignSystem/Organisms/ContentBlock.vue +26 -16
- package/components/DesignSystem/Organisms/FacetedListing.vue +38 -17
- package/components/DesignSystem/Organisms/FactCard.vue +6 -7
- package/components/DesignSystem/Organisms/FeatureCard.vue +14 -14
- package/components/DesignSystem/Organisms/FlipTeaser.vue +19 -19
- package/components/DesignSystem/Organisms/HeaderHero.vue +11 -11
- package/components/DesignSystem/Organisms/HeaderTextImage.vue +10 -10
- package/components/DesignSystem/Organisms/LinkList.vue +20 -12
- package/components/DesignSystem/Organisms/LogoElement.vue +7 -4
- package/components/DesignSystem/Organisms/Map.vue +1 -1
- package/components/DesignSystem/Organisms/SearchResults.vue +15 -6
- package/components/DesignSystem/Organisms/SpotlightCard.vue +18 -18
- package/components/DesignSystem/Organisms/TeamElement.vue +19 -18
- package/components/DesignSystem/Organisms/TeaserCard.vue +16 -17
- package/components/DesignSystem/Organisms/TeaserSimple.vue +4 -31
- package/components/DesignSystem/Organisms/Testimonial.vue +13 -13
- package/components/DesignSystem/Organisms/TextCard.vue +10 -10
- package/components/DesignSystem/Organisms/TimelineElement.vue +20 -19
- package/components/DesignSystem/Organisms/TwoColumnTextElement.vue +5 -5
- package/components/DesignSystem/README.md +59 -40
- package/components/Drupal/Breadcrumb/index.vue +25 -12
- package/components/Drupal/Messages/index.vue +9 -4
- package/components/Drupal/Webform/index.vue +271 -51
- package/components/Drupal/Webform/mutation.graphql +17 -1
- package/components/Form/HiddenInput.vue +1 -4
- package/components/Form/SelectInput.vue +1 -1
- package/components/Form/SubmitButton.vue +2 -2
- package/components/Form/TextAreaInput.vue +3 -3
- package/components/Form/TextEmailInput.vue +1 -1
- package/components/Form/TextInput.vue +1 -1
- package/components/Form/TextPhoneInput.vue +1 -1
- package/components/Form/WebformMarkup.vue +1 -1
- package/components/Form/composables/useFormDefaultValue.ts +9 -13
- package/components/Form/composables/useFormElement.ts +5 -3
- package/components/Form/utils/types.ts +3 -3
- package/components/Media/Image/fragment.graphql +4 -0
- package/components/Media/Image/index.vue +27 -17
- package/components/Media/RemoteVideo/index.vue +3 -3
- package/components/Media/Video/index.vue +1 -1
- package/components/Node/Icms/Page/SearchResult/fragment.graphql +1 -1
- package/components/Node/Icms/Page/SearchResult/index.vue +2 -2
- package/components/Node/Icms/Page/Teaser/index.vue +1 -1
- package/components/Node/Icms/Page/index.vue +8 -2
- package/components/Paragraph/Anchor/fragment.graphql +4 -0
- package/components/Paragraph/Anchor/index.vue +48 -0
- package/components/Paragraph/AnchorNavigation/fragment.graphql +3 -0
- package/components/Paragraph/AnchorNavigation/index.vue +154 -0
- package/components/Paragraph/BlokkliFragment/fragment.graphql +5 -0
- package/components/Paragraph/Button/Element/fragment.graphql +1 -1
- package/components/Paragraph/Button/Element/index.vue +13 -8
- package/components/Paragraph/Card/Element/index.vue +1 -1
- package/components/Paragraph/Card/Layout/index.vue +9 -9
- package/components/Paragraph/Collapsible/Element/index.vue +2 -2
- package/components/Paragraph/Collapsible/Layout/index.vue +19 -18
- package/components/Paragraph/Contact/index.vue +17 -17
- package/components/Paragraph/Downloads/fragment.graphql +1 -1
- package/components/Paragraph/Downloads/index.vue +13 -13
- package/components/Paragraph/FAQ/Element/index.vue +4 -3
- package/components/Paragraph/FAQ/Layout/index.vue +42 -21
- package/components/Paragraph/FromLibrary/fragment.graphql +17 -0
- package/components/Paragraph/Gallery/index.vue +14 -14
- package/components/Paragraph/HeroImage/index.vue +26 -19
- package/components/Paragraph/IconText/Element/index.vue +4 -4
- package/components/Paragraph/IconText/Layout/index.vue +12 -12
- package/components/Paragraph/Introduction/index.vue +17 -17
- package/components/Paragraph/LinkLists/Element/fragment.graphql +1 -1
- package/components/Paragraph/LinkLists/Element/index.vue +5 -5
- package/components/Paragraph/LinkLists/Layout/index.vue +9 -9
- package/components/Paragraph/Listing/fragment.graphql +14 -0
- package/components/Paragraph/Listing/index.vue +91 -0
- package/components/Paragraph/Logo/Element/fragment.graphql +1 -1
- package/components/Paragraph/Logo/Element/index.vue +7 -4
- package/components/Paragraph/Media/index.vue +23 -21
- package/components/Paragraph/Partnerships/Element/fragment.graphql +1 -1
- package/components/Paragraph/Partnerships/Element/index.vue +10 -7
- package/components/Paragraph/Partnerships/Layout/index.vue +13 -13
- package/components/Paragraph/SingleTestimonial/index.vue +13 -13
- package/components/Paragraph/Table/index.vue +12 -12
- package/components/Paragraph/Team/Element/index.vue +16 -14
- package/components/Paragraph/Team/Layout/index.vue +9 -9
- package/components/Paragraph/Teasers/index.vue +14 -17
- package/components/Paragraph/Testimonial/Element/index.vue +19 -18
- package/components/Paragraph/Testimonial/Layout/index.vue +24 -18
- package/components/Paragraph/Text/fragment.graphql +1 -1
- package/components/Paragraph/Text/index.vue +8 -8
- package/components/Paragraph/TextMedia/index.vue +22 -22
- package/components/Paragraph/Timeline/Element/index.vue +19 -19
- package/components/Paragraph/Timeline/Layout/index.vue +51 -20
- package/components/Paragraph/TwoColumnText/Element/index.vue +13 -13
- package/components/Paragraph/TwoColumnText/Layout/index.vue +1 -1
- package/components/Paragraph/iFrame/index.vue +8 -8
- package/components/Search/Facets/FacetDropdownUI.vue +25 -31
- package/components/Search/Facets/FacetedSearchUI.vue +72 -48
- package/components/Search/Facets/fragment.graphql +20 -15
- package/components/Section/Footer/index.vue +55 -32
- package/components/Section/Header/Buttons.vue +4 -5
- package/components/Section/Header/DesktopMainMenu.vue +5 -4
- package/components/Section/Header/DesktopMainMenuDropdown.vue +5 -5
- package/components/Section/Header/LanguageDropdown.vue +2 -3
- package/components/Section/Header/Logo.vue +3 -3
- package/components/Section/Header/MainButtons.vue +4 -5
- package/components/Section/Header/MetaButtons.vue +5 -6
- package/components/Section/Header/MetaMenu.vue +1 -1
- package/components/Section/Header/MobileMainMenu.vue +1 -1
- package/components/Section/Header/MobileMainMenuItem.vue +2 -2
- package/components/Section/Header/index.vue +6 -13
- package/composables/__tests__/useImageSources.spec.ts +91 -0
- package/composables/breadcrumb.ts +12 -7
- package/composables/useAccordionScroll.ts +12 -4
- package/composables/useAllowedFragments.ts +14 -0
- package/composables/useAnchors.ts +48 -0
- package/composables/useChildTitleWeight.ts +1 -1
- package/composables/useCurrentLangcodeEnum.ts +1 -1
- package/composables/useCurrentNodeId.ts +8 -0
- package/composables/useDrupalUrl.ts +15 -0
- package/composables/useEnrichFacetLabels.ts +8 -6
- package/composables/useEntityRegistry.ts +3 -3
- package/composables/useGlobalConfig.ts +1 -2
- package/composables/useGraphqlCacheability.ts +12 -1
- package/composables/useHreflangLinks.ts +54 -0
- package/composables/useImageSources.ts +70 -28
- package/composables/useInitData.ts +1 -2
- package/composables/useIsHomepage.ts +1 -1
- package/composables/useLinkUrl.ts +30 -0
- package/composables/useMediaLightbox.ts +62 -50
- package/composables/useMenu.ts +1 -3
- package/composables/useMessageBus.ts +14 -7
- package/composables/useMobileMenuOverlayBus.ts +1 -1
- package/composables/useNavigationOffset.ts +49 -6
- package/composables/usePageCache.ts +33 -0
- package/composables/useSiteShell.ts +11 -0
- package/composables/useTheme.ts +17 -13
- package/config/__tests__/stubs/nuxt-multi-cache-config.ts +2 -0
- package/config/blokkli.ts +7 -13
- package/config/easyTexts.ts +1 -1
- package/config/graphqlMiddleware.ts +11 -4
- package/config/index.ts +5 -2
- package/config/languageNegotiation.ts +1 -1
- package/config/multiCache.ts +6 -1
- package/config/postcss-drupal/postcss.config.cjs +2 -2
- package/config/runtimeConfig.ts +7 -2
- package/config/svgIconSprite.ts +1 -2
- package/config/vuepal.ts +16 -18
- package/error.vue +1 -22
- package/layouts/default.vue +2 -6
- package/layouts/error.vue +7 -15
- package/modules/blokkliFragments.ts +76 -0
- package/modules/dynamicEntityComponentResolver.ts +47 -47
- package/modules/dynamicParagraphResolver.ts +43 -8
- package/modules/pageCache.ts +106 -0
- package/nuxt.config.ts +62 -33
- package/package.json +23 -22
- package/pages/[...slug]/index.vue +25 -4
- package/pages/index.vue +3 -3
- package/pages/node/[nid]/index.vue +2 -3
- package/patches/@blokkli+editor+2.0.0-alpha.13.patch +15 -1
- package/plugins/currentNode.ts +17 -0
- package/plugins/graphqlMiddleware.ts +10 -2
- package/plugins/initData.ts +15 -7
- package/plugins/registerNodeIcmsPageComponents.ts +6 -2
- package/plugins/siteShell.client.ts +94 -0
- package/queries/facetTermLabels.graphql +1 -5
- package/queries/fragment.linkUrl.graphql +12 -0
- package/queries/query.contentSearch.graphql +17 -2
- package/queries/query.initData.graphql +1 -1
- package/scripts/create-shade.ts +13 -4
- package/scripts/update-icms.ts +3 -1
- package/server/__tests__/graphqlMiddleware.serverOptions.spec.ts +2 -2
- package/server/__tests__/turnstile.spec.ts +119 -0
- package/server/api/icms/blokkli/last-changed.get.ts +24 -0
- package/server/api/icon/[...params].ts +3 -9
- package/server/graphqlMiddleware.serverOptions.ts +9 -8
- package/server/helpers/index.ts +0 -1
- package/server/middleware/turnstile.ts +51 -0
- package/server/plugins/frameAncestors.ts +20 -0
- package/server/plugins/pageCache.ts +59 -0
- package/server/utils/__tests__/accessDeniedHandover.spec.ts +49 -0
- package/server/utils/__tests__/backendRequestHeaders.spec.ts +115 -0
- package/server/utils/accessDeniedHandover.ts +48 -0
- package/server/utils/backendRequestHeaders.ts +51 -0
- package/server/utils/cacheability.ts +4 -4
- package/shared/__tests__/blokkliState.spec.ts +54 -0
- package/shared/__tests__/csp.spec.ts +53 -0
- package/shared/__tests__/drupalUrl.spec.ts +50 -0
- package/shared/blokkliState.ts +40 -0
- package/shared/csp.ts +47 -0
- package/shared/drupalUrl.ts +30 -0
- package/tailwind.config.ts +0 -5
- package/types/breadcrumb.ts +22 -0
- package/types/global.ts +15 -14
- package/types/langcode.ts +16 -0
- package/types/layerConfig.ts +84 -0
- package/utils/pageCache.ts +95 -0
- package/utils/turnstile.ts +35 -0
- package/composables/useWebformEnrichment.ts +0 -68
- package/server/__tests__/graphqlMiddleware.ts +0 -41
- package/server/multiCache.serverOptions.ts +0 -32
- package/vitest.config.mts +0 -28
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { createEvent } from 'h3'
|
|
3
|
+
import { IncomingMessage, ServerResponse } from 'node-mock-http'
|
|
4
|
+
|
|
5
|
+
const metaMock = vi.hoisted(() => ({ importMetaClient: false }))
|
|
6
|
+
vi.mock('#shared/importMeta', () => metaMock)
|
|
7
|
+
|
|
8
|
+
// The app-level helper the layer builds on (frontend/shared/request.ts).
|
|
9
|
+
vi.mock('#shared/request', async () => {
|
|
10
|
+
const { getRequestHeaders } = await import('h3')
|
|
11
|
+
const allowed = new Set([
|
|
12
|
+
'x-forwarded-host',
|
|
13
|
+
'x-forwarded-proto',
|
|
14
|
+
'x-forwarded-for',
|
|
15
|
+
'x-forwarded-port',
|
|
16
|
+
'x-client-ip',
|
|
17
|
+
'cookie',
|
|
18
|
+
])
|
|
19
|
+
return {
|
|
20
|
+
buildProxyRequestHeaders: (
|
|
21
|
+
event: Parameters<typeof getRequestHeaders>[0],
|
|
22
|
+
) => {
|
|
23
|
+
const out: Record<string, string> = {}
|
|
24
|
+
for (const [name, value] of Object.entries(getRequestHeaders(event))) {
|
|
25
|
+
if (allowed.has(name) && value) out[name] = value
|
|
26
|
+
}
|
|
27
|
+
return out
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
vi.stubGlobal('useRuntimeConfig', () => ({
|
|
33
|
+
drupalGraphqlToken: 'runtime-token',
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
const { buildBackendRequestHeaders, GRAPHQL_TOKEN_HEADER } =
|
|
37
|
+
await import('../backendRequestHeaders')
|
|
38
|
+
|
|
39
|
+
function eventWithHeaders(headers: Record<string, string>) {
|
|
40
|
+
const request = new IncomingMessage()
|
|
41
|
+
request.headers = headers
|
|
42
|
+
return createEvent(request, new ServerResponse(request))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('buildBackendRequestHeaders', () => {
|
|
46
|
+
it('sets the forwarded host and scheme from the request', () => {
|
|
47
|
+
expect(
|
|
48
|
+
buildBackendRequestHeaders(
|
|
49
|
+
eventWithHeaders({ host: 'www.example.com', cookie: 'SSESSabc=1' }),
|
|
50
|
+
'token',
|
|
51
|
+
),
|
|
52
|
+
).toEqual({
|
|
53
|
+
cookie: 'SSESSabc=1',
|
|
54
|
+
'x-forwarded-host': 'www.example.com',
|
|
55
|
+
'x-forwarded-proto': 'https',
|
|
56
|
+
[GRAPHQL_TOKEN_HEADER]: 'token',
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('overwrites forwarded headers sent by the client', () => {
|
|
61
|
+
expect(
|
|
62
|
+
buildBackendRequestHeaders(
|
|
63
|
+
eventWithHeaders({
|
|
64
|
+
host: 'www.example.com',
|
|
65
|
+
'x-forwarded-host': 'evil.example',
|
|
66
|
+
'x-forwarded-proto': 'http',
|
|
67
|
+
}),
|
|
68
|
+
'token',
|
|
69
|
+
),
|
|
70
|
+
).toMatchObject({
|
|
71
|
+
'x-forwarded-host': 'www.example.com',
|
|
72
|
+
'x-forwarded-proto': 'https',
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('drops forwarded headers Drupal does not trust', () => {
|
|
77
|
+
const headers = buildBackendRequestHeaders(
|
|
78
|
+
eventWithHeaders({
|
|
79
|
+
host: 'www.example.com',
|
|
80
|
+
'x-forwarded-for': '203.0.113.1',
|
|
81
|
+
'x-forwarded-port': '8443',
|
|
82
|
+
'x-client-ip': '198.51.100.7',
|
|
83
|
+
}),
|
|
84
|
+
'token',
|
|
85
|
+
)
|
|
86
|
+
expect(headers).not.toHaveProperty('x-forwarded-for')
|
|
87
|
+
expect(headers).not.toHaveProperty('x-forwarded-port')
|
|
88
|
+
// The router-set client IP is what Drupal uses as REMOTE_ADDR.
|
|
89
|
+
expect(headers['x-client-ip']).toBe('198.51.100.7')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('drops a client-sent forwarded host without a Host header', () => {
|
|
93
|
+
expect(
|
|
94
|
+
buildBackendRequestHeaders(
|
|
95
|
+
eventWithHeaders({ 'x-forwarded-host': 'evil.example' }),
|
|
96
|
+
'token',
|
|
97
|
+
),
|
|
98
|
+
).not.toHaveProperty('x-forwarded-host')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('sends the GraphQL token from the runtime config', () => {
|
|
102
|
+
expect(
|
|
103
|
+
buildBackendRequestHeaders(eventWithHeaders({ host: 'www.example.com' })),
|
|
104
|
+
).toMatchObject({ [GRAPHQL_TOKEN_HEADER]: 'runtime-token' })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('omits the token header when no token is configured', () => {
|
|
108
|
+
expect(
|
|
109
|
+
buildBackendRequestHeaders(
|
|
110
|
+
eventWithHeaders({ host: 'www.example.com' }),
|
|
111
|
+
'',
|
|
112
|
+
),
|
|
113
|
+
).not.toHaveProperty(GRAPHQL_TOKEN_HEADER)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The part of a route query response this module cares about.
|
|
3
|
+
*/
|
|
4
|
+
interface RouteQueryDataLike {
|
|
5
|
+
route?: {
|
|
6
|
+
__typename?: string
|
|
7
|
+
path?: string | null
|
|
8
|
+
redirect?: { statusCode?: number | null } | null
|
|
9
|
+
} | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hands a Drupal route the visitor may not access over to the backend.
|
|
14
|
+
*
|
|
15
|
+
* For a Drupal route that exists but is not accessible (e.g. a revision page
|
|
16
|
+
* for an anonymous visitor), the GraphQL route resolver answers with a
|
|
17
|
+
* redirect to Drupal's login form, with the path as ?destination. The login
|
|
18
|
+
* form lives on the backend domain — so does the page the visitor wanted —
|
|
19
|
+
* and following the redirect on the frontend would only get there through
|
|
20
|
+
* the router's /user redirect. Send the visitor to the page on the backend
|
|
21
|
+
* directly instead: Drupal then decides between its 403 and its login form.
|
|
22
|
+
*/
|
|
23
|
+
export function handoverAccessDeniedRoute<T>(
|
|
24
|
+
data: T,
|
|
25
|
+
backendOrigin: string,
|
|
26
|
+
): T {
|
|
27
|
+
const route = (data as RouteQueryDataLike | null | undefined)?.route
|
|
28
|
+
if (
|
|
29
|
+
!route ||
|
|
30
|
+
route.redirect?.statusCode !== 403 ||
|
|
31
|
+
typeof route.path !== 'string' ||
|
|
32
|
+
!/^\/(?:[a-z]{2}\/)?user\/login(?:[/?]|$)/.test(route.path)
|
|
33
|
+
) {
|
|
34
|
+
return data
|
|
35
|
+
}
|
|
36
|
+
const destination =
|
|
37
|
+
new URL(route.path, 'http://localhost').searchParams.get('destination') ||
|
|
38
|
+
''
|
|
39
|
+
const path = /^\/(?!\/)/.test(destination) ? destination : route.path
|
|
40
|
+
return {
|
|
41
|
+
...data,
|
|
42
|
+
route: {
|
|
43
|
+
...route,
|
|
44
|
+
path: backendOrigin + path,
|
|
45
|
+
redirect: { ...route.redirect, statusCode: 302 },
|
|
46
|
+
},
|
|
47
|
+
} as T
|
|
48
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { H3Event } from 'h3'
|
|
2
|
+
import { getRequestHeader } from 'h3'
|
|
3
|
+
import { buildProxyRequestHeaders } from '#shared/request'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Carries the token Drupal and the frontend share (DRUPAL_GRAPHQL_TOKEN): it
|
|
7
|
+
* grants access to GraphQL, and Drupal only trusts X-Forwarded-Host and
|
|
8
|
+
* X-Forwarded-Proto on requests carrying it (see settings.php).
|
|
9
|
+
*/
|
|
10
|
+
export const GRAPHQL_TOKEN_HEADER = 'x-drupal-graphql-token'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Forwarded headers not passed on: the host is set from the request below,
|
|
14
|
+
* and Drupal never trusts the others, which clients can set.
|
|
15
|
+
*/
|
|
16
|
+
const DROPPED_HEADERS = new Set([
|
|
17
|
+
'x-forwarded-host',
|
|
18
|
+
'x-forwarded-for',
|
|
19
|
+
'x-forwarded-port',
|
|
20
|
+
'x-forwarded-server',
|
|
21
|
+
'forwarded',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Headers for a server-side request to the Drupal backend.
|
|
26
|
+
*
|
|
27
|
+
* Extends the proxied request headers with the public host and scheme and the
|
|
28
|
+
* GraphQL token: Drupal derives the URLs it generates and the session cookie
|
|
29
|
+
* from X-Forwarded-Host. The host always comes from the request's Host header
|
|
30
|
+
* (the router only routes known domains), never from a client-sent
|
|
31
|
+
* X-Forwarded-Host.
|
|
32
|
+
*/
|
|
33
|
+
export function buildBackendRequestHeaders(
|
|
34
|
+
event: H3Event,
|
|
35
|
+
token: string = useRuntimeConfig().drupalGraphqlToken,
|
|
36
|
+
): Record<string, string> {
|
|
37
|
+
const headers = Object.fromEntries(
|
|
38
|
+
Object.entries(buildProxyRequestHeaders(event)).filter(
|
|
39
|
+
([name]) => !DROPPED_HEADERS.has(name),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
const host = getRequestHeader(event, 'host')
|
|
43
|
+
if (host) {
|
|
44
|
+
headers['x-forwarded-host'] = host
|
|
45
|
+
}
|
|
46
|
+
headers['x-forwarded-proto'] = 'https'
|
|
47
|
+
if (token) {
|
|
48
|
+
headers[GRAPHQL_TOKEN_HEADER] = token
|
|
49
|
+
}
|
|
50
|
+
return headers
|
|
51
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { FetchResponse } from 'ofetch'
|
|
|
2
2
|
import type { H3Event } from 'h3'
|
|
3
3
|
import crypto from 'crypto'
|
|
4
4
|
import { INTERVALS, MAX_AGE } from '../helpers'
|
|
5
|
-
import type { ValidInterval } from '../helpers'
|
|
5
|
+
import type { GraphqlCacheability, ValidInterval } from '../helpers'
|
|
6
6
|
|
|
7
7
|
function extractCacheTags(
|
|
8
8
|
response: FetchResponse<unknown>,
|
|
@@ -45,7 +45,7 @@ function calculateMaxAge(expires: string | null): number {
|
|
|
45
45
|
export function extractCacheability(
|
|
46
46
|
response: FetchResponse<unknown>,
|
|
47
47
|
event: H3Event,
|
|
48
|
-
) {
|
|
48
|
+
): GraphqlCacheability {
|
|
49
49
|
const hasSessionCookie = (event.node.req.headers.cookie || '').includes(
|
|
50
50
|
'SSESS',
|
|
51
51
|
)
|
|
@@ -53,7 +53,7 @@ export function extractCacheability(
|
|
|
53
53
|
// cache the initData in the cache of nuxt_multi_cache in the frontend.
|
|
54
54
|
const tagsNuxt = extractCacheTags(response, 'x-nuxt-cache-tags')
|
|
55
55
|
|
|
56
|
-
// These tags are provided by the generic http purge module and
|
|
56
|
+
// These tags are provided by the generic http purge module and
|
|
57
57
|
// used to invalidate SSR full page caches on varnish.
|
|
58
58
|
const tagsCdn = extractCacheTags(response, 'purge-cache-tags')
|
|
59
59
|
|
|
@@ -136,4 +136,4 @@ export function buildFastlyCacheTags(input: string | string[]): string[] {
|
|
|
136
136
|
.digest('base64')
|
|
137
137
|
.substring(0, 6)
|
|
138
138
|
})
|
|
139
|
-
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { withoutTranslationEditUrls } from '../blokkliState'
|
|
3
|
+
|
|
4
|
+
describe('withoutTranslationEditUrls', () => {
|
|
5
|
+
it('removes the editUrl from every translation', () => {
|
|
6
|
+
const state = {
|
|
7
|
+
currentIndex: 2,
|
|
8
|
+
translationState: {
|
|
9
|
+
isTranslatable: true,
|
|
10
|
+
sourceLanguage: 'de',
|
|
11
|
+
translations: [
|
|
12
|
+
{
|
|
13
|
+
id: 'de',
|
|
14
|
+
url: '/de/seite',
|
|
15
|
+
editUrl: '/de/node/1/edit',
|
|
16
|
+
exists: true,
|
|
17
|
+
status: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'fr',
|
|
21
|
+
url: '/fr/node/1',
|
|
22
|
+
editUrl:
|
|
23
|
+
'/fr/node/1/translations/add/de/fr?destination=/paragraphs_blokkli/redirect',
|
|
24
|
+
exists: false,
|
|
25
|
+
status: false,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = withoutTranslationEditUrls(state)
|
|
32
|
+
|
|
33
|
+
expect(result.translationState.translations).toEqual([
|
|
34
|
+
{ id: 'de', url: '/de/seite', exists: true, status: true },
|
|
35
|
+
{ id: 'fr', url: '/fr/node/1', exists: false, status: false },
|
|
36
|
+
])
|
|
37
|
+
expect(result.translationState.isTranslatable).toBe(true)
|
|
38
|
+
expect(result.translationState.sourceLanguage).toBe('de')
|
|
39
|
+
expect(result.currentIndex).toBe(2)
|
|
40
|
+
// The adapter's state is not mutated.
|
|
41
|
+
expect(state.translationState.translations[1].editUrl).toContain(
|
|
42
|
+
'translations/add',
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns the state as is without translations', () => {
|
|
47
|
+
const untranslatable = {
|
|
48
|
+
translationState: { isTranslatable: false, translations: [] },
|
|
49
|
+
}
|
|
50
|
+
expect(withoutTranslationEditUrls(untranslatable)).toBe(untranslatable)
|
|
51
|
+
const noState = { currentIndex: 0 }
|
|
52
|
+
expect(withoutTranslationEditUrls(noState)).toBe(noState)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mergeFrameAncestors } from '../csp'
|
|
3
|
+
|
|
4
|
+
const sources = ["'self'", 'https://backend.www.example.com']
|
|
5
|
+
|
|
6
|
+
describe('mergeFrameAncestors', () => {
|
|
7
|
+
it('creates the policy when there is none', () => {
|
|
8
|
+
expect(mergeFrameAncestors(undefined, sources)).toEqual([
|
|
9
|
+
"frame-ancestors 'self' https://backend.www.example.com",
|
|
10
|
+
])
|
|
11
|
+
expect(mergeFrameAncestors('', sources)).toEqual([
|
|
12
|
+
"frame-ancestors 'self' https://backend.www.example.com",
|
|
13
|
+
])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('appends the directive to a policy without one', () => {
|
|
17
|
+
expect(mergeFrameAncestors("default-src 'self'; ", sources)).toEqual([
|
|
18
|
+
"default-src 'self'; frame-ancestors 'self' https://backend.www.example.com",
|
|
19
|
+
])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('keeps existing sources and replaces none', () => {
|
|
23
|
+
expect(
|
|
24
|
+
mergeFrameAncestors(
|
|
25
|
+
"default-src 'self'; frame-ancestors 'self' https://other.example.org; img-src *",
|
|
26
|
+
sources,
|
|
27
|
+
),
|
|
28
|
+
).toEqual([
|
|
29
|
+
"default-src 'self'; frame-ancestors 'self' https://other.example.org https://backend.www.example.com; img-src *",
|
|
30
|
+
])
|
|
31
|
+
expect(mergeFrameAncestors("frame-ancestors 'none'", sources)).toEqual([
|
|
32
|
+
"frame-ancestors 'self' https://backend.www.example.com",
|
|
33
|
+
])
|
|
34
|
+
expect(
|
|
35
|
+
mergeFrameAncestors(
|
|
36
|
+
"frame-ancestors https://backend.www.example.com 'self'",
|
|
37
|
+
sources,
|
|
38
|
+
),
|
|
39
|
+
).toEqual(["frame-ancestors https://backend.www.example.com 'self'"])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('merges into the right one of several header values', () => {
|
|
43
|
+
expect(
|
|
44
|
+
mergeFrameAncestors(
|
|
45
|
+
["default-src 'self'", "frame-ancestors 'self'"],
|
|
46
|
+
sources,
|
|
47
|
+
),
|
|
48
|
+
).toEqual([
|
|
49
|
+
"default-src 'self'",
|
|
50
|
+
"frame-ancestors 'self' https://backend.www.example.com",
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { drupalOrigin, toDrupalUrl } from '../drupalUrl'
|
|
3
|
+
|
|
4
|
+
describe('drupalOrigin', () => {
|
|
5
|
+
it('prefixes the request host with the backend subdomain', () => {
|
|
6
|
+
expect(drupalOrigin(new URL('https://www.example.com/de/about?x=1'))).toBe(
|
|
7
|
+
'https://backend.www.example.com',
|
|
8
|
+
)
|
|
9
|
+
expect(drupalOrigin(new URL('https://example.com/'))).toBe(
|
|
10
|
+
'https://backend.example.com',
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('keeps scheme and port', () => {
|
|
15
|
+
expect(drupalOrigin(new URL('http://starterkit.ddev.site:8080/'))).toBe(
|
|
16
|
+
'http://backend.starterkit.ddev.site:8080',
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('toDrupalUrl', () => {
|
|
22
|
+
it('returns the path unchanged without a backend origin', () => {
|
|
23
|
+
expect(toDrupalUrl('/de/user/login')).toBe('/de/user/login')
|
|
24
|
+
expect(toDrupalUrl('/de/user/login', '')).toBe('/de/user/login')
|
|
25
|
+
expect(toDrupalUrl('/de/user/login', null)).toBe('/de/user/login')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('prefixes the path with the backend origin', () => {
|
|
29
|
+
expect(
|
|
30
|
+
toDrupalUrl('/de/user/login', 'https://backend.www.example.com'),
|
|
31
|
+
).toBe('https://backend.www.example.com/de/user/login')
|
|
32
|
+
expect(
|
|
33
|
+
toDrupalUrl(
|
|
34
|
+
'/paragraphs_blokkli/x?paragraphsBlokkli=true',
|
|
35
|
+
'https://backend.example.com/',
|
|
36
|
+
),
|
|
37
|
+
).toBe(
|
|
38
|
+
'https://backend.example.com/paragraphs_blokkli/x?paragraphsBlokkli=true',
|
|
39
|
+
)
|
|
40
|
+
expect(toDrupalUrl('de/user', 'https://backend.example.com')).toBe(
|
|
41
|
+
'https://backend.example.com/de/user',
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('leaves absolute URLs alone', () => {
|
|
46
|
+
expect(
|
|
47
|
+
toDrupalUrl('https://other.example.com/x', 'https://backend.example.com'),
|
|
48
|
+
).toBe('https://other.example.com/x')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blökkli's mapped edit state, as far as this module cares.
|
|
3
|
+
*/
|
|
4
|
+
interface TranslationStateLike {
|
|
5
|
+
translationState?: {
|
|
6
|
+
translations?: Array<{ editUrl?: string | null }>
|
|
7
|
+
} | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drops the Drupal edit URLs from the translations of blökkli's state.
|
|
12
|
+
*
|
|
13
|
+
* paragraphs_blokkli sends an `editUrl` per language (Drupal's
|
|
14
|
+
* content-translation add or edit form with `?destination`), and blökkli's
|
|
15
|
+
* edit-form feature loads that URL directly when an editor creates a
|
|
16
|
+
* translation. It is a relative URL without the `paragraphsBlokkli` marker,
|
|
17
|
+
* so the iframe shows the form with Drupal's whole navigation and toolbar, and
|
|
18
|
+
* on a split domain it first lands on the frontend, which hands it over to the
|
|
19
|
+
* backend. Without the URL blökkli asks the adapter's form frame builder
|
|
20
|
+
* ("entity:translate") instead, whose paragraphs_blokkli route redirects to
|
|
21
|
+
* the same form with the marker and on the backend domain: the way the page
|
|
22
|
+
* settings ("entity:edit") open.
|
|
23
|
+
*/
|
|
24
|
+
export function withoutTranslationEditUrls<T extends TranslationStateLike>(
|
|
25
|
+
state: T,
|
|
26
|
+
): T {
|
|
27
|
+
const translations = state.translationState?.translations
|
|
28
|
+
if (!translations?.length) {
|
|
29
|
+
return state
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...state,
|
|
33
|
+
translationState: {
|
|
34
|
+
...state.translationState,
|
|
35
|
+
translations: translations.map(
|
|
36
|
+
({ editUrl: _editUrl, ...translation }) => translation,
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
} as T
|
|
40
|
+
}
|
package/shared/csp.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds sources to the frame-ancestors directive of a Content-Security-Policy.
|
|
3
|
+
*
|
|
4
|
+
* A policy without the directive gets it appended, a policy that has one keeps
|
|
5
|
+
* its sources and gains the missing ones ('none' cannot be combined and is
|
|
6
|
+
* dropped); without any policy one is created. Several header values are
|
|
7
|
+
* merged into the first one that has the directive, or the first one.
|
|
8
|
+
*/
|
|
9
|
+
export function mergeFrameAncestors(
|
|
10
|
+
policy: string | string[] | number | undefined | null,
|
|
11
|
+
sources: string[],
|
|
12
|
+
): string[] {
|
|
13
|
+
const policies = (Array.isArray(policy) ? policy : [policy])
|
|
14
|
+
.filter((p): p is string => typeof p === 'string' && p.trim() !== '')
|
|
15
|
+
.map((p) => p.trim())
|
|
16
|
+
if (!policies.length) {
|
|
17
|
+
return [`frame-ancestors ${sources.join(' ')}`]
|
|
18
|
+
}
|
|
19
|
+
let merged = false
|
|
20
|
+
const result = policies.map((p) => {
|
|
21
|
+
const directives = p
|
|
22
|
+
.split(';')
|
|
23
|
+
.map((d) => d.trim())
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((directive) => {
|
|
26
|
+
const match = directive.match(/^frame-ancestors(?:\s+(.*))?$/i)
|
|
27
|
+
if (!match) {
|
|
28
|
+
return directive
|
|
29
|
+
}
|
|
30
|
+
const existing = (match[1] || '')
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.filter((s) => s && s.toLowerCase() !== "'none'")
|
|
33
|
+
for (const source of sources) {
|
|
34
|
+
if (!existing.includes(source)) {
|
|
35
|
+
existing.push(source)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
merged = true
|
|
39
|
+
return `frame-ancestors ${existing.join(' ')}`
|
|
40
|
+
})
|
|
41
|
+
return directives.join('; ')
|
|
42
|
+
})
|
|
43
|
+
if (!merged) {
|
|
44
|
+
result[0] = `${result[0]}; frame-ancestors ${sources.join(' ')}`
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subdomain label of the Drupal backend, see docs/routing.md.
|
|
3
|
+
*/
|
|
4
|
+
export const DRUPAL_SUBDOMAIN = 'backend'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The browser-facing origin of the Drupal backend for a frontend URL.
|
|
8
|
+
*
|
|
9
|
+
* The frontend serves the public site on a domain of its own and Drupal is
|
|
10
|
+
* reachable on the "backend." subdomain of that same domain, so the backend
|
|
11
|
+
* origin follows from the request: https://www.example.com →
|
|
12
|
+
* https://backend.www.example.com, with scheme and port kept.
|
|
13
|
+
*/
|
|
14
|
+
export function drupalOrigin(frontendUrl: URL | Location): string {
|
|
15
|
+
return `${frontendUrl.protocol}//${DRUPAL_SUBDOMAIN}.${frontendUrl.host}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds the browser-facing URL of a Drupal path.
|
|
20
|
+
*
|
|
21
|
+
* A Drupal path such as /de/user/login lives on the backend origin; without
|
|
22
|
+
* one the path is returned unchanged. Absolute URLs are never touched.
|
|
23
|
+
*/
|
|
24
|
+
export function toDrupalUrl(path: string, drupalUrl?: string | null): string {
|
|
25
|
+
if (!drupalUrl || /^[a-z][a-z0-9+.-]*:\/\//i.test(path)) {
|
|
26
|
+
return path
|
|
27
|
+
}
|
|
28
|
+
const base = drupalUrl.replace(/\/+$/, '')
|
|
29
|
+
return path.startsWith('/') ? base + path : `${base}/${path}`
|
|
30
|
+
}
|
package/tailwind.config.ts
CHANGED
|
@@ -2,16 +2,11 @@ import type { Config } from 'tailwindcss'
|
|
|
2
2
|
|
|
3
3
|
import plugin from 'tailwindcss/plugin'
|
|
4
4
|
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
|
-
import { dirname, join } from 'path'
|
|
7
|
-
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
|
|
9
5
|
const config: Config = {
|
|
10
6
|
// Added as part of https://support-iqual.atlassian.net/browse/ICMS-312
|
|
11
7
|
safelist: [{ pattern: /bg-gray-\d+/ }],
|
|
12
8
|
|
|
13
9
|
plugins: [
|
|
14
|
-
|
|
15
10
|
/**
|
|
16
11
|
* Various additional variants
|
|
17
12
|
*/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single breadcrumb link, matching the `breadcrumb` GraphQL fragment in
|
|
3
|
+
* `components/Drupal/Breadcrumb/fragment.breadcrumb.graphql`.
|
|
4
|
+
*
|
|
5
|
+
* The fragment is not part of any operation in the base layer, so codegen does
|
|
6
|
+
* not emit a `BreadcrumbFragment` type for it. Layers that add a `breadcrumb`
|
|
7
|
+
* field to their route query produce the same shape.
|
|
8
|
+
*/
|
|
9
|
+
export type BreadcrumbLink = {
|
|
10
|
+
title: string
|
|
11
|
+
url?: {
|
|
12
|
+
path?: string
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The part of a route query the breadcrumb is read from. Only layers that
|
|
18
|
+
* select the field have it, hence the optional, widened shape.
|
|
19
|
+
*/
|
|
20
|
+
export type RouteQueryWithBreadcrumb = {
|
|
21
|
+
route?: { breadcrumb?: (BreadcrumbLink | null)[] } | object | null
|
|
22
|
+
}
|
package/types/global.ts
CHANGED
|
@@ -2,23 +2,24 @@ import type { RuntimeBlockOptions } from '#blokkli-build/runtime-options'
|
|
|
2
2
|
|
|
3
3
|
export type ImageFormat = 'landscape' | 'portrait' | 'auto'
|
|
4
4
|
|
|
5
|
-
export type ButtonStyle =
|
|
5
|
+
export type ButtonStyle =
|
|
6
|
+
RuntimeBlockOptions['icms_button_element']['buttonStyle']
|
|
6
7
|
export type ButtonColor =
|
|
7
|
-
| '
|
|
8
|
-
|
|
9
|
-
| '
|
|
10
|
-
| 'success'
|
|
11
|
-
| 'info'
|
|
12
|
-
| 'warning'
|
|
13
|
-
| 'neutral'
|
|
14
|
-
export type ButtonVariant = 'solid' | 'outline' | 'soft' | 'subtle' | 'ghost' | 'link'
|
|
8
|
+
'error' | 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'neutral'
|
|
9
|
+
export type ButtonVariant =
|
|
10
|
+
'solid' | 'outline' | 'soft' | 'subtle' | 'ghost' | 'link'
|
|
15
11
|
|
|
16
|
-
export type CardVariant =
|
|
17
|
-
|
|
18
|
-
export type
|
|
19
|
-
|
|
12
|
+
export type CardVariant =
|
|
13
|
+
RuntimeBlockOptions['icms_layout_cards']['cardVariant'] | undefined
|
|
14
|
+
export type CollapsibleVariant =
|
|
15
|
+
| RuntimeBlockOptions['icms_layout_collapsible']['collapsibleVariant']
|
|
16
|
+
| undefined
|
|
17
|
+
export type CallToActionVariant =
|
|
18
|
+
RuntimeBlockOptions['icms_layout_call_to_action']['callToActionVariant']
|
|
19
|
+
export type TeaserVariant =
|
|
20
|
+
RuntimeBlockOptions['icms_layout_teasers']['teaserVariant']
|
|
20
21
|
|
|
21
22
|
export type ColorScheme = 'none' | 'dark' | 'light' | 'accent' | undefined
|
|
22
23
|
export type TeaserStyle = 'none' | 'highlight'
|
|
23
24
|
export type AlignHorizontalFull = 'left' | 'center' | 'right' | undefined
|
|
24
|
-
export type AlignHorizontalReduced = 'left' | 'right' | undefined
|
|
25
|
+
export type AlignHorizontalReduced = 'left' | 'right' | undefined
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Langcode` enum of the Drupal GraphQL schema.
|
|
3
|
+
*
|
|
4
|
+
* Codegen only emits schema enums that an operation actually references, so
|
|
5
|
+
* `#graphql-operations` does not export `Langcode` unless a query declares a
|
|
6
|
+
* variable of that type. The shape below mirrors what codegen would generate,
|
|
7
|
+
* so the values stay assignable to the generated type where it does exist.
|
|
8
|
+
*/
|
|
9
|
+
export const Langcode = Object.freeze({
|
|
10
|
+
DE: 'DE',
|
|
11
|
+
EN: 'EN',
|
|
12
|
+
FR: 'FR',
|
|
13
|
+
IT: 'IT',
|
|
14
|
+
} as const)
|
|
15
|
+
|
|
16
|
+
export type Langcode = (typeof Langcode)[keyof typeof Langcode]
|