feat(SW-186): Added refs and some extra querys

This commit is contained in:
Erik Tiekstra
2024-09-05 09:25:21 +02:00
parent 52fdc1daac
commit 55fdbc527b
11 changed files with 388 additions and 303 deletions

View File

@@ -13,16 +13,12 @@ import type { NavigationMenuItemProps } from "@/types/components/header/navigati
export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) {
const { submenu, title, link, seeAllLink, card } = item const { submenu, title, link, seeAllLink, card } = item
const [isExpanded, setIsExpanded] = useState(false) const [isExpanded, setIsExpanded] = useState(false) // TODO: Use store to manage this state when adding the menu itself.
function handleButtonClick() { function handleButtonClick() {
setIsExpanded((prev) => !prev) setIsExpanded((prev) => !prev)
} }
if (!submenu.length && !link) {
return null
}
return submenu.length ? ( return submenu.length ? (
<MainMenuButton <MainMenuButton
onClick={handleButtonClick} onClick={handleButtonClick}

View File

@@ -46,7 +46,7 @@ export default async function MainMenu({
/> />
</NextLink> </NextLink>
<div className={styles.menus}> <div className={styles.menus}>
{menuItems ? ( {menuItems?.length ? (
<NavigationMenu items={menuItems} isMobile={false} /> <NavigationMenu items={menuItems} isMobile={false} />
) : null} ) : null}
{user ? ( {user ? (
@@ -74,7 +74,7 @@ export default async function MainMenu({
</span> </span>
</Link> </Link>
)} )}
{menuItems ? ( {menuItems?.length ? (
<MobileMenu languageUrls={languageUrls} menuItems={menuItems} /> <MobileMenu languageUrls={languageUrls} menuItems={menuItems} />
) : null} ) : null}
</div> </div>
@@ -82,26 +82,3 @@ export default async function MainMenu({
</div> </div>
) )
} }
const error = {
query: { lang: "sv" },
error: {
issues: [
{
code: "invalid_type",
expected: "string",
received: "null",
path: ["all_header", "items", 0, "top_link", "title"],
message: "Expected string, received null",
},
{
code: "invalid_type",
expected: "array",
received: "null",
path: ["all_header", "items", 0, "menu_items"],
message: "Expected array, received null",
},
],
name: "ZodError",
},
}

View File

@@ -1,16 +1,13 @@
.topMenu { .topMenu {
display: none; display: none;
background-color: var(--Base-Surface-Subtle-Normal); background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x2) var(--Spacing-x-one-and-half); padding: var(--Spacing-x2);
border-bottom: 1px solid var(--Base-Border-Subtle); border-bottom: 1px solid var(--Base-Border-Subtle);
} }
.content { .content {
max-width: var(--max-width-navigation); max-width: var(--max-width-navigation);
margin: 0 auto; margin: 0 auto;
display: grid;
justify-content: space-between;
gap: var(--Spacing-x3);
} }
.options { .options {
@@ -24,7 +21,10 @@
display: block; display: block;
} }
.content { .content {
display: grid;
grid-template-areas: "topLink options"; grid-template-areas: "topLink options";
justify-content: space-between;
gap: var(--Spacing-x3);
} }
.topLink { .topLink {

View File

@@ -1,22 +0,0 @@
#import "../PageLink/ContentPageLink.graphql"
#import "../PageLink/LoyaltyPageLink.graphql"
fragment InternalOrExternalLink on InternalOrExternalLink {
is_external_link
open_in_new_tab
external_link {
href
title
}
page_link {
linkConnection {
edges {
node {
...ContentPageLink
...LoyaltyPageLink
}
}
}
link_title
}
}

View File

@@ -5,4 +5,8 @@ fragment HotelPageLink on HotelPage {
} }
title title
url url
# TODO: Might need to add this if this is needed for hotel pages.
# web {
# original_url
# }
} }

View File

@@ -0,0 +1,48 @@
#import "../System.graphql"
fragment HotelPageBreadcrumbsRefs on HotelPage {
web {
breadcrumbs {
title
parentsConnection {
edges {
node {
... on ContentPage {
web {
breadcrumbs {
title
}
}
system {
...System
}
}
... on HotelPage {
web {
breadcrumbs {
title
}
}
system {
...System
}
}
... on LoyaltyPage {
web {
breadcrumbs {
title
}
}
system {
...System
}
}
}
}
}
}
}
system {
...System
}
}

View File

@@ -0,0 +1,7 @@
#import "../System.graphql"
fragment HotelPageRef on HotelPage {
system {
...System
}
}

View File

@@ -1,11 +1,17 @@
#import "../Fragments/Refs/System.graphql" #import "../Fragments/Refs/System.graphql"
#import "../Fragments/Header/InternalOrExternalLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql" #import "../Fragments/PageLink/AccountPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/HotelPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/Blocks/Card.graphql" #import "../Fragments/Blocks/Card.graphql"
#import "../Fragments/Blocks/Refs/Card.graphql"
#import "../Fragments/Refs/ContentPage/ContentPage.graphql"
#import "../Fragments/Refs/HotelPage/HotelPage.graphql"
#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql"
#import "../Fragments/Refs/MyPages/AccountPage.graphql"
query GetHeader($locale: String!) { query GetHeader($locale: String!) {
all_header(limit: 1, locale: $locale) { all_header(limit: 1, locale: $locale) {
items { items {
@@ -15,6 +21,7 @@ query GetHeader($locale: String!) {
edges { edges {
node { node {
...ContentPageLink ...ContentPageLink
...HotelPageLink
...LoyaltyPageLink ...LoyaltyPageLink
} }
} }
@@ -26,6 +33,7 @@ query GetHeader($locale: String!) {
edges { edges {
node { node {
...ContentPageLink ...ContentPageLink
...HotelPageLink
...LoyaltyPageLink ...LoyaltyPageLink
} }
} }
@@ -36,6 +44,7 @@ query GetHeader($locale: String!) {
edges { edges {
node { node {
...ContentPageLink ...ContentPageLink
...HotelPageLink
...LoyaltyPageLink ...LoyaltyPageLink
} }
} }
@@ -49,6 +58,7 @@ query GetHeader($locale: String!) {
edges { edges {
node { node {
...ContentPageLink ...ContentPageLink
...HotelPageLink
...LoyaltyPageLink ...LoyaltyPageLink
} }
} }
@@ -70,6 +80,59 @@ query GetHeader($locale: String!) {
query GetHeaderRef($locale: String!) { query GetHeaderRef($locale: String!) {
all_header(limit: 1, locale: $locale) { all_header(limit: 1, locale: $locale) {
items { items {
top_link {
linkConnection {
edges {
node {
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
}
}
}
}
menu_items {
linkConnection {
edges {
node {
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
}
}
}
see_all_link {
linkConnection {
edges {
node {
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
}
}
}
}
submenu {
links {
linkConnection {
edges {
node {
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
}
}
}
}
}
cardConnection {
edges {
node {
...CardBlockRef
}
}
}
}
system { system {
...System ...System
} }

View File

@@ -268,24 +268,26 @@ export type FooterRefDataRaw = z.infer<typeof validateFooterRefConfigSchema>
const linkConnectionNodeSchema = z const linkConnectionNodeSchema = z
.object({ .object({
edges: z.array( edges: z
z.object({ .array(
node: z.object({ z.object({
system: z.object({ node: z.object({
uid: z.string(), system: z.object({
locale: z.nativeEnum(Lang), uid: z.string(),
locale: z.nativeEnum(Lang),
}),
url: z.string(),
title: z.string(),
web: z.object({
original_url: z.string(),
}),
}), }),
url: z.string(), })
title: z.string(), )
web: z.object({ .max(1),
original_url: z.string().nullable().optional(),
}),
}),
})
),
}) })
.transform((rawData) => { .transform((data) => {
const node = rawData.edges[0]?.node const node = data.edges[0]?.node
if (!node) { if (!node) {
return null return null
} }
@@ -301,15 +303,14 @@ const linkConnectionNodeSchema = z
const linkWithTitleSchema = z const linkWithTitleSchema = z
.object({ .object({
title: z.string().nullable(), title: z.string(),
linkConnection: linkConnectionNodeSchema, linkConnection: linkConnectionNodeSchema,
}) })
.transform((rawData) => { .transform((rawData) => {
return rawData.linkConnection && rawData.title return rawData.linkConnection && rawData.title
? { ? {
...rawData.linkConnection,
title: rawData.title, title: rawData.title,
href: rawData.linkConnection.href,
isExternal: rawData.linkConnection.isExternal,
} }
: null : null
}) })
@@ -325,21 +326,18 @@ const cardButtonSchema = z
linkConnection: linkConnectionNodeSchema, linkConnection: linkConnectionNodeSchema,
open_in_new_tab: z.boolean(), open_in_new_tab: z.boolean(),
}) })
.transform((rawData) => { .transform((data) => {
if (!rawData) { const linkConnectionData = data.linkConnection
return null const isContentstackLink = data.is_contentstack_link
} const externalLink = data.external_link
const linkConnectionData = rawData.linkConnection
const isContentstackLink = rawData.is_contentstack_link
const externalLink = rawData.external_link
const href = const href =
isContentstackLink && externalLink.href isContentstackLink && externalLink.href
? externalLink.href ? externalLink.href
: linkConnectionData?.href || "" : linkConnectionData?.href || ""
return { return {
openInNewTab: rawData.open_in_new_tab, openInNewTab: data.open_in_new_tab,
title: rawData.cta_text, title: data.cta_text,
href, href,
isExternal: !isContentstackLink || linkConnectionData?.isExternal, isExternal: !isContentstackLink || linkConnectionData?.isExternal,
} }
@@ -347,23 +345,25 @@ const cardButtonSchema = z
const cardConnectionSchema = z const cardConnectionSchema = z
.object({ .object({
edges: z.array( edges: z
z.object({ .array(
node: z.object({ z.object({
heading: z.string(), node: z.object({
body_text: z.string(), heading: z.string(),
background_image: imageVaultAssetTransformedSchema, body_text: z.string(),
has_primary_button: z.boolean(), background_image: imageVaultAssetTransformedSchema,
has_secondary_button: z.boolean(), has_primary_button: z.boolean(),
scripted_top_title: z.string(), has_secondary_button: z.boolean(),
primary_button: cardButtonSchema, scripted_top_title: z.string(),
secondary_button: cardButtonSchema, primary_button: cardButtonSchema.nullable(),
}), secondary_button: cardButtonSchema.nullable(),
}) }),
), })
)
.max(1),
}) })
.transform((rawData) => { .transform((data) => {
const node = rawData.edges[0]?.node const node = data.edges[0]?.node
if (!node) { if (!node) {
return null return null
} }
@@ -391,31 +391,33 @@ export const menuItemSchema = z
see_all_link: linkWithTitleSchema, see_all_link: linkWithTitleSchema,
cardConnection: cardConnectionSchema, cardConnection: cardConnectionSchema,
}) })
.transform( .transform((data) => {
({ submenu, linkConnection, cardConnection, see_all_link, title }) => { const { submenu, linkConnection, cardConnection, see_all_link, title } =
return { data
title, return {
link: submenu.length ? null : linkConnection, title,
seeAllLink: submenu.length ? see_all_link : null, link: submenu.length ? null : linkConnection,
submenu, seeAllLink: submenu.length ? see_all_link : null,
card: cardConnection, submenu,
} card: cardConnection,
} }
) })
export const getHeaderSchema = z export const getHeaderSchema = z
.object({ .object({
all_header: z.object({ all_header: z.object({
items: z.array( items: z
z.object({ .array(
top_link: linkWithTitleSchema.nullable(), z.object({
menu_items: z.array(menuItemSchema).nullable(), top_link: linkWithTitleSchema,
}) menu_items: z.array(menuItemSchema),
), })
)
.length(1),
}), }),
}) })
.transform((rawData) => { .transform((data) => {
const { top_link, menu_items } = rawData.all_header.items[0] const { top_link, menu_items } = data.all_header.items[0]
return { return {
topLink: top_link, topLink: top_link,
@@ -423,15 +425,78 @@ export const getHeaderSchema = z
} }
}) })
export const getHeaderRefSchema = z.object({ const linkConnectionRefs = z.object({
all_header: z.object({ edges: z
items: z.array( .array(
z.object({ z.object({
system: z.object({ node: z.object({
content_type_uid: z.string(), system: z.object({
uid: z.string(), content_type_uid: z.string(),
uid: z.string(),
}),
}), }),
}) })
), )
.max(1),
})
const cardConnectionRefs = z.object({
primary_button: z
.object({
linkConnection: linkConnectionRefs,
})
.nullable(),
secondary_button: z
.object({
linkConnection: linkConnectionRefs,
})
.nullable(),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
})
export const getHeaderRefSchema = z.object({
all_header: z.object({
items: z
.array(
z.object({
top_link: z
.object({
linkConnection: linkConnectionRefs,
})
.nullable(),
menu_items: z.array(
z.object({
linkConnection: linkConnectionRefs,
see_all_link: z.object({
linkConnection: linkConnectionRefs,
}),
cardConnection: z.object({
edges: z
.array(
z.object({
node: cardConnectionRefs,
})
)
.max(1),
}),
submenu: z.array(
z.object({
links: z.array(
z.object({ linkConnection: linkConnectionRefs })
),
})
),
})
),
system: z.object({
content_type_uid: z.string(),
uid: z.string(),
}),
})
)
.length(1),
}), }),
}) })

View File

@@ -14,7 +14,11 @@ import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc" import { notFound } from "@/server/errors/trpc"
import { contentstackBaseProcedure, router } from "@/server/trpc" import { contentstackBaseProcedure, router } from "@/server/trpc"
import { generateRefsResponseTag, generateTag } from "@/utils/generateTag" import {
generateRefsResponseTag,
generateTag,
generateTags,
} from "@/utils/generateTag"
import { langInput } from "./input" import { langInput } from "./input"
import { import {
@@ -24,11 +28,13 @@ import {
CurrentHeaderRefDataRaw, CurrentHeaderRefDataRaw,
FooterDataRaw, FooterDataRaw,
FooterRefDataRaw, FooterRefDataRaw,
getHeaderRefSchema,
getHeaderSchema, getHeaderSchema,
validateContactConfigSchema, validateContactConfigSchema,
validateCurrentHeaderConfigSchema, validateCurrentHeaderConfigSchema,
validateFooterConfigSchema, validateFooterConfigSchema,
} from "./output" } from "./output"
import { getConnections } from "./utils"
import { HeaderRefResponse, HeaderResponse } from "@/types/header" import { HeaderRefResponse, HeaderResponse } from "@/types/header"
@@ -64,13 +70,13 @@ const getCurrentHeaderFailCounter = meter.createCounter(
) )
// OpenTelemetry metrics: Header // OpenTelemetry metrics: Header
const getHeaderRefCounter = meter.createCounter( const getHeaderRefsCounter = meter.createCounter(
"trpc.contentstack.header.ref.get" "trpc.contentstack.header.ref.get"
) )
const getHeaderRefSuccessCounter = meter.createCounter( const getHeaderRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-success" "trpc.contentstack.header.ref.get-success"
) )
const getHeaderRefFailCounter = meter.createCounter( const getHeaderRefsFailCounter = meter.createCounter(
"trpc.contentstack.header.ref.get-fail" "trpc.contentstack.header.ref.get-fail"
) )
const getHeaderCounter = meter.createCounter("trpc.contentstack.header.get") const getHeaderCounter = meter.createCounter("trpc.contentstack.header.get")
@@ -164,31 +170,90 @@ export const baseQueryRouter = router({
}), }),
header: contentstackBaseProcedure.query(async ({ ctx }) => { header: contentstackBaseProcedure.query(async ({ ctx }) => {
const { lang } = ctx const { lang } = ctx
getHeaderRefCounter.add(1, { lang }) getHeaderRefsCounter.add(1, { lang })
console.info( console.info(
"contentstack.header.ref start", "contentstack.header.refs start",
JSON.stringify({ query: { lang } }) JSON.stringify({ query: { lang } })
) )
// TODO: Add better ref types and error handling for responseRef const responseRef = await request<HeaderRefResponse>(
const responseRef = await request<HeaderRefResponse>(GetHeaderRef, { GetHeaderRef,
locale: lang, {
}) locale: lang,
},
{
cache: "force-cache",
next: {
tags: [generateRefsResponseTag(lang, "header")],
},
}
)
getCurrentHeaderCounter.add(1, { lang }) if (!responseRef.data) {
const notFoundError = notFound(responseRef)
getHeaderRefsFailCounter.add(1, {
lang,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.header.refs not found error",
JSON.stringify({
query: {
lang,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedHeaderRefs = getHeaderRefSchema.safeParse(responseRef.data)
if (!validatedHeaderRefs.success) {
getHeaderRefsFailCounter.add(1, {
lang,
error_type: "validation_error",
error: JSON.stringify(validatedHeaderRefs.error),
})
console.error(
"contentstack.header.refs validation error",
JSON.stringify({
query: {
lang,
},
error: validatedHeaderRefs.error,
})
)
return null
}
getHeaderRefsSuccessCounter.add(1, { lang })
console.info(
"contentstack.header.refs success",
JSON.stringify({ query: { lang } })
)
const connections = getConnections(validatedHeaderRefs.data)
getHeaderCounter.add(1, { lang })
console.info( console.info(
"contentstack.header start", "contentstack.header start",
JSON.stringify({ query: { lang } }) JSON.stringify({ query: { lang } })
) )
const tags = [
generateTags(lang, connections),
generateTag(
lang,
validatedHeaderRefs.data.all_header.items[0].system.uid
),
].flat()
const response = await request<HeaderResponse>( const response = await request<HeaderResponse>(
GetHeader, GetHeader,
{ locale: lang } { locale: lang },
// { { cache: "force-cache", next: { tags } }
// tags: [
// generateTag(lang, responseRef.data.all_header.items[0].system.uid),
// ],
// }
) )
if (!response.data) { if (!response.data) {
@@ -422,161 +487,3 @@ export const baseQueryRouter = router({
return validatedFooterConfig.data.all_current_footer.items[0] return validatedFooterConfig.data.all_current_footer.items[0]
}), }),
}) })
const json = {
query: { lang: "en" },
error: {
issues: [
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: ["all_header", "items", 0, "top_link", "link"],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: ["all_header", "items", 0, "menu_items", 0, "link"],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: [
"all_header",
"items",
0,
"menu_items",
0,
"submenu",
0,
"links",
0,
"link",
],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: [
"all_header",
"items",
0,
"menu_items",
0,
"submenu",
0,
"links",
1,
"link",
],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: [
"all_header",
"items",
0,
"menu_items",
0,
"submenu",
1,
"links",
0,
"link",
],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: [
"all_header",
"items",
0,
"menu_items",
0,
"see_all_link",
"link",
],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: ["all_header", "items", 0, "menu_items", 1, "link"],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: [
"all_header",
"items",
0,
"menu_items",
1,
"see_all_link",
"link",
],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: ["all_header", "items", 0, "menu_items", 2, "link"],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: [
"all_header",
"items",
0,
"menu_items",
2,
"see_all_link",
"link",
],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: ["all_header", "items", 0, "menu_items", 3, "link"],
message: "Required",
},
{
code: "invalid_type",
expected: "object",
received: "undefined",
path: [
"all_header",
"items",
0,
"menu_items",
3,
"see_all_link",
"link",
],
message: "Required",
},
],
name: "ZodError",
},
}

View File

@@ -0,0 +1,40 @@
import { HeaderRefResponse } from "@/types/header"
import { Edges } from "@/types/requests/utils/edges"
import { NodeRefs } from "@/types/requests/utils/refs"
export function getConnections(refs: HeaderRefResponse) {
const connections: Edges<NodeRefs>[] = []
const headerData = refs.all_header.items[0]
const topLink = headerData.top_link
if (topLink) {
connections.push(topLink.linkConnection)
}
headerData.menu_items.forEach(
({ linkConnection, see_all_link, cardConnection, submenu }) => {
const card = cardConnection.edges[0]?.node
connections.push(linkConnection)
if (see_all_link) {
connections.push(see_all_link.linkConnection)
}
if (card) {
if (card.primary_button) {
connections.push(card.primary_button.linkConnection)
}
if (card.secondary_button) {
connections.push(card.secondary_button.linkConnection)
}
}
submenu.forEach(({ links }) => {
links.forEach(({ linkConnection }) => {
connections.push(linkConnection)
})
})
}
)
return connections
}