From ffb273ca62323c1b265a5db39797f83d158c3157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:12:55 +0000 Subject: [PATCH 1/2] feat(web): Organization parent subpage (#17022) * First draft * Stop fetching organization page twice * Handle if there is only a single child link * Update apps/web/screens/Organization/OrganizationNews/OrganizationNewsArticle.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add errorHandler to backend endpoint * Remove question mark * Filter out invalid child links --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/pages/s/[...slugs]/index.tsx | 99 ++++--- .../OrganizationSubPageGenericListItem.tsx | 12 +- .../OrganizationEventArticle.tsx | 43 +-- .../OrganizationNewsArticle.tsx | 47 ++-- .../Organization/Standalone/ParentSubpage.tsx | 251 ++++++++++++++++++ apps/web/screens/Organization/SubPage.tsx | 48 +++- apps/web/screens/queries/Organization.tsx | 14 + libs/cms/src/lib/cms.contentful.service.ts | 26 +- libs/cms/src/lib/cms.resolver.ts | 10 + .../dto/getOrganizationParentSubpage.input.ts | 18 ++ .../models/organizationParentSubpage.model.ts | 48 ++++ 11 files changed, 526 insertions(+), 90 deletions(-) create mode 100644 apps/web/screens/Organization/Standalone/ParentSubpage.tsx create mode 100644 libs/cms/src/lib/dto/getOrganizationParentSubpage.input.ts create mode 100644 libs/cms/src/lib/models/organizationParentSubpage.model.ts diff --git a/apps/web/pages/s/[...slugs]/index.tsx b/apps/web/pages/s/[...slugs]/index.tsx index 8c2cbde982af..5632dbe156ba 100644 --- a/apps/web/pages/s/[...slugs]/index.tsx +++ b/apps/web/pages/s/[...slugs]/index.tsx @@ -31,6 +31,9 @@ import PublishedMaterial, { import StandaloneHome, { type StandaloneHomeProps, } from '@island.is/web/screens/Organization/Standalone/Home' +import StandaloneParentSubpage, { + StandaloneParentSubpageProps, +} from '@island.is/web/screens/Organization/Standalone/ParentSubpage' import SubPage, { type SubPageProps, } from '@island.is/web/screens/Organization/SubPage' @@ -42,6 +45,7 @@ import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePro enum PageType { FRONTPAGE = 'frontpage', STANDALONE_FRONTPAGE = 'standalone-frontpage', + STANDALONE_PARENT_SUBPAGE = 'standalone-parent-subpage', SUBPAGE = 'subpage', ALL_NEWS = 'news', PUBLISHED_MATERIAL = 'published-material', @@ -55,6 +59,9 @@ enum PageType { const pageMap: Record> = { [PageType.FRONTPAGE]: (props) => , [PageType.STANDALONE_FRONTPAGE]: (props) => , + [PageType.STANDALONE_PARENT_SUBPAGE]: (props) => ( + + ), [PageType.SUBPAGE]: (props) => , [PageType.ALL_NEWS]: (props) => , [PageType.PUBLISHED_MATERIAL]: (props) => , @@ -79,6 +86,10 @@ interface Props { type: PageType.STANDALONE_FRONTPAGE props: StandaloneHomeProps } + | { + type: PageType.STANDALONE_PARENT_SUBPAGE + props: StandaloneParentSubpageProps + } | { type: PageType.SUBPAGE props: { @@ -135,32 +146,32 @@ Component.getProps = async (context) => { const slugs = context.query.slugs as string[] const locale = context.locale || 'is' - // Frontpage - if (slugs.length === 1) { - const { - data: { getOrganizationPage: organizationPage }, - } = await context.apolloClient.query({ - query: GET_ORGANIZATION_PAGE_QUERY, - variables: { - input: { - slug: slugs[0], - lang: locale, - }, + const { + data: { getOrganizationPage: organizationPage }, + } = await context.apolloClient.query({ + query: GET_ORGANIZATION_PAGE_QUERY, + variables: { + input: { + slug: slugs[0], + lang: locale, }, - }) + }, + }) - if (!organizationPage) { - throw new CustomNextError(404) - } + if (!organizationPage) { + throw new CustomNextError(404, 'Organization page was not found') + } + + const modifiedContext = { ...context, organizationPage } + + const STANDALONE_THEME = 'standalone' - if (organizationPage.theme === 'standalone') { + if (slugs.length === 1) { + if (organizationPage.theme === STANDALONE_THEME) { return { page: { type: PageType.STANDALONE_FRONTPAGE, - props: await StandaloneHome.getProps({ - ...context, - organizationPage, - }), + props: await StandaloneHome.getProps(modifiedContext), }, } } @@ -168,7 +179,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.FRONTPAGE, - props: await Home.getProps({ ...context, organizationPage }), + props: await Home.getProps(modifiedContext), }, } } @@ -179,7 +190,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.ALL_NEWS, - props: await OrganizationNewsList.getProps(context), + props: await OrganizationNewsList.getProps(modifiedContext), }, } } @@ -187,7 +198,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.ALL_EVENTS, - props: await OrganizationEventList.getProps(context), + props: await OrganizationEventList.getProps(modifiedContext), }, } } @@ -195,7 +206,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.PUBLISHED_MATERIAL, - props: await PublishedMaterial.getProps(context), + props: await PublishedMaterial.getProps(modifiedContext), }, } } @@ -204,7 +215,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.ALL_NEWS, - props: await OrganizationNewsList.getProps(context), + props: await OrganizationNewsList.getProps(modifiedContext), }, } } @@ -212,7 +223,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.ALL_EVENTS, - props: await OrganizationEventList.getProps(context), + props: await OrganizationEventList.getProps(modifiedContext), }, } } @@ -220,18 +231,25 @@ Component.getProps = async (context) => { return { page: { type: PageType.PUBLISHED_MATERIAL, - props: await PublishedMaterial.getProps(context), + props: await PublishedMaterial.getProps(modifiedContext), }, } } } - // Subpage - const props = await SubPage.getProps(context) + if (organizationPage.theme === STANDALONE_THEME) { + return { + page: { + type: PageType.STANDALONE_PARENT_SUBPAGE, + props: await StandaloneParentSubpage.getProps(modifiedContext), + }, + } + } + return { page: { type: PageType.SUBPAGE, - props, + props: await SubPage.getProps(modifiedContext), }, } } @@ -242,7 +260,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.NEWS_DETAILS, - props: await OrganizationNewsArticle.getProps(context), + props: await OrganizationNewsArticle.getProps(modifiedContext), }, } } @@ -250,7 +268,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.EVENT_DETAILS, - props: await OrganizationEventArticle.getProps(context), + props: await OrganizationEventArticle.getProps(modifiedContext), }, } } @@ -259,7 +277,7 @@ Component.getProps = async (context) => { return { page: { type: PageType.NEWS_DETAILS, - props: await OrganizationNewsArticle.getProps(context), + props: await OrganizationNewsArticle.getProps(modifiedContext), }, } } @@ -267,16 +285,27 @@ Component.getProps = async (context) => { return { page: { type: PageType.EVENT_DETAILS, - props: await OrganizationEventArticle.getProps(context), + props: await OrganizationEventArticle.getProps(modifiedContext), }, } } } + if (organizationPage.theme === STANDALONE_THEME) { + return { + page: { + type: PageType.STANDALONE_PARENT_SUBPAGE, + props: await StandaloneParentSubpage.getProps(modifiedContext), + }, + } + } + return { page: { type: PageType.GENERIC_LIST_ITEM, - props: await OrganizationSubPageGenericListItem.getProps(context), + props: await OrganizationSubPageGenericListItem.getProps( + modifiedContext, + ), }, } } diff --git a/apps/web/screens/GenericList/OrganizationSubPageGenericListItem.tsx b/apps/web/screens/GenericList/OrganizationSubPageGenericListItem.tsx index 7bba1474eef6..062fca30e94b 100644 --- a/apps/web/screens/GenericList/OrganizationSubPageGenericListItem.tsx +++ b/apps/web/screens/GenericList/OrganizationSubPageGenericListItem.tsx @@ -1,10 +1,11 @@ import { useMemo } from 'react' import { useRouter } from 'next/router' +import { Query } from '@island.is/web/graphql/schema' import { useLinkResolver } from '@island.is/web/hooks' import { useI18n } from '@island.is/web/i18n' -import { type LayoutProps, withMainLayout } from '@island.is/web/layouts/main' -import { Screen } from '@island.is/web/types' +import { type LayoutProps } from '@island.is/web/layouts/main' +import { Screen, ScreenContext } from '@island.is/web/types' import SubPage, { type SubPageProps } from '../Organization/SubPage' import GenericListItemPage, { @@ -19,8 +20,13 @@ export interface OrganizationSubPageGenericListItemProps { genericListItemProps: GenericListItemPageProps } +type OrganizationSubPageGenericListItemScreenContext = ScreenContext & { + organizationPage?: Query['getOrganizationPage'] +} + const OrganizationSubPageGenericListItem: Screen< - OrganizationSubPageGenericListItemProps + OrganizationSubPageGenericListItemProps, + OrganizationSubPageGenericListItemScreenContext > = (props) => { const { organizationPage, subpage } = props.parentProps.componentProps const router = useRouter() diff --git a/apps/web/screens/Organization/OrganizationEvents/OrganizationEventArticle.tsx b/apps/web/screens/Organization/OrganizationEvents/OrganizationEventArticle.tsx index 760d40330a9e..c8295301cdfa 100644 --- a/apps/web/screens/Organization/OrganizationEvents/OrganizationEventArticle.tsx +++ b/apps/web/screens/Organization/OrganizationEvents/OrganizationEventArticle.tsx @@ -41,7 +41,7 @@ import { useWindowSize } from '@island.is/web/hooks/useViewport' import { useI18n } from '@island.is/web/i18n' import { useDateUtils } from '@island.is/web/i18n/useDateUtils' import { withMainLayout } from '@island.is/web/layouts/main' -import type { Screen } from '@island.is/web/types' +import type { Screen, ScreenContext } from '@island.is/web/types' import { CustomNextError } from '@island.is/web/units/errors' import { extractNamespaceFromOrganization } from '@island.is/web/utils/extractNamespaceFromOrganization' import { getOrganizationSidebarNavigationItems } from '@island.is/web/utils/organization' @@ -141,12 +141,14 @@ export interface OrganizationEventArticleProps { locale: Locale } -const OrganizationEventArticle: Screen = ({ - organizationPage, - event, - namespace, - locale, -}) => { +type OrganizationEventArticleScreenContext = ScreenContext & { + organizationPage?: Query['getOrganizationPage'] +} + +const OrganizationEventArticle: Screen< + OrganizationEventArticleProps, + OrganizationEventArticleScreenContext +> = ({ organizationPage, event, namespace, locale }) => { const n = useNamespace(namespace) const router = useRouter() @@ -285,19 +287,26 @@ const OrganizationEventArticle: Screen = ({ ) } -OrganizationEventArticle.getProps = async ({ apolloClient, query, locale }) => { +OrganizationEventArticle.getProps = async ({ + apolloClient, + query, + locale, + organizationPage: _organizationPage, +}) => { const [organizationPageSlug, _, eventSlug] = query.slugs as string[] const [organizationPageResponse, eventResponse, namespace] = await Promise.all([ - apolloClient.query({ - query: GET_ORGANIZATION_PAGE_QUERY, - variables: { - input: { - slug: organizationPageSlug, - lang: locale as Locale, - }, - }, - }), + !_organizationPage + ? apolloClient.query({ + query: GET_ORGANIZATION_PAGE_QUERY, + variables: { + input: { + slug: organizationPageSlug, + lang: locale as Locale, + }, + }, + }) + : { data: { getOrganizationPage: _organizationPage } }, apolloClient.query({ query: GET_SINGLE_EVENT_QUERY, variables: { diff --git a/apps/web/screens/Organization/OrganizationNews/OrganizationNewsArticle.tsx b/apps/web/screens/Organization/OrganizationNews/OrganizationNewsArticle.tsx index d5da623eb539..24bf3f060b60 100644 --- a/apps/web/screens/Organization/OrganizationNews/OrganizationNewsArticle.tsx +++ b/apps/web/screens/Organization/OrganizationNews/OrganizationNewsArticle.tsx @@ -28,7 +28,7 @@ import { GET_ORGANIZATION_PAGE_QUERY, GET_SINGLE_NEWS_ITEM_QUERY, } from '@island.is/web/screens/queries' -import { Screen } from '@island.is/web/types' +import type { Screen, ScreenContext } from '@island.is/web/types' import { CustomNextError } from '@island.is/web/units/errors' import { extractNamespaceFromOrganization } from '@island.is/web/utils/extractNamespaceFromOrganization' @@ -39,12 +39,14 @@ export interface OrganizationNewsArticleProps { locale: Locale } -const OrganizationNewsArticle: Screen = ({ - newsItem, - namespace, - organizationPage, - locale, -}) => { +type OrganizationNewsArticleScreenContext = ScreenContext & { + organizationPage?: Query['getOrganizationPage'] +} + +const OrganizationNewsArticle: Screen< + OrganizationNewsArticleProps, + OrganizationNewsArticleScreenContext +> = ({ newsItem, namespace, organizationPage, locale }) => { const router = useRouter() const { linkResolver } = useLinkResolver() // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -163,22 +165,27 @@ const OrganizationNewsArticle: Screen = ({ ) } -OrganizationNewsArticle.getProps = async ({ apolloClient, locale, query }) => { +OrganizationNewsArticle.getProps = async ({ + apolloClient, + locale, + query, + organizationPage: _organizationPage, +}) => { const [organizationPageSlug, _, newsSlug] = query.slugs as string[] - const organizationPage = ( - await Promise.resolve( - apolloClient.query({ - query: GET_ORGANIZATION_PAGE_QUERY, - variables: { - input: { - slug: organizationPageSlug, - lang: locale as Locale, + const organizationPage = !_organizationPage + ? ( + await apolloClient.query({ + query: GET_ORGANIZATION_PAGE_QUERY, + variables: { + input: { + slug: organizationPageSlug, + lang: locale as Locale, + }, }, - }, - }), - ) - ).data?.getOrganizationPage + }) + ).data?.getOrganizationPage + : _organizationPage if (!organizationPage) { throw new CustomNextError( diff --git a/apps/web/screens/Organization/Standalone/ParentSubpage.tsx b/apps/web/screens/Organization/Standalone/ParentSubpage.tsx new file mode 100644 index 000000000000..ad4e08f629c9 --- /dev/null +++ b/apps/web/screens/Organization/Standalone/ParentSubpage.tsx @@ -0,0 +1,251 @@ +import { useRouter } from 'next/router' + +import { + Breadcrumbs, + GridColumn, + GridContainer, + GridRow, + Stack, + TableOfContents, + Text, +} from '@island.is/island-ui/core' +import { + ContentLanguage, + OrganizationPage, + OrganizationParentSubpage, + OrganizationSubpage, + Query, + QueryGetNamespaceArgs, + QueryGetOrganizationPageArgs, + QueryGetOrganizationParentSubpageArgs, + QueryGetOrganizationSubpageArgs, +} from '@island.is/web/graphql/schema' +import { useLinkResolver } from '@island.is/web/hooks' +import { useI18n } from '@island.is/web/i18n' +import { StandaloneLayout } from '@island.is/web/layouts/organization/standalone' +import type { Screen, ScreenContext } from '@island.is/web/types' +import { CustomNextError } from '@island.is/web/units/errors' + +import { + GET_NAMESPACE_QUERY, + GET_ORGANIZATION_PAGE_QUERY, + GET_ORGANIZATION_PARENT_SUBPAGE_QUERY, + GET_ORGANIZATION_SUBPAGE_QUERY, +} from '../../queries' +import { SubPageContent } from '../SubPage' + +type StandaloneParentSubpageScreenContext = ScreenContext & { + organizationPage?: Query['getOrganizationPage'] +} + +export interface StandaloneParentSubpageProps { + organizationPage: OrganizationPage + subpage: OrganizationSubpage + tableOfContentHeadings: { + headingId: string + headingTitle: string + label: string + href: string + }[] + selectedHeadingId: string + parentSubpage: OrganizationParentSubpage + namespace: Record +} + +const StandaloneParentSubpage: Screen< + StandaloneParentSubpageProps, + StandaloneParentSubpageScreenContext +> = ({ + organizationPage, + parentSubpage, + selectedHeadingId, + subpage, + tableOfContentHeadings, + namespace, +}) => { + const router = useRouter() + const { activeLocale } = useI18n() + const { linkResolver } = useLinkResolver() + + return ( + + + + + + + {parentSubpage.childLinks.length > 1 && ( + + + {parentSubpage.title} + + { + const href = tableOfContentHeadings.find( + (heading) => heading.headingId === headingId, + )?.href + if (href) { + router.push(href) + } + }} + tableOfContentsTitle={ + namespace?.['standaloneTableOfContentsTitle'] ?? + activeLocale === 'is' + ? 'Efnisyfirlit' + : 'Table of contents' + } + selectedHeadingId={selectedHeadingId} + /> + + )} + + + + 1 ? 'h2' : 'h1' + } + /> + + + ) +} + +StandaloneParentSubpage.getProps = async ({ + apolloClient, + locale, + query, + organizationPage, +}) => { + const [organizationPageSlug, parentSubpageSlug, subpageSlug] = (query.slugs ?? + []) as string[] + + const [ + { + data: { getOrganizationPage }, + }, + { + data: { getOrganizationParentSubpage }, + }, + namespace, + ] = await Promise.all([ + !organizationPage + ? apolloClient.query({ + query: GET_ORGANIZATION_PAGE_QUERY, + variables: { + input: { + slug: organizationPageSlug, + lang: locale as ContentLanguage, + }, + }, + }) + : { + data: { getOrganizationPage: organizationPage }, + }, + apolloClient.query({ + query: GET_ORGANIZATION_PARENT_SUBPAGE_QUERY, + variables: { + input: { + organizationPageSlug: organizationPageSlug as string, + slug: parentSubpageSlug as string, + lang: locale as ContentLanguage, + }, + }, + }), + apolloClient + .query({ + query: GET_NAMESPACE_QUERY, + variables: { + input: { + namespace: 'OrganizationPages', + lang: locale, + }, + }, + }) + .then((variables) => + variables.data.getNamespace?.fields + ? JSON.parse(variables.data.getNamespace.fields) + : {}, + ), + ]) + + if (!getOrganizationPage) { + throw new CustomNextError(404, 'Organization page not found') + } + + if (!getOrganizationParentSubpage) { + throw new CustomNextError(404, 'Organization parent subpage was not found') + } + + let selectedIndex = 0 + + if (subpageSlug) { + const index = getOrganizationParentSubpage.childLinks.findIndex( + (link) => link.href.split('/').pop() === subpageSlug, + ) + if (index >= 0) { + selectedIndex = index + } else { + throw new CustomNextError( + 404, + 'Subpage belonging to an organization parent subpage was not found', + ) + } + } + + const subpage = ( + await apolloClient.query({ + query: GET_ORGANIZATION_SUBPAGE_QUERY, + variables: { + input: { + organizationSlug: organizationPageSlug, + slug: getOrganizationParentSubpage.childLinks[selectedIndex].href + .split('/') + .pop() as string, + lang: locale as ContentLanguage, + }, + }, + }) + ).data.getOrganizationSubpage + + if (!subpage) { + throw new CustomNextError( + 404, + 'Subpage belonging to an organization parent subpage was not found', + ) + } + + const tableOfContentHeadings = getOrganizationParentSubpage.childLinks.map( + (link) => ({ + headingId: link.href, + headingTitle: link.label, + label: link.label, + href: link.href, + }), + ) + const selectedHeadingId = tableOfContentHeadings[selectedIndex].headingId + + return { + organizationPage: getOrganizationPage, + parentSubpage: getOrganizationParentSubpage, + subpage, + tableOfContentHeadings, + selectedHeadingId, + namespace, + } +} + +export default StandaloneParentSubpage diff --git a/apps/web/screens/Organization/SubPage.tsx b/apps/web/screens/Organization/SubPage.tsx index 6d1dc7697cb3..6d81780073b2 100644 --- a/apps/web/screens/Organization/SubPage.tsx +++ b/apps/web/screens/Organization/SubPage.tsx @@ -43,7 +43,7 @@ import { extractNamespaceFromOrganization } from '@island.is/web/utils/extractNa import { webRichText } from '@island.is/web/utils/richText' import { safelyExtractPathnameFromUrl } from '@island.is/web/utils/safelyExtractPathnameFromUrl' -import { Screen } from '../../types' +import { Screen, ScreenContext } from '../../types' import { GET_NAMESPACE_QUERY, GET_ORGANIZATION_PAGE_QUERY, @@ -61,11 +61,14 @@ export interface SubPageProps { customContentfulIds?: (string | undefined)[] } -const SubPageContent = ({ +export const SubPageContent = ({ subpage, namespace, organizationPage, -}: Pick) => { + subpageTitleVariant = 'h1', +}: Pick & { + subpageTitleVariant?: 'h1' | 'h2' +}) => { const n = useNamespace(namespace) const { activeLocale } = useI18n() const content = ( @@ -132,7 +135,10 @@ const SubPageContent = ({ > <> - + {subpage?.title} @@ -208,7 +214,11 @@ const SubPageContent = ({ ) } -const SubPage: Screen = ({ +type SubPageScreenContext = ScreenContext & { + organizationPage?: Query['getOrganizationPage'] +} + +const SubPage: Screen = ({ organizationPage, subpage, namespace, @@ -357,7 +367,13 @@ const renderSlices = ( } } -SubPage.getProps = async ({ apolloClient, locale, query, req }) => { +SubPage.getProps = async ({ + apolloClient, + locale, + query, + req, + organizationPage, +}) => { const pathname = safelyExtractPathnameFromUrl(req.url) const { slug, subSlug } = getSlugAndSubSlug(query, pathname) @@ -370,15 +386,19 @@ SubPage.getProps = async ({ apolloClient, locale, query, req }) => { }, namespace, ] = await Promise.all([ - apolloClient.query({ - query: GET_ORGANIZATION_PAGE_QUERY, - variables: { - input: { - slug: slug as string, - lang: locale as ContentLanguage, + !organizationPage + ? apolloClient.query({ + query: GET_ORGANIZATION_PAGE_QUERY, + variables: { + input: { + slug: slug as string, + lang: locale as ContentLanguage, + }, + }, + }) + : { + data: { getOrganizationPage: organizationPage }, }, - }, - }), apolloClient.query({ query: GET_ORGANIZATION_SUBPAGE_QUERY, variables: { diff --git a/apps/web/screens/queries/Organization.tsx b/apps/web/screens/queries/Organization.tsx index f5a6cd3c5e92..fea842378a77 100644 --- a/apps/web/screens/queries/Organization.tsx +++ b/apps/web/screens/queries/Organization.tsx @@ -416,3 +416,17 @@ export const EMAIL_SIGNUP_MUTATION = gql` } } ` + +export const GET_ORGANIZATION_PARENT_SUBPAGE_QUERY = gql` + query GetOrganizationParentSubpageQuery( + $input: GetOrganizationParentSubpageInput! + ) { + getOrganizationParentSubpage(input: $input) { + title + childLinks { + label + href + } + } + } +` diff --git a/libs/cms/src/lib/cms.contentful.service.ts b/libs/cms/src/lib/cms.contentful.service.ts index f54e8a4794c9..4488f25f5201 100644 --- a/libs/cms/src/lib/cms.contentful.service.ts +++ b/libs/cms/src/lib/cms.contentful.service.ts @@ -83,10 +83,11 @@ import { LifeEventPage, mapLifeEventPage } from './models/lifeEventPage.model' import { GetGenericTagBySlugInput } from './dto/getGenericTagBySlug.input' import { GetGenericTagsInTagGroupsInput } from './dto/getGenericTagsInTagGroups.input' import { Grant, mapGrant } from './models/grant.model' -import { GrantList } from './models/grantList.model' import { mapManual } from './models/manual.model' import { mapServiceWebPage } from './models/serviceWebPage.model' import { mapEvent } from './models/event.model' +import { GetOrganizationParentSubpageInput } from './dto/getOrganizationParentSubpage.input' +import { mapOrganizationParentSubpage } from './models/organizationParentSubpage.model' const errorHandler = (name: string) => { return (error: Error) => { @@ -1151,4 +1152,27 @@ export class CmsContentfulService { return (result.items as types.IGenericTag[]).map(mapGenericTag) } + + async getOrganizationParentSubpage(input: GetOrganizationParentSubpageInput) { + const params = { + content_type: 'organizationParentSubpage', + 'fields.slug': input.slug, + 'fields.organizationPage.sys.contentType.sys.id': 'organizationPage', + 'fields.organizationPage.fields.slug': input.organizationPageSlug, + limit: 1, + } + + const response = await this.contentfulRepository + .getLocalizedEntries( + input.lang, + params, + ) + .catch(errorHandler('getOrganizationParentSubpage')) + + return ( + (response.items as types.IOrganizationParentSubpage[]).map( + mapOrganizationParentSubpage, + )[0] ?? null + ) + } } diff --git a/libs/cms/src/lib/cms.resolver.ts b/libs/cms/src/lib/cms.resolver.ts index 462b5e4531ec..8c82719d6bdc 100644 --- a/libs/cms/src/lib/cms.resolver.ts +++ b/libs/cms/src/lib/cms.resolver.ts @@ -126,6 +126,8 @@ import { Grant } from './models/grant.model' import { GetGrantsInput } from './dto/getGrants.input' import { GetSingleGrantInput } from './dto/getSingleGrant.input' import { GrantList } from './models/grantList.model' +import { OrganizationParentSubpage } from './models/organizationParentSubpage.model' +import { GetOrganizationParentSubpageInput } from './dto/getOrganizationParentSubpage.input' const defaultCache: CacheControlOptions = { maxAge: CACHE_CONTROL_MAX_AGE } @@ -707,6 +709,14 @@ export class CmsResolver { ): Promise { return this.cmsElasticsearchService.getTeamMembers(input) } + + @CacheControl(defaultCache) + @Query(() => OrganizationParentSubpage, { nullable: true }) + getOrganizationParentSubpage( + @Args('input') input: GetOrganizationParentSubpageInput, + ): Promise { + return this.cmsContentfulService.getOrganizationParentSubpage(input) + } } @Resolver(() => LatestNewsSlice) diff --git a/libs/cms/src/lib/dto/getOrganizationParentSubpage.input.ts b/libs/cms/src/lib/dto/getOrganizationParentSubpage.input.ts new file mode 100644 index 000000000000..c45e5ef9fd33 --- /dev/null +++ b/libs/cms/src/lib/dto/getOrganizationParentSubpage.input.ts @@ -0,0 +1,18 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsString } from 'class-validator' +import { ElasticsearchIndexLocale } from '@island.is/content-search-index-manager' + +@InputType() +export class GetOrganizationParentSubpageInput { + @Field() + @IsString() + slug!: string + + @Field() + @IsString() + organizationPageSlug!: string + + @Field(() => String) + @IsString() + lang: ElasticsearchIndexLocale = 'is' +} diff --git a/libs/cms/src/lib/models/organizationParentSubpage.model.ts b/libs/cms/src/lib/models/organizationParentSubpage.model.ts new file mode 100644 index 000000000000..9c9a4d927059 --- /dev/null +++ b/libs/cms/src/lib/models/organizationParentSubpage.model.ts @@ -0,0 +1,48 @@ +import { CacheField } from '@island.is/nest/graphql' +import { Field, ID, ObjectType } from '@nestjs/graphql' +import { IOrganizationParentSubpage } from '../generated/contentfulTypes' +import { getOrganizationPageUrlPrefix } from '@island.is/shared/utils' + +@ObjectType() +class OrganizationSubpageLink { + @Field() + label!: string + + @Field() + href!: string +} + +@ObjectType() +export class OrganizationParentSubpage { + @Field(() => ID) + id!: string + + @Field() + title!: string + + @CacheField(() => [OrganizationSubpageLink]) + childLinks!: OrganizationSubpageLink[] +} + +export const mapOrganizationParentSubpage = ({ + sys, + fields, +}: IOrganizationParentSubpage): OrganizationParentSubpage => { + return { + id: sys.id, + title: fields.title, + childLinks: + fields.pages + ?.filter( + (page) => + Boolean(page.fields.organizationPage?.fields?.slug) && + Boolean(page.fields.slug), + ) + .map((page) => ({ + label: page.fields.title, + href: `/${getOrganizationPageUrlPrefix(sys.locale)}/${ + page.fields.organizationPage.fields.slug + }/${fields.slug}/${page.fields.slug}`, + })) ?? [], + } +} From 331545656d7a9797fef305175462d62f107c8b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3nas=20G=2E=20Sigur=C3=B0sson?= Date: Tue, 26 Nov 2024 15:00:35 +0000 Subject: [PATCH 2/2] feat(app-sys): Shared display field (#17007) * feat: start of display field * feat: display field * fix: revert changes to another template * feat: simplify useEffect logic and undo example form changes * chore: undo example form changes --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../application/core/src/lib/fieldBuilders.ts | 32 ++++++++ libs/application/types/src/lib/Fields.ts | 18 ++++- .../lib/DisplayFormField/DisplayFormField.tsx | 80 +++++++++++++++++++ libs/application/ui-fields/src/lib/index.ts | 1 + 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 libs/application/ui-fields/src/lib/DisplayFormField/DisplayFormField.tsx diff --git a/libs/application/core/src/lib/fieldBuilders.ts b/libs/application/core/src/lib/fieldBuilders.ts index dabe437861a0..2c77343d5aca 100644 --- a/libs/application/core/src/lib/fieldBuilders.ts +++ b/libs/application/core/src/lib/fieldBuilders.ts @@ -46,6 +46,7 @@ import { SliderField, MaybeWithApplication, MaybeWithApplicationAndFieldAndLocale, + DisplayField, FieldsRepeaterField, } from '@island.is/application/types' import { Locale } from '@island.is/shared/types' @@ -1009,3 +1010,34 @@ export const buildSliderField = ( saveAsString, } } + +export const buildDisplayField = ( + data: Omit, +): DisplayField => { + const { + title, + titleVariant, + label, + variant, + marginTop, + marginBottom, + value, + suffix, + rightAlign, + } = data + return { + ...extractCommonFields(data), + title, + titleVariant, + label, + variant, + marginTop, + marginBottom, + type: FieldTypes.DISPLAY, + component: FieldComponents.DISPLAY, + children: undefined, + value, + suffix, + rightAlign, + } +} diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index c6763dbb9141..e5425ca66464 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -16,7 +16,7 @@ import { StaticText, } from './Form' import { ApolloClient } from '@apollo/client' -import { Application } from './Application' +import { Application, FormValue } from './Application' import { CallToAction } from './StateMachine' import { Colors, theme } from '@island.is/island-ui/theme' import { Condition } from './Condition' @@ -262,6 +262,7 @@ export enum FieldTypes { ACCORDION = 'ACCORDION', BANK_ACCOUNT = 'BANK_ACCOUNT', SLIDER = 'SLIDER', + DISPLAY = 'DISPLAY', } export enum FieldComponents { @@ -298,6 +299,7 @@ export enum FieldComponents { ACCORDION = 'AccordionFormField', BANK_ACCOUNT = 'BankAccountFormField', SLIDER = 'SliderFormField', + DISPLAY = 'DisplayFormField', } export interface CheckboxField extends InputField { @@ -796,6 +798,19 @@ export interface SliderField extends BaseField { saveAsString?: boolean } +export interface DisplayField extends BaseField { + readonly type: FieldTypes.DISPLAY + component: FieldComponents.DISPLAY + marginTop?: ResponsiveProp + marginBottom?: ResponsiveProp + titleVariant?: TitleVariants + suffix?: MessageDescriptor | string + rightAlign?: boolean + variant?: TextFieldVariant + label?: MessageDescriptor | string + value: (answers: FormValue) => string +} + export type Field = | CheckboxField | CustomField @@ -832,3 +847,4 @@ export type Field = | AccordionField | BankAccountField | SliderField + | DisplayField diff --git a/libs/application/ui-fields/src/lib/DisplayFormField/DisplayFormField.tsx b/libs/application/ui-fields/src/lib/DisplayFormField/DisplayFormField.tsx new file mode 100644 index 000000000000..ae6faba48fed --- /dev/null +++ b/libs/application/ui-fields/src/lib/DisplayFormField/DisplayFormField.tsx @@ -0,0 +1,80 @@ +import { formatTextWithLocale } from '@island.is/application/core' +import { DisplayField, FieldBaseProps } from '@island.is/application/types' +import { Box, Text } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { InputController } from '@island.is/shared/form-fields' +import { useEffect, useState } from 'react' +import { useFormContext } from 'react-hook-form' +import { Locale } from '@island.is/shared/types' + +interface Props extends FieldBaseProps { + field: DisplayField +} + +export const DisplayFormField = ({ field, application }: Props) => { + const { + value, + id, + title, + titleVariant = 'h4', + label, + variant, + suffix, + rightAlign = false, + } = field + const { watch, setValue } = useFormContext() + const allValues = watch() + const { formatMessage, lang: locale } = useLocale() + const [displayValue, setDisplayValue] = useState(allValues[id]) + + useEffect(() => { + const newDisplayValue = value(allValues) + setDisplayValue(newDisplayValue) + setValue(id, newDisplayValue) + }, [allValues]) + + return ( + + {title ? ( + + {formatTextWithLocale( + title, + application, + locale as Locale, + formatMessage, + )} + + ) : null} + + + + ) +} diff --git a/libs/application/ui-fields/src/lib/index.ts b/libs/application/ui-fields/src/lib/index.ts index 5ca103c7e046..d796e89afbe9 100644 --- a/libs/application/ui-fields/src/lib/index.ts +++ b/libs/application/ui-fields/src/lib/index.ts @@ -31,3 +31,4 @@ export { StaticTableFormField } from './StaticTableFormField/StaticTableFormFiel export { AccordionFormField } from './AccordionFormField/AccordionFormField' export { BankAccountFormField } from './BankAccountFormField/BankAccountFormField' export { SliderFormField } from './SliderFormField/SliderFormField' +export { DisplayFormField } from './DisplayFormField/DisplayFormField'