From 55fdbc527b95d158b6457f61b7295c2eb221ee68 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 5 Sep 2024 09:25:21 +0200 Subject: [PATCH] feat(SW-186): Added refs and some extra querys --- .../NavigationMenuItem/index.tsx | 6 +- components/Header/MainMenu/index.tsx | 27 +- components/Header/TopMenu/topMenu.module.css | 8 +- .../Header/InternalOrExternalLink.graphql | 22 -- .../Fragments/PageLink/HotelPageLink.graphql | 4 + .../Refs/HotelPage/Breadcrumbs.graphql | 48 ++++ .../Refs/HotelPage/HotelPage.graphql | 7 + lib/graphql/Query/Header.graphql | 69 ++++- server/routers/contentstack/base/output.ts | 203 +++++++++----- server/routers/contentstack/base/query.ts | 257 ++++++------------ server/routers/contentstack/base/utils.ts | 40 +++ 11 files changed, 388 insertions(+), 303 deletions(-) delete mode 100644 lib/graphql/Fragments/Header/InternalOrExternalLink.graphql create mode 100644 lib/graphql/Fragments/Refs/HotelPage/Breadcrumbs.graphql create mode 100644 lib/graphql/Fragments/Refs/HotelPage/HotelPage.graphql create mode 100644 server/routers/contentstack/base/utils.ts diff --git a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx index b08047c79..84a20d71f 100644 --- a/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx +++ b/components/Header/MainMenu/NavigationMenu/NavigationMenuItem/index.tsx @@ -13,16 +13,12 @@ import type { NavigationMenuItemProps } from "@/types/components/header/navigati export default function MenuItem({ item, isMobile }: NavigationMenuItemProps) { 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() { setIsExpanded((prev) => !prev) } - if (!submenu.length && !link) { - return null - } - return submenu.length ? (
- {menuItems ? ( + {menuItems?.length ? ( ) : null} {user ? ( @@ -74,7 +74,7 @@ export default async function MainMenu({ )} - {menuItems ? ( + {menuItems?.length ? ( ) : null}
@@ -82,26 +82,3 @@ export default async function MainMenu({ ) } - -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", - }, -} diff --git a/components/Header/TopMenu/topMenu.module.css b/components/Header/TopMenu/topMenu.module.css index a41a638cd..6d6c99abe 100644 --- a/components/Header/TopMenu/topMenu.module.css +++ b/components/Header/TopMenu/topMenu.module.css @@ -1,16 +1,13 @@ .topMenu { display: none; 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); } .content { max-width: var(--max-width-navigation); margin: 0 auto; - display: grid; - justify-content: space-between; - gap: var(--Spacing-x3); } .options { @@ -24,7 +21,10 @@ display: block; } .content { + display: grid; grid-template-areas: "topLink options"; + justify-content: space-between; + gap: var(--Spacing-x3); } .topLink { diff --git a/lib/graphql/Fragments/Header/InternalOrExternalLink.graphql b/lib/graphql/Fragments/Header/InternalOrExternalLink.graphql deleted file mode 100644 index 1c37ed202..000000000 --- a/lib/graphql/Fragments/Header/InternalOrExternalLink.graphql +++ /dev/null @@ -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 - } -} diff --git a/lib/graphql/Fragments/PageLink/HotelPageLink.graphql b/lib/graphql/Fragments/PageLink/HotelPageLink.graphql index 619b374d7..eda76855c 100644 --- a/lib/graphql/Fragments/PageLink/HotelPageLink.graphql +++ b/lib/graphql/Fragments/PageLink/HotelPageLink.graphql @@ -5,4 +5,8 @@ fragment HotelPageLink on HotelPage { } title url + # TODO: Might need to add this if this is needed for hotel pages. + # web { + # original_url + # } } diff --git a/lib/graphql/Fragments/Refs/HotelPage/Breadcrumbs.graphql b/lib/graphql/Fragments/Refs/HotelPage/Breadcrumbs.graphql new file mode 100644 index 000000000..1575f4eb6 --- /dev/null +++ b/lib/graphql/Fragments/Refs/HotelPage/Breadcrumbs.graphql @@ -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 + } +} diff --git a/lib/graphql/Fragments/Refs/HotelPage/HotelPage.graphql b/lib/graphql/Fragments/Refs/HotelPage/HotelPage.graphql new file mode 100644 index 000000000..d0dbdf6b3 --- /dev/null +++ b/lib/graphql/Fragments/Refs/HotelPage/HotelPage.graphql @@ -0,0 +1,7 @@ +#import "../System.graphql" + +fragment HotelPageRef on HotelPage { + system { + ...System + } +} diff --git a/lib/graphql/Query/Header.graphql b/lib/graphql/Query/Header.graphql index 32a9d686d..1e3fddd01 100644 --- a/lib/graphql/Query/Header.graphql +++ b/lib/graphql/Query/Header.graphql @@ -1,11 +1,17 @@ #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/ContentPageLink.graphql" +#import "../Fragments/PageLink/HotelPageLink.graphql" +#import "../Fragments/PageLink/LoyaltyPageLink.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!) { all_header(limit: 1, locale: $locale) { items { @@ -15,6 +21,7 @@ query GetHeader($locale: String!) { edges { node { ...ContentPageLink + ...HotelPageLink ...LoyaltyPageLink } } @@ -26,6 +33,7 @@ query GetHeader($locale: String!) { edges { node { ...ContentPageLink + ...HotelPageLink ...LoyaltyPageLink } } @@ -36,6 +44,7 @@ query GetHeader($locale: String!) { edges { node { ...ContentPageLink + ...HotelPageLink ...LoyaltyPageLink } } @@ -49,6 +58,7 @@ query GetHeader($locale: String!) { edges { node { ...ContentPageLink + ...HotelPageLink ...LoyaltyPageLink } } @@ -70,6 +80,59 @@ query GetHeader($locale: String!) { query GetHeaderRef($locale: String!) { all_header(limit: 1, locale: $locale) { 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 } diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts index 62a68238c..983a024ed 100644 --- a/server/routers/contentstack/base/output.ts +++ b/server/routers/contentstack/base/output.ts @@ -268,24 +268,26 @@ export type FooterRefDataRaw = z.infer const linkConnectionNodeSchema = z .object({ - edges: z.array( - z.object({ - node: z.object({ - system: z.object({ - uid: z.string(), - locale: z.nativeEnum(Lang), + edges: z + .array( + z.object({ + node: z.object({ + system: z.object({ + 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({ - original_url: z.string().nullable().optional(), - }), - }), - }) - ), + }) + ) + .max(1), }) - .transform((rawData) => { - const node = rawData.edges[0]?.node + .transform((data) => { + const node = data.edges[0]?.node if (!node) { return null } @@ -301,15 +303,14 @@ const linkConnectionNodeSchema = z const linkWithTitleSchema = z .object({ - title: z.string().nullable(), + title: z.string(), linkConnection: linkConnectionNodeSchema, }) .transform((rawData) => { return rawData.linkConnection && rawData.title ? { + ...rawData.linkConnection, title: rawData.title, - href: rawData.linkConnection.href, - isExternal: rawData.linkConnection.isExternal, } : null }) @@ -325,21 +326,18 @@ const cardButtonSchema = z linkConnection: linkConnectionNodeSchema, open_in_new_tab: z.boolean(), }) - .transform((rawData) => { - if (!rawData) { - return null - } - const linkConnectionData = rawData.linkConnection - const isContentstackLink = rawData.is_contentstack_link - const externalLink = rawData.external_link + .transform((data) => { + const linkConnectionData = data.linkConnection + const isContentstackLink = data.is_contentstack_link + const externalLink = data.external_link const href = isContentstackLink && externalLink.href ? externalLink.href : linkConnectionData?.href || "" return { - openInNewTab: rawData.open_in_new_tab, - title: rawData.cta_text, + openInNewTab: data.open_in_new_tab, + title: data.cta_text, href, isExternal: !isContentstackLink || linkConnectionData?.isExternal, } @@ -347,23 +345,25 @@ const cardButtonSchema = z const cardConnectionSchema = z .object({ - edges: z.array( - z.object({ - node: z.object({ - heading: z.string(), - body_text: z.string(), - background_image: imageVaultAssetTransformedSchema, - has_primary_button: z.boolean(), - has_secondary_button: z.boolean(), - scripted_top_title: z.string(), - primary_button: cardButtonSchema, - secondary_button: cardButtonSchema, - }), - }) - ), + edges: z + .array( + z.object({ + node: z.object({ + heading: z.string(), + body_text: z.string(), + background_image: imageVaultAssetTransformedSchema, + has_primary_button: z.boolean(), + has_secondary_button: z.boolean(), + scripted_top_title: z.string(), + primary_button: cardButtonSchema.nullable(), + secondary_button: cardButtonSchema.nullable(), + }), + }) + ) + .max(1), }) - .transform((rawData) => { - const node = rawData.edges[0]?.node + .transform((data) => { + const node = data.edges[0]?.node if (!node) { return null } @@ -391,31 +391,33 @@ export const menuItemSchema = z see_all_link: linkWithTitleSchema, cardConnection: cardConnectionSchema, }) - .transform( - ({ submenu, linkConnection, cardConnection, see_all_link, title }) => { - return { - title, - link: submenu.length ? null : linkConnection, - seeAllLink: submenu.length ? see_all_link : null, - submenu, - card: cardConnection, - } + .transform((data) => { + const { submenu, linkConnection, cardConnection, see_all_link, title } = + data + return { + title, + link: submenu.length ? null : linkConnection, + seeAllLink: submenu.length ? see_all_link : null, + submenu, + card: cardConnection, } - ) + }) export const getHeaderSchema = z .object({ all_header: z.object({ - items: z.array( - z.object({ - top_link: linkWithTitleSchema.nullable(), - menu_items: z.array(menuItemSchema).nullable(), - }) - ), + items: z + .array( + z.object({ + top_link: linkWithTitleSchema, + menu_items: z.array(menuItemSchema), + }) + ) + .length(1), }), }) - .transform((rawData) => { - const { top_link, menu_items } = rawData.all_header.items[0] + .transform((data) => { + const { top_link, menu_items } = data.all_header.items[0] return { topLink: top_link, @@ -423,15 +425,78 @@ export const getHeaderSchema = z } }) -export const getHeaderRefSchema = z.object({ - all_header: z.object({ - items: z.array( +const linkConnectionRefs = z.object({ + edges: z + .array( z.object({ - system: z.object({ - content_type_uid: z.string(), - uid: z.string(), + node: z.object({ + system: z.object({ + 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), }), }) diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index fa99e3667..51cfb09ba 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -14,7 +14,11 @@ import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/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 { @@ -24,11 +28,13 @@ import { CurrentHeaderRefDataRaw, FooterDataRaw, FooterRefDataRaw, + getHeaderRefSchema, getHeaderSchema, validateContactConfigSchema, validateCurrentHeaderConfigSchema, validateFooterConfigSchema, } from "./output" +import { getConnections } from "./utils" import { HeaderRefResponse, HeaderResponse } from "@/types/header" @@ -64,13 +70,13 @@ const getCurrentHeaderFailCounter = meter.createCounter( ) // OpenTelemetry metrics: Header -const getHeaderRefCounter = meter.createCounter( +const getHeaderRefsCounter = meter.createCounter( "trpc.contentstack.header.ref.get" ) -const getHeaderRefSuccessCounter = meter.createCounter( +const getHeaderRefsSuccessCounter = meter.createCounter( "trpc.contentstack.header.ref.get-success" ) -const getHeaderRefFailCounter = meter.createCounter( +const getHeaderRefsFailCounter = meter.createCounter( "trpc.contentstack.header.ref.get-fail" ) const getHeaderCounter = meter.createCounter("trpc.contentstack.header.get") @@ -164,31 +170,90 @@ export const baseQueryRouter = router({ }), header: contentstackBaseProcedure.query(async ({ ctx }) => { const { lang } = ctx - getHeaderRefCounter.add(1, { lang }) + getHeaderRefsCounter.add(1, { lang }) console.info( - "contentstack.header.ref start", + "contentstack.header.refs start", JSON.stringify({ query: { lang } }) ) - // TODO: Add better ref types and error handling for responseRef - const responseRef = await request(GetHeaderRef, { - locale: lang, - }) + const responseRef = await request( + GetHeaderRef, + { + 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( "contentstack.header start", JSON.stringify({ query: { lang } }) ) + const tags = [ + generateTags(lang, connections), + generateTag( + lang, + validatedHeaderRefs.data.all_header.items[0].system.uid + ), + ].flat() + const response = await request( GetHeader, - { locale: lang } - // { - // tags: [ - // generateTag(lang, responseRef.data.all_header.items[0].system.uid), - // ], - // } + { locale: lang }, + { cache: "force-cache", next: { tags } } ) if (!response.data) { @@ -422,161 +487,3 @@ export const baseQueryRouter = router({ 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", - }, -} diff --git a/server/routers/contentstack/base/utils.ts b/server/routers/contentstack/base/utils.ts new file mode 100644 index 000000000..dfdacfb66 --- /dev/null +++ b/server/routers/contentstack/base/utils.ts @@ -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[] = [] + 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 +}