From fd48f86c9015cbf954097438a8a7aa5e4c5d88af Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Thu, 28 Aug 2025 07:25:17 +0000 Subject: [PATCH] feat/SW-3108 external links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(SW-3108): Added external link options to shortcuts * feat(SW-3108): Added external link options to header Approved-by: Matilda Landström --- .../ShortcutsListItems/index.tsx | 2 +- .../NavigationMenu/MegaMenu/index.tsx | 10 +- .../NavigationMenuItem/index.tsx | 96 ++++++++++--------- .../components/Header/TopLink/index.tsx | 4 +- .../Fragments/Blocks/Shortcuts.graphql | 5 + .../trpc/lib/graphql/Query/Header.graphql | 25 +++++ .../lib/routers/contentstack/base/output.ts | 83 ++++++++++++---- .../contentstack/schemas/blocks/shortcuts.ts | 86 +++++++++++------ 8 files changed, 213 insertions(+), 98 deletions(-) diff --git a/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/index.tsx b/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/index.tsx index b75effd20..b97c99fb1 100644 --- a/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/index.tsx +++ b/apps/scandic-web/components/Blocks/ShortcutsList/ShortcutsListItems/index.tsx @@ -24,7 +24,7 @@ export default function ShortcutsListItems({ className={styles.link} > - {shortcut.text || shortcut.title} + {shortcut.text} diff --git a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx index b9df8d7fa..fabcb3671 100644 --- a/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/NavigationMenu/MegaMenu/index.tsx @@ -56,9 +56,9 @@ export default function MegaMenu({ ) : null}
- {seeAllLink?.link ? ( + {seeAllLink?.url ? ( {item.title}
    - {item.links.map(({ title, link }) => - link ? ( + {item.links.map(({ title, url }) => + url ? (
  • - toggleMegaMenu(megaMenuTitle)} + if (submenu.length) { + return ( + <> + toggleMegaMenu(megaMenuTitle)} + className={`${styles.navigationMenuItem} ${isMobile ? styles.mobile : ""}`} + > + {title} + {isMobile ? ( + + ) : ( + + )} + +
    + +
    + + ) + } else if (link?.url) { + return ( + {title} - {isMobile ? ( - - ) : ( - - )} -
    -
    - -
    - - ) : ( - - {title} - - ) + + ) + } + + return null } diff --git a/apps/scandic-web/components/Header/TopLink/index.tsx b/apps/scandic-web/components/Header/TopLink/index.tsx index 5d85fb48d..7013b41bb 100644 --- a/apps/scandic-web/components/Header/TopLink/index.tsx +++ b/apps/scandic-web/components/Header/TopLink/index.tsx @@ -11,13 +11,13 @@ export default function TopLink({ }: TopLinkProps) { const linkData = isLoggedIn ? topLink.logged_in : topLink.logged_out - if (!linkData?.link?.url || !linkData?.title) { + if (!linkData?.url || !linkData?.title) { return null } return ( diff --git a/packages/trpc/lib/graphql/Fragments/Blocks/Shortcuts.graphql b/packages/trpc/lib/graphql/Fragments/Blocks/Shortcuts.graphql index 8edd5453e..a9610f07d 100644 --- a/packages/trpc/lib/graphql/Fragments/Blocks/Shortcuts.graphql +++ b/packages/trpc/lib/graphql/Fragments/Blocks/Shortcuts.graphql @@ -27,6 +27,11 @@ fragment Shortcuts on Shortcuts { title two_column_list shortcuts { + is_contentstack_link + external_link { + href + title + } open_in_new_tab text linkConnection { diff --git a/packages/trpc/lib/graphql/Query/Header.graphql b/packages/trpc/lib/graphql/Query/Header.graphql index 326867002..f7c72c27d 100644 --- a/packages/trpc/lib/graphql/Query/Header.graphql +++ b/packages/trpc/lib/graphql/Query/Header.graphql @@ -33,6 +33,7 @@ query GetHeader($locale: String!) { top_link { logged_in { icon + is_contentstack_link title linkConnection { edges { @@ -52,9 +53,14 @@ query GetHeader($locale: String!) { } } } + external_link { + href + title + } } logged_out { icon + is_contentstack_link title linkConnection { edges { @@ -74,9 +80,14 @@ query GetHeader($locale: String!) { } } } + external_link { + href + title + } } } menu_items { + is_contentstack_link title linkConnection { edges { @@ -96,7 +107,12 @@ query GetHeader($locale: String!) { } } } + external_link { + href + title + } see_all_link { + is_contentstack_link title linkConnection { edges { @@ -116,10 +132,15 @@ query GetHeader($locale: String!) { } } } + external_link { + href + title + } } submenu { title links { + is_contentstack_link title linkConnection { edges { @@ -139,6 +160,10 @@ query GetHeader($locale: String!) { } } } + external_link { + href + title + } } } cardConnection { diff --git a/packages/trpc/lib/routers/contentstack/base/output.ts b/packages/trpc/lib/routers/contentstack/base/output.ts index 56923da24..03d08db1c 100644 --- a/packages/trpc/lib/routers/contentstack/base/output.ts +++ b/packages/trpc/lib/routers/contentstack/base/output.ts @@ -7,6 +7,10 @@ import { import { Lang } from "@scandic-hotels/common/constants/language" import { logger } from "@scandic-hotels/common/logger" import { removeMultipleSlashes } from "@scandic-hotels/common/utils/url" +import { + nullableStringUrlValidator, + nullableStringValidator, +} from "@scandic-hotels/common/utils/zod/stringValidator" import { discriminatedUnion } from "../../../utils/discriminatedUnion" import { @@ -97,7 +101,7 @@ export const validateCurrentHeaderConfigSchema = z edges: z.array( z.object({ node: z.object({ - description: z.string().optional().nullable(), + description: z.string().nullish(), dimension: z.object({ height: z.number(), width: z.number(), @@ -106,8 +110,8 @@ export const validateCurrentHeaderConfigSchema = z system: z.object({ uid: z.string(), }), - title: z.string().optional().default(""), - url: z.string().optional().default(""), + title: nullableStringValidator, + url: nullableStringUrlValidator, }), }) ), @@ -178,7 +182,7 @@ const validateAppDownload = z.object({ edges: z.array( z.object({ node: z.object({ - description: z.string().optional().nullable(), + description: z.string().nullish(), dimension: z.object({ height: z.number(), width: z.number(), @@ -220,7 +224,7 @@ export const validateCurrentFooterConfigSchema = z.object({ edges: z.array( z.object({ node: z.object({ - description: z.string().optional().nullable(), + description: z.string().nullish(), dimension: z.object({ height: z.number(), width: z.number(), @@ -248,7 +252,7 @@ export const validateCurrentFooterConfigSchema = z.object({ edges: z.array( z.object({ node: z.object({ - description: z.string().optional().nullable(), + description: z.string().nullish(), dimension: z.object({ height: z.number(), width: z.number(), @@ -561,6 +565,51 @@ export const headerRefsSchema = z } }) +const internalOrExternalLinkSchema = z + .object({ + is_contentstack_link: z.boolean().nullish(), + external_link: z + .object({ + href: nullableStringUrlValidator, + title: z.string().nullish(), + }) + .nullish(), + title: nullableStringValidator, + linkConnection: z.object({ + edges: z.array( + z.object({ + node: linkUnionSchema.transform((data) => { + const link = transformPageLink(data) + if (link) { + return link + } + return data + }), + }) + ), + }), + }) + .transform( + ({ is_contentstack_link, external_link, linkConnection, title }) => { + if (is_contentstack_link !== false && linkConnection.edges.length) { + const linkRef = linkConnection.edges[0].node + return { + title: title || linkRef.title, + url: linkRef.url, + } + } else if (is_contentstack_link === false && external_link?.href) { + return { + title: title || external_link.title || "", + url: external_link.href, + } + } else { + return { + title, + } + } + } + ) + const linkSchema = z .object({ linkConnection: z.object({ @@ -590,7 +639,7 @@ const linkSchema = z }) const titleSchema = z.object({ - title: z.string().optional().default(""), + title: nullableStringValidator, }) /** @@ -605,7 +654,7 @@ const linkAndTitleSchema = z.intersection(linkSchema, titleSchema) */ export const menuItemSchema = z .intersection( - linkAndTitleSchema, + internalOrExternalLinkSchema, z .object({ cardConnection: z.object({ @@ -615,11 +664,11 @@ export const menuItemSchema = z }) ), }), - see_all_link: linkAndTitleSchema, + see_all_link: internalOrExternalLinkSchema, submenu: z.array( z.object({ - links: z.array(linkAndTitleSchema), - title: z.string().optional().default(""), + links: z.array(internalOrExternalLinkSchema), + title: nullableStringValidator, }) ), }) @@ -636,11 +685,13 @@ export const menuItemSchema = z } }) ) - .transform((data) => { + .transform(({ title, url, card, seeAllLink, submenu }) => { return { - ...data, - link: data.submenu.length ? null : data.link, - seeAllLink: data.submenu.length ? data.seeAllLink : null, + title, + link: submenu.length ? null : { url }, + seeAllLink: submenu.length ? seeAllLink : null, + card, + submenu, } }) @@ -652,7 +703,7 @@ enum IconName { } const topLinkItemSchema = z.intersection( - linkAndTitleSchema, + internalOrExternalLinkSchema, z.object({ icon: z .enum(["loyalty", "info", "offer"]) diff --git a/packages/trpc/lib/routers/contentstack/schemas/blocks/shortcuts.ts b/packages/trpc/lib/routers/contentstack/schemas/blocks/shortcuts.ts index db0797688..a9d6c321a 100644 --- a/packages/trpc/lib/routers/contentstack/schemas/blocks/shortcuts.ts +++ b/packages/trpc/lib/routers/contentstack/schemas/blocks/shortcuts.ts @@ -15,37 +15,65 @@ export const shortcutsBlockSchema = z.object({ two_column_list: z.boolean().nullable().default(false), shortcuts: z .array( - z.object({ - open_in_new_tab: z.boolean(), - text: z.string().optional().default(""), - linkConnection: z.object({ - edges: z.array( - z.object({ - node: linkUnionSchema.transform((data) => { - const link = transformPageLink(data) - if (link) { - return link - } - return data - }), + z + .object({ + is_contentstack_link: z.boolean().nullish(), + external_link: z + .object({ + href: z.string().nullish().default(""), + title: z.string().nullish(), }) - ), - }), - }) - ) - .transform((data) => { - return data - .filter((node) => node.linkConnection.edges.length) - .map((node) => { - const link = node.linkConnection.edges[0].node - return { - openInNewTab: node.open_in_new_tab, - text: node.text, - title: link.title, - url: link.url, - } + .nullish(), + open_in_new_tab: z.boolean(), + text: z.string().optional().default(""), + linkConnection: z.object({ + edges: z.array( + z.object({ + node: linkUnionSchema.transform((data) => { + const link = transformPageLink(data) + if (link) { + return link + } + return data + }), + }) + ), + }), }) - }), + .transform( + ({ + is_contentstack_link, + external_link, + linkConnection, + open_in_new_tab, + text, + }) => { + if ( + is_contentstack_link !== false && + linkConnection.edges.length + ) { + const linkRef = linkConnection.edges[0].node + return { + openInNewTab: open_in_new_tab, + text: text || linkRef.title, + url: linkRef.url, + } + } else if ( + is_contentstack_link === false && + external_link?.href + ) { + return { + openInNewTab: open_in_new_tab, + text: text || external_link.title || "", + url: external_link.href, + } + } else { + return null + } + } + ) + ) + .transform((data) => data.filter((item) => !!item)), }) .transform(({ two_column_list, ...rest }) => { return {