diff --git a/apps/application-system/form/src/routes/Application.tsx b/apps/application-system/form/src/routes/Application.tsx index 6089533ae754..713ba8a0b2d3 100644 --- a/apps/application-system/form/src/routes/Application.tsx +++ b/apps/application-system/form/src/routes/Application.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { useParams } from 'react-router-dom' import { ApplicationForm, ErrorShell } from '@island.is/application/ui-shell' diff --git a/apps/contentful-apps/constants/index.ts b/apps/contentful-apps/constants/index.ts index 61f10e9bc30c..b7f7d094ff8d 100644 --- a/apps/contentful-apps/constants/index.ts +++ b/apps/contentful-apps/constants/index.ts @@ -5,3 +5,8 @@ export const DEV_WEB_BASE_URL = 'https://beta.dev01.devland.is' export const TITLE_SEARCH_POSTFIX = '--title-search' export const SLUGIFIED_POSTFIX = '--slugified' + +export const CUSTOM_SLUGIFY_REPLACEMENTS: ReadonlyArray<[string, string]> = [ + ['ö', 'o'], + ['þ', 'th'], +] diff --git a/apps/contentful-apps/pages/fields/event-slug-field.tsx b/apps/contentful-apps/pages/fields/event-slug-field.tsx index 44aa381b3e90..d949b73d6ffd 100644 --- a/apps/contentful-apps/pages/fields/event-slug-field.tsx +++ b/apps/contentful-apps/pages/fields/event-slug-field.tsx @@ -5,14 +5,7 @@ import { TextInput } from '@contentful/f36-components' import { useSDK } from '@contentful/react-apps-toolkit' import slugify from '@sindresorhus/slugify' -const slugifyDate = (value: string) => { - try { - const date = new Date(value) - return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}` - } catch { - return '' - } -} +import { slugifyDate } from '../../utils' const EventSlugField = () => { const sdk = useSDK() diff --git a/apps/contentful-apps/pages/fields/generic-list-item-slug-field.tsx b/apps/contentful-apps/pages/fields/generic-list-item-slug-field.tsx new file mode 100644 index 000000000000..c93144b3a659 --- /dev/null +++ b/apps/contentful-apps/pages/fields/generic-list-item-slug-field.tsx @@ -0,0 +1,166 @@ +import { useEffect, useState } from 'react' +import { useDebounce } from 'react-use' +import type { FieldExtensionSDK } from '@contentful/app-sdk' +import { Stack, Text, TextInput } from '@contentful/f36-components' +import { useCMA, useSDK } from '@contentful/react-apps-toolkit' +import slugify from '@sindresorhus/slugify' + +import { CUSTOM_SLUGIFY_REPLACEMENTS } from '../../constants' +import { slugifyDate } from '../../utils' + +const DEBOUNCE_TIME = 100 + +const GenericListItemSlugField = () => { + const sdk = useSDK() + const cma = useCMA() + + const [genericList, setGenericList] = useState(null) + const [hasEntryBeenPublished, setHasEntryBeenPublished] = useState( + Boolean(sdk.entry.getSys()?.firstPublishedAt), + ) + + const [value, setValue] = useState(sdk.field.getValue() ?? '') + const [isValid, setIsValid] = useState(true) + + useEffect(() => { + sdk.entry.onSysChanged((newSys) => { + setHasEntryBeenPublished(Boolean(newSys?.firstPublishedAt)) + }) + }, [sdk.entry]) + + useEffect(() => { + const unsubscribe = sdk.entry.fields.genericList.onValueChanged( + (updatedList) => { + if (updatedList?.sys?.id) { + cma.entry + .get({ + entryId: updatedList.sys.id, + }) + .then(setGenericList) + } else { + setGenericList(null) + } + }, + ) + + return unsubscribe + }, [cma.entry, sdk.entry.fields.genericList]) + + useEffect(() => { + sdk.window.startAutoResizer() + }, [sdk.window]) + + useEffect(() => { + const unsubscribeFromTitleValueChanges = sdk.entry.fields.title + .getForLocale(sdk.field.locale) + .onValueChanged((newTitle) => { + if (!newTitle || hasEntryBeenPublished) { + return + } + const date = sdk.entry.fields.date.getValue() + setValue( + `${slugify(newTitle, { + customReplacements: CUSTOM_SLUGIFY_REPLACEMENTS, + })}${date ? '-' + slugifyDate(date) : ''}`, + ) + }) + + const unsubscribeFromDateValueChanges = + sdk.entry.fields.date.onValueChanged((newDate) => { + const date = newDate + const title = sdk.entry.fields.title + .getForLocale(sdk.field.locale) + .getValue() + if (!title || hasEntryBeenPublished) { + return + } + setValue( + `${slugify(title, { + customReplacements: CUSTOM_SLUGIFY_REPLACEMENTS, + })}${date ? `-${slugifyDate(date)}` : ''}`, + ) + }) + + return () => { + unsubscribeFromTitleValueChanges() + unsubscribeFromDateValueChanges() + } + }, [ + hasEntryBeenPublished, + sdk.entry.fields.date, + sdk.entry.fields.title, + sdk.field.locale, + ]) + + useDebounce( + async () => { + const genericListId = sdk.entry.fields.genericList.getValue()?.sys?.id + if (!genericListId || !value) { + return + } + const itemsInSameListWithSameSlug = + ( + await cma.entry.getMany({ + environmentId: sdk.ids.environment, + spaceId: sdk.ids.space, + query: { + locale: sdk.field.locale, + content_type: 'genericListItem', + 'fields.slug': value, + 'sys.id[ne]': sdk.entry.getSys().id, + 'sys.archivedVersion[exists]': false, + }, + }) + )?.items ?? [] + setIsValid(itemsInSameListWithSameSlug.length <= 0) + }, + DEBOUNCE_TIME, + [value], + ) + + useDebounce( + () => { + if (isValid) { + sdk.field.setValue(value) + } else { + sdk.field.setValue(null) // Set to null to prevent entry publish + } + sdk.field.setInvalid(!isValid) + }, + DEBOUNCE_TIME, + [isValid, value], + ) + + if ( + genericList && + genericList?.fields?.itemType?.[sdk.locales.default] !== 'Clickable' + ) { + return ( + + Slug can only be changed if the list item type is {`"Clickable"`} + + ) + } + + const isInvalid = value.length === 0 || !isValid + + return ( + + { + setValue(ev.target.value) + }} + isInvalid={isInvalid} + /> + {value.length === 0 && sdk.field.locale === sdk.locales.default && ( + Invalid slug + )} + {value.length > 0 && isInvalid && ( + Item already exists with this slug + )} + + ) +} + +export default GenericListItemSlugField diff --git a/apps/contentful-apps/utils/index.ts b/apps/contentful-apps/utils/index.ts index 93d523d9741e..6a73c2d1841e 100644 --- a/apps/contentful-apps/utils/index.ts +++ b/apps/contentful-apps/utils/index.ts @@ -60,3 +60,13 @@ export const parseContentfulErrorMessage = (error: unknown) => { } return errorMessage } + +export const slugifyDate = (value: string) => { + if (!value) return '' + try { + const date = new Date(value) + return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}` + } catch { + return '' + } +} diff --git a/apps/web/components/FilterTag/FilterTag.tsx b/apps/web/components/FilterTag/FilterTag.tsx index 69a8f5b2ea70..19a59ba70b6a 100644 --- a/apps/web/components/FilterTag/FilterTag.tsx +++ b/apps/web/components/FilterTag/FilterTag.tsx @@ -1,17 +1,21 @@ -import { Tag, Box } from '@island.is/island-ui/core' import React from 'react' + +import { Box, Tag } from '@island.is/island-ui/core' + import * as styles from './FilterTag.css' interface FilterTagProps { onClick?: () => void + active?: boolean } export const FilterTag: React.FC> = ({ children, onClick, + active, }) => { return ( - + {children} diff --git a/apps/web/components/GenericList/GenericList.tsx b/apps/web/components/GenericList/GenericList.tsx index 835f62905a46..46c72da665d6 100644 --- a/apps/web/components/GenericList/GenericList.tsx +++ b/apps/web/components/GenericList/GenericList.tsx @@ -1,18 +1,21 @@ import { useRef, useState } from 'react' import { useDebounce } from 'react-use' import { Locale } from 'locale' +import { useRouter } from 'next/router' import { useLazyQuery } from '@apollo/client' import { AlertMessage, Box, FilterInput, + FocusableBox, GridContainer, Pagination, Stack, Text, } from '@island.is/island-ui/core' import { + GenericListItem, GenericListItemResponse, GetGenericListItemsQueryVariables, Query, @@ -44,16 +47,80 @@ const getResultsFoundText = (totalItems: number, locale: Locale) => { return plural } +interface ItemProps { + item: GenericListItem +} + +const NonClickableItem = ({ item }: ItemProps) => { + const { format } = useDateUtils() + + return ( + + + + + {item.date && format(new Date(item.date), 'dd.MM.yyyy')} + + + {item.title} + + + {item.cardIntro?.length > 0 && ( + {webRichText(item.cardIntro ?? [])} + )} + + + ) +} + +const ClickableItem = ({ item }: ItemProps) => { + const { format } = useDateUtils() + const router = useRouter() + + const pathname = new URL(router.asPath, 'https://island.is').pathname + + return ( + + + + + {item.date && format(new Date(item.date), 'dd.MM.yyyy')} + + + {item.title} + + + {item.cardIntro?.length > 0 && ( + {webRichText(item.cardIntro ?? [])} + )} + + + ) +} + interface GenericListProps { id: string firstPageItemResponse?: GenericListItemResponse searchInputPlaceholder?: string | null + itemType?: string | null } export const GenericList = ({ id, firstPageItemResponse, searchInputPlaceholder, + itemType, }: GenericListProps) => { const [searchValue, setSearchValue] = useState('') const [page, setPage] = useState(1) @@ -61,7 +128,6 @@ export const GenericList = ({ const searchValueRef = useRef(searchValue) const [itemsResponse, setItemsResponse] = useState(firstPageItemResponse) const firstRender = useRef(true) - const { format } = useDateUtils() const [errorOccurred, setErrorOccurred] = useState(false) const { activeLocale } = useI18n() @@ -119,6 +185,8 @@ export const GenericList = ({ const resultsFoundText = getResultsFoundText(totalItems, activeLocale) + const itemsAreClickable = itemType === 'Clickable' + return ( @@ -153,28 +221,14 @@ export const GenericList = ({ {totalItems} {resultsFoundText} - {items.map((item) => ( - - - - - {item.date && format(new Date(item.date), 'dd.MM.yyyy')} - - - {item.title} - - - {item.cardIntro?.length > 0 && ( - {webRichText(item.cardIntro ?? [])} - )} - - - ))} + {!itemsAreClickable && + items.map((item) => ( + + ))} + {itemsAreClickable && + items.map((item) => ( + + ))} )} diff --git a/apps/web/components/Header/Header.tsx b/apps/web/components/Header/Header.tsx index 5771b490d7de..b0ab4d768c57 100644 --- a/apps/web/components/Header/Header.tsx +++ b/apps/web/components/Header/Header.tsx @@ -1,4 +1,6 @@ /* eslint-disable jsx-a11y/anchor-is-valid */ +import React, { FC, useContext } from 'react' + import { Box, ButtonTypes, @@ -13,14 +15,14 @@ import { Logo, ResponsiveSpace, } from '@island.is/island-ui/core' +import { webMenuButtonClicked } from '@island.is/plausible' import { FixedNav, SearchInput } from '@island.is/web/components' -import { LoginButton } from './LoginButton' import { useI18n } from '@island.is/web/i18n' import { LayoutProps } from '@island.is/web/layouts/main' -import React, { FC, useContext } from 'react' + import { LanguageToggler } from '../LanguageToggler' import { Menu } from '../Menu/Menu' -import { webMenuButtonClicked } from '@island.is/plausible' +import { LoginButton } from './LoginButton' interface HeaderProps { showSearchInHeader?: boolean @@ -29,6 +31,7 @@ interface HeaderProps { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore make web strict megaMenuData + organizationSearchFilter?: string } const marginLeft = [1, 1, 1, 2] as ResponsiveSpace @@ -38,6 +41,7 @@ export const Header: FC> = ({ buttonColorScheme = 'default', megaMenuData, languageToggleQueryParams, + organizationSearchFilter, children, }) => { const { activeLocale, t } = useI18n() @@ -92,6 +96,7 @@ export const Header: FC> = ({ placeholder={t.searchPlaceholder} autocomplete={true} autosuggest={true} + organization={organizationSearchFilter} /> )} @@ -114,6 +119,7 @@ export const Header: FC> = ({ {...megaMenuData} buttonColorScheme={buttonColorScheme} onMenuOpen={webMenuButtonClicked} + organizationSearchFilter={organizationSearchFilter} /> diff --git a/apps/web/components/LanguageToggler/LanguageToggler.tsx b/apps/web/components/LanguageToggler/LanguageToggler.tsx index 92387da822c9..18acbb753c27 100644 --- a/apps/web/components/LanguageToggler/LanguageToggler.tsx +++ b/apps/web/components/LanguageToggler/LanguageToggler.tsx @@ -110,7 +110,7 @@ export const LanguageToggler = ({ activeTranslations = res.data?.getContentSlug?.activeTranslations } - if (resolveLinkTypeLocally) { + if ((type as string) === 'genericListItem' || resolveLinkTypeLocally) { const localType = typeResolver(pathWithoutQueryParams)?.type if (localType) { type = localType diff --git a/apps/web/components/Menu/Menu.tsx b/apps/web/components/Menu/Menu.tsx index a4662e08f668..88a371db5243 100644 --- a/apps/web/components/Menu/Menu.tsx +++ b/apps/web/components/Menu/Menu.tsx @@ -28,6 +28,7 @@ interface Props { mainLinks: MegaMenuLink[] buttonColorScheme?: ButtonTypes['colorScheme'] onMenuOpen?: () => void + organizationSearchFilter?: string } const minarsidurLink = '/minarsidur/' @@ -39,6 +40,7 @@ export const Menu = ({ mainLinks, buttonColorScheme = 'default', onMenuOpen, + organizationSearchFilter, }: Props) => { const searchInput = useRef() const { activeLocale, t } = useI18n() @@ -146,6 +148,7 @@ export const Menu = ({ autosuggest={true} onRouting={closeModal} skipContext + organization={organizationSearchFilter} /> )} /> diff --git a/apps/web/components/Organization/Slice/SliceMachine.tsx b/apps/web/components/Organization/Slice/SliceMachine.tsx index ddd2f254b5c1..1ff93403ffa8 100644 --- a/apps/web/components/Organization/Slice/SliceMachine.tsx +++ b/apps/web/components/Organization/Slice/SliceMachine.tsx @@ -191,6 +191,7 @@ const renderSlice = ( searchInputPlaceholder={ (slice as GenericListSchema).searchInputPlaceholder } + itemType={(slice as GenericListSchema).itemType} /> ) default: diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index fe35329d43a5..f3ddc9978dbb 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -14,6 +14,7 @@ import { GridRow, Inline, Link, + LinkV2, Navigation, NavigationItem, ProfileCard, @@ -123,6 +124,7 @@ interface WrapperProps { showExternalLinks?: boolean showReadSpeaker?: boolean isSubpage?: boolean + backLink?: { text: string; url: string } } interface HeaderProps { @@ -872,6 +874,7 @@ export const OrganizationWrapper: React.FC< showExternalLinks = false, showReadSpeaker = true, isSubpage = true, + backLink, }) => { const router = useRouter() const { width } = useWindowSize() @@ -922,21 +925,39 @@ export const OrganizationWrapper: React.FC< fullWidthContent={fullWidthContent} sidebarContent={ - { - return item?.href ? ( - - {link} - - ) : ( - link - ) - }} - /> + + {backLink && ( + + + + + + )} + { + return item?.href ? ( + + {link} + + ) : ( + link + ) + }} + /> + {showSecondaryMenu && ( <> {organizationPage.secondaryMenu && diff --git a/apps/web/components/SearchInput/SearchInput.tsx b/apps/web/components/SearchInput/SearchInput.tsx index cedfd7c1651d..62dd3ad5a18d 100644 --- a/apps/web/components/SearchInput/SearchInput.tsx +++ b/apps/web/components/SearchInput/SearchInput.tsx @@ -1,43 +1,44 @@ import React, { - useState, - useEffect, - useCallback, - useRef, forwardRef, ReactElement, + useCallback, + useEffect, useReducer, + useRef, + useState, } from 'react' -import Downshift from 'downshift' import { useMeasure } from 'react-use' +import Downshift from 'downshift' import { useRouter } from 'next/router' import { useApolloClient } from '@apollo/client/react' -import { GET_SEARCH_RESULTS_QUERY } from '@island.is/web/screens/queries' + import { AsyncSearchInput, + AsyncSearchInputProps, AsyncSearchSizes, Box, - Text, - Stack, Link, - AsyncSearchInputProps, + Stack, + Text, } from '@island.is/island-ui/core' +import { TestSupport } from '@island.is/island-ui/utils' +import { trackSearchQuery } from '@island.is/plausible' import { Locale } from '@island.is/shared/types' import { - GetSearchResultsQuery, - QuerySearchResultsArgs, - ContentLanguage, - Article, - SubArticle, - SearchableContentTypes, AnchorPage, + Article, + ContentLanguage, + GetSearchResultsQuery, + LifeEventPage, News, OrganizationSubpage, - LifeEventPage, + QuerySearchResultsArgs, + SearchableContentTypes, + SearchableTags, + SubArticle, } from '@island.is/web/graphql/schema' - import { LinkType, useLinkResolver } from '@island.is/web/hooks/useLinkResolver' -import { TestSupport } from '@island.is/island-ui/utils' -import { trackSearchQuery } from '@island.is/plausible' +import { GET_SEARCH_RESULTS_QUERY } from '@island.is/web/screens/queries' import { extractAnchorPageLinkType } from '@island.is/web/utils/anchorPage' import * as styles from './SearchInput.css' @@ -90,6 +91,7 @@ const useSearch = ( locale: Locale, term?: string, autocomplete?: boolean, + organization?: string, ): SearchState => { const [state, dispatch] = useReducer(searchReducer, initialSearchState) const client = useApolloClient() @@ -134,6 +136,9 @@ const useSearch = ( ], highlightResults: true, useQuery: 'suggestions', + tags: organization + ? [{ key: organization, type: SearchableTags.Organization }] + : undefined, }, }, }) @@ -169,7 +174,11 @@ type SubmitType = { string: string } -const useSubmit = (locale: Locale, onRouting?: () => void) => { +const useSubmit = ( + locale: Locale, + onRouting?: () => void, + organization?: string, +) => { const Router = useRouter() const { linkResolver } = useLinkResolver() @@ -183,6 +192,10 @@ const useSubmit = (locale: Locale, onRouting?: () => void) => { query.referencedBy = Router.query.referencedBy } + if (organization) { + query.organization = organization + } + Router.push({ ...(item.type === 'query' && { pathname: linkResolver('search').href, @@ -199,7 +212,7 @@ const useSubmit = (locale: Locale, onRouting?: () => void) => { onRouting() } }, - [Router, linkResolver, onRouting], + [Router, linkResolver, onRouting, organization], ) } @@ -217,6 +230,7 @@ interface SearchInputProps { onRouting?: () => void skipContext?: boolean quickContentLabel?: string + organization?: string } export const SearchInput = forwardRef< @@ -239,13 +253,14 @@ export const SearchInput = forwardRef< skipContext, quickContentLabel, dataTestId, + organization, }, ref, ) => { const [searchTerm, setSearchTerm] = useState(initialInputValue) - const search = useSearch(locale, searchTerm, autocomplete) + const search = useSearch(locale, searchTerm, autocomplete, organization) - const onSubmit = useSubmit(locale) + const onSubmit = useSubmit(locale, undefined, organization) const [hasFocus, setHasFocus] = useState(false) const onBlur = useCallback(() => setHasFocus(false), [setHasFocus]) const onFocus = useCallback(() => { diff --git a/apps/web/hooks/useLinkResolver/useLinkResolver.ts b/apps/web/hooks/useLinkResolver/useLinkResolver.ts index 216dcabce6a5..338bbb0a8bb4 100644 --- a/apps/web/hooks/useLinkResolver/useLinkResolver.ts +++ b/apps/web/hooks/useLinkResolver/useLinkResolver.ts @@ -39,6 +39,14 @@ export const routesTemplate = { is: '/s/[organization]/vidburdir', en: '/en/o/[organization]/events', }, + organizationsubpagelistitem: { + is: '/s/[organization]/[slug]/[listItemSlug]', + en: '/en/o/[organization]/[slug]/[listItemSlug]', + }, + projectsubpagelistitem: { + is: '/v/[project]/[slug]/[listItemSlug]', + en: '/en/p/[project]/[slug]/[listItemSlug]', + }, aboutsubpage: { is: '/s/stafraent-island/[slug]', en: '', diff --git a/apps/web/layouts/main.tsx b/apps/web/layouts/main.tsx index 07ae2743e7cc..4a3dda2ee68b 100644 --- a/apps/web/layouts/main.tsx +++ b/apps/web/layouts/main.tsx @@ -57,6 +57,7 @@ import { GET_CATEGORIES_QUERY, GET_NAMESPACE_QUERY } from '../screens/queries' import { GET_ALERT_BANNER_QUERY } from '../screens/queries/AlertBanner' import { GET_GROUPED_MENU_QUERY } from '../screens/queries/Menu' import { Screen } from '../types' +import { extractOrganizationSlugFromPathname } from '../utils/organization' import { formatMegaMenuCategoryLinks, formatMegaMenuLinks, @@ -246,6 +247,11 @@ const Layout: Screen = ({ const isServiceWeb = pathIsRoute(router.asPath, 'serviceweb', activeLocale) + const organizationSearchFilter = extractOrganizationSlugFromPathname( + router.asPath, + activeLocale, + ) + return ( @@ -403,6 +409,7 @@ const Layout: Screen = ({ showSearchInHeader={showSearchInHeader} megaMenuData={megaMenuData} languageToggleQueryParams={languageToggleQueryParams} + organizationSearchFilter={organizationSearchFilter} /> )} diff --git a/apps/web/pages/en/o/[slug]/[subSlug]/[genericListItemSlug]/index.ts b/apps/web/pages/en/o/[slug]/[subSlug]/[genericListItemSlug]/index.ts new file mode 100644 index 000000000000..64f34948d7ef --- /dev/null +++ b/apps/web/pages/en/o/[slug]/[subSlug]/[genericListItemSlug]/index.ts @@ -0,0 +1,12 @@ +import withApollo from '@island.is/web/graphql/withApollo' +import { withLocale } from '@island.is/web/i18n' +import { Component } from '@island.is/web/pages/s/[slug]/[subSlug]/[genericListItemSlug]' +import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePropsWrapper' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore make web strict +const Screen = withApollo(withLocale('en')(Component)) + +export default Screen + +export const getServerSideProps = getServerSidePropsWrapper(Screen) diff --git a/apps/web/pages/en/p/[slug]/[subSlug]/[genericListItemSlug]/index.ts b/apps/web/pages/en/p/[slug]/[subSlug]/[genericListItemSlug]/index.ts new file mode 100644 index 000000000000..a0af49de48f0 --- /dev/null +++ b/apps/web/pages/en/p/[slug]/[subSlug]/[genericListItemSlug]/index.ts @@ -0,0 +1,12 @@ +import withApollo from '@island.is/web/graphql/withApollo' +import { withLocale } from '@island.is/web/i18n' +import { Component } from '@island.is/web/pages/v/[slug]/[subSlug]/[genericListItemSlug]' +import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePropsWrapper' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore make web strict +const Screen = withApollo(withLocale('en')(Component)) + +export default Screen + +export const getServerSideProps = getServerSidePropsWrapper(Screen) diff --git a/apps/web/pages/s/[slug]/[subSlug]/[genericListItemSlug]/index.tsx b/apps/web/pages/s/[slug]/[subSlug]/[genericListItemSlug]/index.tsx new file mode 100644 index 000000000000..26366989d023 --- /dev/null +++ b/apps/web/pages/s/[slug]/[subSlug]/[genericListItemSlug]/index.tsx @@ -0,0 +1,115 @@ +import { useMemo } from 'react' +import { useRouter } from 'next/router' + +import withApollo from '@island.is/web/graphql/withApollo' +import { useLinkResolver } from '@island.is/web/hooks' +import { useI18n, withLocale } from '@island.is/web/i18n' +import type { LayoutProps } from '@island.is/web/layouts/main' +import GenericListItemPage, { + type GenericListItemPageProps, +} from '@island.is/web/screens/GenericList/GenericListItem' +import SubPageLayout, { + type SubPageProps, +} from '@island.is/web/screens/Organization/SubPage' +import type { Screen as ScreenType } from '@island.is/web/types' +import { CustomNextError } from '@island.is/web/units/errors' +import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePropsWrapper' + +interface ComponentProps { + parentProps: { + layoutProps: LayoutProps + componentProps: SubPageProps + } + genericListItemProps: GenericListItemPageProps +} + +export const Component: ScreenType = ({ + parentProps, + genericListItemProps, +}) => { + const { activeLocale } = useI18n() + const router = useRouter() + const { linkResolver } = useLinkResolver() + const backLinkUrl = useMemo(() => { + const pathname = new URL(router.asPath, 'https://island.is').pathname + return pathname.slice(0, pathname.lastIndexOf('/')) + }, [router.asPath]) + + const { organizationPage, subpage } = parentProps.componentProps + + return ( + + ), + customBreadcrumbItems: [ + { + title: 'Ísland.is', + href: linkResolver('homepage').href, + }, + { + title: organizationPage?.title ?? '', + href: linkResolver('organizationpage', [ + organizationPage?.slug ?? '', + ]).href, + }, + { + title: subpage?.title ?? '', + href: backLinkUrl, + isTag: true, + }, + ], + backLink: { + text: activeLocale === 'is' ? 'Til baka' : 'Go back', + url: backLinkUrl, + }, + customContentfulIds: [ + organizationPage?.id, + subpage?.id, + genericListItemProps.item.id, + ], + }} + /> + ) +} + +Component.getProps = async (ctx) => { + const [parentProps, genericListItemProps] = await Promise.all([ + SubPageLayout.getProps?.(ctx), + GenericListItemPage.getProps?.(ctx), + ]) + + if (!parentProps) { + throw new CustomNextError( + 404, + 'Could not fetch subpage layout for generic list item', + ) + } + if (!genericListItemProps) { + throw new CustomNextError(404, 'Could not fetch generic list item props') + } + + return { + parentProps, + genericListItemProps, + } +} + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore make web strict +const Screen = withApollo(withLocale('is')(Component)) + +export default Screen + +export const getServerSideProps = getServerSidePropsWrapper(Screen) diff --git a/apps/web/pages/v/[slug]/[subSlug]/[genericListItemSlug]/index.tsx b/apps/web/pages/v/[slug]/[subSlug]/[genericListItemSlug]/index.tsx new file mode 100644 index 000000000000..94a01d0664e6 --- /dev/null +++ b/apps/web/pages/v/[slug]/[subSlug]/[genericListItemSlug]/index.tsx @@ -0,0 +1,121 @@ +import { useMemo } from 'react' +import { useRouter } from 'next/router' + +import type { ProjectPage } from '@island.is/web/graphql/schema' +import withApollo from '@island.is/web/graphql/withApollo' +import { useLinkResolver } from '@island.is/web/hooks' +import { useI18n, withLocale } from '@island.is/web/i18n' +import type { LayoutProps } from '@island.is/web/layouts/main' +import type { GenericListItemPageProps } from '@island.is/web/screens/GenericList/GenericListItem' +import GenericListItemPage from '@island.is/web/screens/GenericList/GenericListItem' +import SubPageLayout, { + type PageProps, +} from '@island.is/web/screens/Project/Project' +import type { Screen as ScreenType } from '@island.is/web/types' +import { CustomNextError } from '@island.is/web/units/errors' +import { getServerSidePropsWrapper } from '@island.is/web/utils/getServerSidePropsWrapper' + +interface ComponentProps { + parentProps: { + layoutProps: LayoutProps + componentProps: PageProps + } + genericListItemProps: GenericListItemPageProps +} + +export const Component: ScreenType = ({ + parentProps, + genericListItemProps, +}) => { + const { activeLocale } = useI18n() + const router = useRouter() + const { linkResolver } = useLinkResolver() + const backLinkUrl = useMemo(() => { + const pathname = new URL(router.asPath, 'https://island.is').pathname + return pathname.slice(0, pathname.lastIndexOf('/')) + }, [router.asPath]) + + const projectPage = parentProps.componentProps.projectPage as ProjectPage + const subpage = projectPage.projectSubpages.find( + (subpage) => subpage.slug === router.query.subSlug, + ) + + return ( + + ), + projectPage: { + ...projectPage, + backLink: { + date: '', + id: '', + text: activeLocale === 'is' ? 'Til baka' : 'Go back', + url: backLinkUrl, + }, + }, + }} + /> + ) +} + +Component.getProps = async (ctx) => { + const [parentProps, genericListItemProps] = await Promise.all([ + SubPageLayout.getProps?.(ctx), + GenericListItemPage.getProps?.(ctx), + ]) + + if (!parentProps) { + throw new CustomNextError( + 404, + 'Could not fetch subpage layout for generic list item', + ) + } + if (!genericListItemProps) { + throw new CustomNextError(404, 'Could not fetch generic list item props') + } + + return { + parentProps, + genericListItemProps, + } +} + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore make web strict +const Screen = withApollo(withLocale('is')(Component)) + +export default Screen + +export const getServerSideProps = getServerSidePropsWrapper(Screen) diff --git a/apps/web/screens/GenericList/GenericListItem.tsx b/apps/web/screens/GenericList/GenericListItem.tsx new file mode 100644 index 000000000000..579279a5904e --- /dev/null +++ b/apps/web/screens/GenericList/GenericListItem.tsx @@ -0,0 +1,86 @@ +import { Box, GridContainer, Stack, Text } from '@island.is/island-ui/core' +import { HeadWithSocialSharing, Webreader } from '@island.is/web/components' +import type { + GenericListItem, + Query, + QueryGetGenericListItemBySlugArgs, +} from '@island.is/web/graphql/schema' +import { useDateUtils } from '@island.is/web/i18n/useDateUtils' +import { CustomNextError } from '@island.is/web/units/errors' +import { webRichText } from '@island.is/web/utils/richText' + +import type { Screen } from '../../types' +import { GET_GENERIC_LIST_ITEM_BY_SLUG_QUERY } from '../queries/GenericList' + +export interface GenericListItemPageProps { + item: GenericListItem + showReadspeaker?: boolean + ogTitle?: string +} + +const GenericListItemPage: Screen = ({ + item, + showReadspeaker = true, + ogTitle, +}) => { + const { format } = useDateUtils() + + return ( + + + {ogTitle && } + + {item.date && ( + + {format(new Date(item.date), 'dd.MM.yyyy')} + + )} + + + {item.title} + + {showReadspeaker && } + + {webRichText(item.content ?? [])} + + + + ) +} + +GenericListItemPage.getProps = async ({ apolloClient, query, locale }) => { + const slug = query.genericListItemSlug + + if (!slug) { + throw new CustomNextError( + 404, + 'Generic List item could not be found since no slug was provided', + ) + } + + const response = await apolloClient.query< + Query, + QueryGetGenericListItemBySlugArgs + >({ + query: GET_GENERIC_LIST_ITEM_BY_SLUG_QUERY, + variables: { + input: { + lang: locale, + slug: slug as string, + }, + }, + }) + + if (!response?.data?.getGenericListItemBySlug) { + throw new CustomNextError( + 404, + `Generic List item with slug: ${slug} could not be found`, + ) + } + + return { + item: response.data.getGenericListItemBySlug, + } +} + +export default GenericListItemPage diff --git a/apps/web/screens/Organization/SubPage.tsx b/apps/web/screens/Organization/SubPage.tsx index 814241cb3c2f..79f0f6c5cbd7 100644 --- a/apps/web/screens/Organization/SubPage.tsx +++ b/apps/web/screens/Organization/SubPage.tsx @@ -5,6 +5,7 @@ import { ParsedUrlQuery } from 'querystring' import { SliceType } from '@island.is/island-ui/contentful' import { Box, + BreadCrumbItem, GridColumn, GridContainer, GridRow, @@ -14,7 +15,6 @@ import { Text, } from '@island.is/island-ui/core' import { - Form, getThemeConfig, OrganizationWrapper, SignLanguageButton, @@ -49,11 +49,15 @@ import { GET_ORGANIZATION_SUBPAGE_QUERY, } from '../queries' -interface SubPageProps { +export interface SubPageProps { organizationPage: Query['getOrganizationPage'] subpage: Query['getOrganizationSubpage'] namespace: Record locale: Locale + customContent?: React.ReactNode + backLink?: { text: string; url: string } + customBreadcrumbItems?: BreadCrumbItem[] + customContentfulIds?: (string | undefined)[] } const SubPage: Screen = ({ @@ -61,6 +65,10 @@ const SubPage: Screen = ({ subpage, namespace, locale, + customContent, + customBreadcrumbItems, + customContentfulIds, + backLink, }) => { const router = useRouter() const { activeLocale } = useI18n() @@ -68,7 +76,11 @@ const SubPage: Screen = ({ const n = useNamespace(namespace) const { linkResolver } = useLinkResolver() - useContentfulId(organizationPage?.id, subpage?.id) + const contentfulIds = customContentfulIds + ? customContentfulIds + : [organizationPage?.id, subpage?.id] + + useContentfulId(...contentfulIds) const pathWithoutHash = router.asPath.split('#')[0] // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -150,20 +162,25 @@ const SubPage: Screen = ({ pageFeaturedImage={ subpage?.featuredImage ?? organizationPage?.featuredImage } - breadcrumbItems={[ - { - title: 'Ísland.is', - href: linkResolver('homepage', [], locale).href, - }, - { - title: organizationPage?.title ?? '', - href: linkResolver( - 'organizationpage', - [organizationPage?.slug ?? ''], - locale, - ).href, - }, - ]} + backLink={backLink} + breadcrumbItems={ + customBreadcrumbItems + ? customBreadcrumbItems + : [ + { + title: 'Ísland.is', + href: linkResolver('homepage', [], locale).href, + }, + { + title: organizationPage?.title ?? '', + href: linkResolver( + 'organizationpage', + [organizationPage?.slug ?? ''], + locale, + ).href, + }, + ] + } navigationData={{ title: n('navigationTitle', 'Efnisyfirlit'), items: navList, @@ -182,56 +199,62 @@ const SubPage: Screen = ({ subpage?.links?.length ? '7/12' : '12/12', ]} > - - - {subpage?.title} - - - - - {subpage?.signLanguageVideo?.url && ( - - - {subpage.title} - - {content} - {renderSlices( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict - subpage.slices, - subpage.sliceCustomRenderer, - subpage.sliceExtraText, - namespace, - organizationPage?.slug, - )} - - } - /> - )} - + {customContent ? ( + customContent + ) : ( + <> + + + {subpage?.title} + + + + + {subpage?.signLanguageVideo?.url && ( + + + {subpage.title} + + {content} + {renderSlices( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore make web strict + subpage.slices, + subpage.sliceCustomRenderer, + subpage.sliceExtraText, + namespace, + organizationPage?.slug, + )} + + } + /> + )} + + + )} - {content} + {!customContent && content} diff --git a/apps/web/screens/Project/Project.tsx b/apps/web/screens/Project/Project.tsx index cc5ca0380ccc..433573aa5fbf 100644 --- a/apps/web/screens/Project/Project.tsx +++ b/apps/web/screens/Project/Project.tsx @@ -46,7 +46,7 @@ import { ProjectFooter } from './components/ProjectFooter' import { ProjectWrapper } from './components/ProjectWrapper' import { getThemeConfig } from './utils' -interface PageProps { +export interface PageProps { projectPage: Query['getProjectPage'] namespace: Record projectNamespace: Record @@ -54,6 +54,10 @@ interface PageProps { stepOptionsFromNamespace: { data: Record[]; slug: string }[] stepperNamespace: Record locale: Locale + backLink?: { url: string; text: string } + customContentfulIds?: (string | undefined)[] + customBreadcrumbItems?: BreadCrumbItem[] + customContent?: React.ReactNode } const ProjectPage: Screen = ({ projectPage, @@ -62,6 +66,10 @@ const ProjectPage: Screen = ({ stepperNamespace, stepOptionsFromNamespace, locale, + backLink, + customContentfulIds, + customBreadcrumbItems, + customContent, }) => { const n = useNamespace(namespace) const p = useNamespace(projectNamespace) @@ -77,7 +85,11 @@ const ProjectPage: Screen = ({ [router.query.subSlug, projectPage?.projectSubpages], ) - useContentfulId(projectPage?.id, subpage?.id) + const contentfulIds = customContentfulIds + ? customContentfulIds + : [projectPage?.id, subpage?.id] + + useContentfulId(...contentfulIds) const baseRouterPath = router.asPath.split('?')[0].split('#')[0] @@ -116,7 +128,9 @@ const ProjectPage: Screen = ({ } }, [renderSlicesAsTabs, subpage, router.asPath]) - const breadCrumbs: BreadCrumbItem[] = !subpage + const breadCrumbs: BreadCrumbItem[] = customBreadcrumbItems + ? customBreadcrumbItems + : !subpage ? [] : [ { @@ -140,6 +154,128 @@ const ProjectPage: Screen = ({ const pageSlices = (subpage ?? projectPage)?.slices ?? [] + const mainContent = ( + <> + {!subpage && shouldDisplayWebReader && ( + + )} + {!!subpage && ( + + + {subpage.title} + + {shouldDisplayWebReader && ( + + )} + {subpage.showTableOfContents && ( + + + + )} + {subpage.content && ( + + {webRichText( + subpage.content as SliceType[], + undefined, + activeLocale, + )} + + )} + + )} + {renderSlicesAsTabs && !!subpage && subpage.slices.length > 1 && ( + + ({ + headingId: slice.id, + headingTitle: (slice as OneColumnText).title, + }))} + selectedHeadingId={selectedSliceTab?.id} + onClick={(id) => { + const slice = subpage.slices.find( + (s) => s.id === id, + ) as OneColumnText + router.push( + `${baseRouterPath}#${slugify(slice.title)}`, + undefined, + { shallow: true }, + ) + setSelectedSliceTab(slice) + }} + /> + + )} + {renderSlicesAsTabs && selectedSliceTab && ( + + + {selectedSliceTab.title} + + + )} + {content?.length > 0 && ( + + {webRichText(content, { + renderComponent: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore make web strict + TabSection: (slice) => ( + + ), + }, + })} + + )} + {!subpage && projectPage?.stepper && ( + + + + )} + {!renderSlicesAsTabs && pageSlices.length > 0 && ( + + {pageSlices.map((slice: Slice, index) => { + const sliceCount = pageSlices.length + return ( + + + + ) + })} + + )} + + ) + return ( <> = ({ breadcrumbItems={breadCrumbs} sidebarNavigationTitle={navigationTitle} withSidebar={projectPage?.sidebar} + backLink={backLink} > - {!subpage && shouldDisplayWebReader && ( - - )} - {!!subpage && ( - - - {subpage.title} - - {shouldDisplayWebReader && ( - - )} - {subpage.showTableOfContents && ( - - - - )} - {subpage.content && ( - - {webRichText( - subpage.content as SliceType[], - undefined, - activeLocale, - )} - - )} - - )} - {renderSlicesAsTabs && !!subpage && subpage.slices.length > 1 && ( - - ({ - headingId: slice.id, - headingTitle: (slice as OneColumnText).title, - }))} - selectedHeadingId={selectedSliceTab?.id} - onClick={(id) => { - const slice = subpage.slices.find( - (s) => s.id === id, - ) as OneColumnText - router.push( - `${baseRouterPath}#${slugify(slice.title)}`, - undefined, - { shallow: true }, - ) - setSelectedSliceTab(slice) - }} - /> - - )} - {renderSlicesAsTabs && selectedSliceTab && ( - - - {selectedSliceTab.title} - - - )} - {content?.length > 0 && ( - - {webRichText(content, { - renderComponent: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict - TabSection: (slice) => ( - - ), - }, - })} - - )} - {!subpage && projectPage?.stepper && ( - - - - )} - {!renderSlicesAsTabs && pageSlices.length > 0 && ( - - {pageSlices.map((slice: Slice, index) => { - const sliceCount = pageSlices.length - return ( - - - - ) - })} - - )} + {customContent ? customContent : mainContent} referencedByTitle?: string } @@ -137,7 +137,7 @@ const connectedTypes: Partial< webManual: ['WebManual', 'WebManualChapterItem'], } -const stringToArray = (value: string | string[]) => +const stringToArray = (value: string | string[] | undefined) => Array.isArray(value) ? value : value?.length ? [value] : [] const Search: Screen = ({ @@ -153,14 +153,8 @@ const Search: Screen = ({ ...initialState, query: { q, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict type: stringToArray(query.type) as SearchableContentTypes[], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict category: stringToArray(query.category), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict organization: stringToArray(query.organization), }, }) @@ -173,8 +167,6 @@ const Search: Screen = ({ const { activeLocale } = useI18n() const searchRef = useRef(null) const routerReplace = useRouterReplace() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict const n = useNamespace(namespace) const { linkResolver } = useLinkResolver() @@ -227,27 +219,17 @@ const Search: Screen = ({ const getLabels = (item: SearchEntryType) => { const labels = [] - switch (item.__typename) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict + switch (item.__typename as string | undefined) { case 'LifeEventPage': labels.push(n('lifeEvent')) break - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict case 'News': labels.push(n('newsTitle')) break - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict case 'AdgerdirPage': labels.push(n('adgerdirTitle')) break - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict case 'ManualChapterItem': - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore make web strict labels.push(item.manualChapter.title) break default: @@ -452,32 +434,36 @@ const Search: Screen = ({ } } - const categories: CategoriesProps[] = [ - { - id: 'category', - label: n('categories', 'Þjónustuflokkar'), - selected: state.query.category ?? [], - singleOption: true, - filters: (countResults?.tagCounts ?? []) - .filter((x) => x.value.trim() && x.type === 'category') - .map(({ key, value }) => ({ - label: value, - value: key, - })), - }, - { - id: 'organization', - label: n('organizations', 'Opinberir aðilar'), - selected: state.query.organization ?? [], - singleOption: true, - filters: (countResults?.tagCounts ?? []) - .filter((x) => x.value.trim() && x.type === 'organization') - .map(({ key, value }) => ({ - label: value, - value: key, - })), - }, - ] + const categories: CategoriesProps[] = useMemo( + () => [ + { + id: 'category', + label: n('categories', 'Þjónustuflokkar'), + selected: stringToArray(state.query.category), + singleOption: true, + filters: (countResults?.tagCounts ?? []) + .filter((x) => x.value.trim() && x.type === 'category') + .map(({ key, value }) => ({ + label: value, + value: key, + })), + }, + { + id: 'organization', + label: n('organizations', 'Opinberir aðilar'), + selected: stringToArray(state.query.organization), + singleOption: true, + filters: (countResults?.tagCounts ?? []) + .filter((x) => x.value.trim() && x.type === 'organization') + .map(({ key, value }) => ({ + label: value, + value: key, + })), + }, + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [countResults?.tagCounts, state.query.category, state.query.organization], + ) const filterLabels: FilterLabels = { labelClearAll: n('labelClearAll', 'Hreinsa allar síur'), @@ -491,6 +477,49 @@ const Search: Screen = ({ const [referencedBy, setReferencedBy] = useQueryState('referencedBy') + const filterTags = useMemo(() => { + const filterTags: { label: string; onClick: () => void }[] = [] + + if (referencedBy && referencedByTitle) { + filterTags.push({ + label: referencedByTitle, + onClick: () => { + dispatch({ + type: ActionType.SET_PARAMS, + payload: { + referencedBy: null, + }, + }) + }, + }) + } + + for (const category of categories) { + for (const selectedCategory of category.selected) { + const label = category.filters.find( + (c) => c.value === selectedCategory, + )?.label + filterTags.push({ + label: typeof label === 'string' ? label : selectedCategory, + onClick: () => { + dispatch({ + type: ActionType.SET_PARAMS, + payload: { + query: { + [category.id]: category.selected.filter( + (c) => c !== selectedCategory, + ), + }, + }, + }) + }, + }) + } + } + + return filterTags + }, [categories, referencedBy, referencedByTitle]) + return ( <> @@ -537,109 +566,103 @@ const Search: Screen = ({ 0 ? 'top' : 'center'} space={3} flexWrap="nowrap" collapseBelow="md" > - {referencedBy && ( - - {referencedByTitle && ( + + {filterTags.length > 0 && ( + {n( - 'referencedByPrefix', + 'filteredByPrefix', activeLocale === 'is' ? 'Síað eftir' : 'Filtered by', )} : - )} - { - setReferencedBy(null) - dispatch({ - type: ActionType.RESET_SEARCH, - }) - }} - > - {referencedByTitle || - n( - 'clearSearchFilters', - activeLocale === 'is' - ? 'Hreinsa síu' - : 'Clear filters', - )} - - - )} - {!referencedBy && ( - - {countResults.total > 0 && ( - { - dispatch({ - type: ActionType.RESET_SEARCH, - }) - }} - > - {n('showAllResults', 'Sýna allt')} - - )} - {tagsList - .filter((x) => x.count > 0) - .map(({ title, key }, index) => ( + + {filterTags.map((tag) => ( + + {tag.label} + + ))} + + + )} + {!referencedBy && ( + + {countResults.total > 0 && ( { dispatch({ - type: ActionType.SET_PARAMS, - payload: { - query: { - processentry: false, - ...getSearchParams(key), - category: [], - organization: [], - }, - searchLocked: false, - }, + type: ActionType.RESET_SEARCH, }) }} > - {title} - - ))} - {typeof countResults.processEntryCount == 'number' && - countResults.processEntryCount > 0 && ( - { - dispatch({ - type: ActionType.SET_PARAMS, - payload: { - query: { - processentry: true, - ...getSearchParams('webArticle'), - }, - searchLocked: false, - }, - }) - }} - > - {n('processEntry', 'Umsóknir')} + {n('showAllResults', 'Sýna allt')} )} - - )} - + {tagsList + .filter((x) => x.count > 0) + .map(({ title, key }) => ( + { + dispatch({ + type: ActionType.SET_PARAMS, + payload: { + query: { + processentry: false, + ...getSearchParams(key), + category: [], + organization: [], + }, + searchLocked: false, + }, + }) + }} + > + {title} + + ))} + {typeof countResults.processEntryCount == 'number' && + countResults.processEntryCount > 0 && ( + { + dispatch({ + type: ActionType.SET_PARAMS, + payload: { + query: { + processentry: true, + ...getSearchParams('webArticle'), + }, + searchLocked: false, + }, + }) + }} + > + {n('processEntry', 'Umsóknir')} + + )} + + )} + - {total} niðurstöður + {total} {total === 1 ? 'niðurstaða' : 'niðurstöður'} {' '} - fundust á ensku. + {total === 1 ? 'fannst' : 'fundust'} á ensku. ) diff --git a/apps/web/screens/queries/GenericList.ts b/apps/web/screens/queries/GenericList.ts index 0767d0a6cba8..73cd9b6f972a 100644 --- a/apps/web/screens/queries/GenericList.ts +++ b/apps/web/screens/queries/GenericList.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag' -import { htmlFields } from './fragments' +import { htmlFields, nestedFields, slices } from './fragments' export const GET_GENERIC_LIST_ITEMS_QUERY = gql` query GetGenericListItems($input: GetGenericListItemsInput!) { @@ -16,9 +16,26 @@ export const GET_GENERIC_LIST_ITEMS_QUERY = gql` cardIntro { ...HtmlFields } + slug } total } } ${htmlFields} ` + +export const GET_GENERIC_LIST_ITEM_BY_SLUG_QUERY = gql` + query GetGenericListItemBySlug($input: GetGenericListItemBySlugInput!) { + getGenericListItemBySlug(input: $input) { + id + date + title + slug + content { + ...AllSlices + ${nestedFields} + } + } + } + ${slices} +` diff --git a/apps/web/screens/queries/fragments.ts b/apps/web/screens/queries/fragments.ts index 67e69cb46f0e..36f6292a4c57 100644 --- a/apps/web/screens/queries/fragments.ts +++ b/apps/web/screens/queries/fragments.ts @@ -853,6 +853,7 @@ export const slices = gql` __typename id searchInputPlaceholder + itemType firstPageListItemResponse { input { genericListId @@ -865,6 +866,7 @@ export const slices = gql` id date title + slug cardIntro { ...HtmlFields } diff --git a/apps/web/utils/organization.ts b/apps/web/utils/organization.ts index eec4959d238f..be9cbb93ff83 100644 --- a/apps/web/utils/organization.ts +++ b/apps/web/utils/organization.ts @@ -1,7 +1,8 @@ import { Locale } from 'locale' import { OrganizationPage, OrganizationTheme } from '../graphql/schema' -import { linkResolver } from '../hooks' +import { linkResolver, pathIsRoute } from '../hooks' +import { isLocale } from '../i18n/I18n' // TODO: Perhaps add this functionality to the linkResolver export const getOrganizationLink = ( @@ -46,3 +47,16 @@ export const getBackgroundStyle = ( linear-gradient(180deg, rgba(0,0,0,0.5) 0%, rgba(0, 0, 0, 0) 70%)` return background.backgroundColor ?? '' } + +export const extractOrganizationSlugFromPathname = ( + pathname: string, + locale: Locale, +) => { + const isOrganizationPage = pathIsRoute(pathname, 'organizations', locale) + if (!isOrganizationPage) { + return '' + } + const segments = pathname.split('/').filter((x) => x) + const localeSegment = isLocale(segments[0]) ? segments[0] : '' + return (localeSegment ? segments[2] : segments[1]) ?? '' +} diff --git a/apps/web/utils/richText.tsx b/apps/web/utils/richText.tsx index c3d5635d6e08..a8a0b163ddad 100644 --- a/apps/web/utils/richText.tsx +++ b/apps/web/utils/richText.tsx @@ -193,6 +193,7 @@ const defaultRenderComponent = { id={slice.id} firstPageItemResponse={slice.firstPageListItemResponse} searchInputPlaceholder={slice.searchInputPlaceholder} + itemType={slice.itemType} /> ), } diff --git a/libs/application/core/src/lib/fieldBuilders.ts b/libs/application/core/src/lib/fieldBuilders.ts index fe5a1bc8ec2b..575115e45a09 100644 --- a/libs/application/core/src/lib/fieldBuilders.ts +++ b/libs/application/core/src/lib/fieldBuilders.ts @@ -65,6 +65,7 @@ const extractCommonFields = ( title, dataTestId, width = 'full', + nextButtonText, } = data return { @@ -77,6 +78,7 @@ const extractCommonFields = ( doesNotRequireAnswer, title, width, + nextButtonText, } } @@ -89,6 +91,7 @@ export function buildCheckboxField( large = true, required, backgroundColor = 'blue', + spacing, } = data return { ...extractCommonFields(data), @@ -98,6 +101,7 @@ export function buildCheckboxField( backgroundColor, options, required, + spacing, type: FieldTypes.CHECKBOX, component: FieldComponents.CHECKBOX, } diff --git a/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts b/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts index 9a17c660c61e..772018808d1f 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/inheritance-report/utils/mappers.ts @@ -92,6 +92,7 @@ export const expandAnswers = ( caseNumber: string } => { return { + applicationFor: answers.applicationFor ?? '', applicant: answers.applicant, executors: { executor: { diff --git a/libs/application/templates/aosh/change-machine-supervisor/src/forms/ChangeMachineSupervisorForm/InformationSection/pickMachineSubSection.ts b/libs/application/templates/aosh/change-machine-supervisor/src/forms/ChangeMachineSupervisorForm/InformationSection/pickMachineSubSection.ts index cc875ba4df91..06e6ce54fbeb 100644 --- a/libs/application/templates/aosh/change-machine-supervisor/src/forms/ChangeMachineSupervisorForm/InformationSection/pickMachineSubSection.ts +++ b/libs/application/templates/aosh/change-machine-supervisor/src/forms/ChangeMachineSupervisorForm/InformationSection/pickMachineSubSection.ts @@ -7,6 +7,7 @@ import { } from '@island.is/application/core' import { information } from '../../../lib/messages' import { MachinesWithTotalCount } from '@island.is/clients/work-machines' +import { Application } from '@island.is/application/types' export const pickMachineSubSection = buildSubSection({ id: 'pickMachine', @@ -29,7 +30,11 @@ export const pickMachineSubSection = buildSubSection({ return machines.totalCount <= 5 }, - + defaultValue: (application: Application) => { + const machineList = application?.externalData.machinesList + .data as MachinesWithTotalCount + return machineList?.machines[0].id ?? '' + }, options: (application) => { const machineList = application?.externalData.machinesList .data as MachinesWithTotalCount @@ -39,6 +44,7 @@ export const pickMachineSubSection = buildSubSection({ label: machine?.regNumber || '', subLabel: `${machine.category}: ${machine.type} - ${machine.subType}`, disabled: machine?.disabled || false, + tag: machine?.disabled ? { label: machine?.status || '', diff --git a/libs/application/templates/aosh/change-machine-supervisor/src/lib/dataSchema.ts b/libs/application/templates/aosh/change-machine-supervisor/src/lib/dataSchema.ts index 10805abbff94..48cd0ccb378b 100644 --- a/libs/application/templates/aosh/change-machine-supervisor/src/lib/dataSchema.ts +++ b/libs/application/templates/aosh/change-machine-supervisor/src/lib/dataSchema.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import * as kennitala from 'kennitala' +import { isValidNumber } from 'libphonenumber-js' export const MachineAnswersSchema = z.object({ machine: z.object({ @@ -54,7 +55,7 @@ export const MachineAnswersSchema = z.object({ ) .refine( ({ phone, isOwner }) => { - return isOwner[0] === 'ownerIsSupervisor' || phone !== '' + return isOwner[0] === 'ownerIsSupervisor' || isValidNumber(phone ?? '') }, { path: ['phone'] }, ), diff --git a/libs/application/templates/aosh/deregister-machine/src/forms/DeregisterMachineForm/InformationSection/pickMachineSubSection.ts b/libs/application/templates/aosh/deregister-machine/src/forms/DeregisterMachineForm/InformationSection/pickMachineSubSection.ts index e46de1d3ee9c..517664712fa1 100644 --- a/libs/application/templates/aosh/deregister-machine/src/forms/DeregisterMachineForm/InformationSection/pickMachineSubSection.ts +++ b/libs/application/templates/aosh/deregister-machine/src/forms/DeregisterMachineForm/InformationSection/pickMachineSubSection.ts @@ -7,6 +7,7 @@ import { } from '@island.is/application/core' import { information } from '../../../lib/messages' import { MachinesWithTotalCount } from '@island.is/clients/work-machines' +import { Application } from '@island.is/application/types' export const pickMachineSubSection = buildSubSection({ id: 'pickMachine', @@ -28,7 +29,11 @@ export const pickMachineSubSection = buildSubSection({ ) as MachinesWithTotalCount return machines.totalCount <= 5 }, - + defaultValue: (application: Application) => { + const machineList = application?.externalData.machinesList + .data as MachinesWithTotalCount + return machineList?.machines[0].id ?? '' + }, options: (application) => { const machineList = application?.externalData.machinesList .data as MachinesWithTotalCount diff --git a/libs/application/templates/aosh/request-for-inspection/src/forms/RequestMachineForInspectionForm/InformationSection/pickMachineSubSection.ts b/libs/application/templates/aosh/request-for-inspection/src/forms/RequestMachineForInspectionForm/InformationSection/pickMachineSubSection.ts index b36e78549f14..c3a63d2ad6fb 100644 --- a/libs/application/templates/aosh/request-for-inspection/src/forms/RequestMachineForInspectionForm/InformationSection/pickMachineSubSection.ts +++ b/libs/application/templates/aosh/request-for-inspection/src/forms/RequestMachineForInspectionForm/InformationSection/pickMachineSubSection.ts @@ -8,6 +8,7 @@ import { } from '@island.is/application/core' import { information } from '../../../lib/messages' import { MachinesWithTotalCount } from '@island.is/clients/work-machines' +import { Application } from '@island.is/application/types' export const pickMachineSubSection = buildSubSection({ id: 'pickMachine', @@ -36,7 +37,11 @@ export const pickMachineSubSection = buildSubSection({ return machines.totalCount <= 5 }, - + defaultValue: (application: Application) => { + const machineList = application?.externalData.machinesList + .data as MachinesWithTotalCount + return machineList?.machines[0].id ?? '' + }, options: (application) => { const machineList = application?.externalData.machinesList .data as MachinesWithTotalCount diff --git a/libs/application/templates/aosh/street-registration/src/forms/StreetRegistrationForm/InformationSection/pickMachineSubSection.ts b/libs/application/templates/aosh/street-registration/src/forms/StreetRegistrationForm/InformationSection/pickMachineSubSection.ts index 4e5c60c870ab..b59d0ab2c99b 100644 --- a/libs/application/templates/aosh/street-registration/src/forms/StreetRegistrationForm/InformationSection/pickMachineSubSection.ts +++ b/libs/application/templates/aosh/street-registration/src/forms/StreetRegistrationForm/InformationSection/pickMachineSubSection.ts @@ -9,6 +9,7 @@ import { information } from '../../../lib/messages' import { MachinesWithTotalCount } from '@island.is/clients/work-machines' import { mustInspectBeforeStreetRegistration } from '../../../utils/getSelectedMachine' import { useLocale } from '@island.is/localization' +import { Application } from '@island.is/application/types' export const pickMachineSubSection = buildSubSection({ id: 'pickMachine', @@ -30,7 +31,11 @@ export const pickMachineSubSection = buildSubSection({ ) as MachinesWithTotalCount return machines.totalCount <= 5 }, - + defaultValue: (application: Application) => { + const machineList = application?.externalData.machinesList + .data as MachinesWithTotalCount + return machineList?.machines[0].id ?? '' + }, options: (application) => { const machineList = application?.externalData.machinesList .data as MachinesWithTotalCount diff --git a/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/pickMachineSubSection.ts b/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/pickMachineSubSection.ts index cc875ba4df91..a5717964b01a 100644 --- a/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/pickMachineSubSection.ts +++ b/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/pickMachineSubSection.ts @@ -7,6 +7,7 @@ import { } from '@island.is/application/core' import { information } from '../../../lib/messages' import { MachinesWithTotalCount } from '@island.is/clients/work-machines' +import { Application } from '@island.is/application/types' export const pickMachineSubSection = buildSubSection({ id: 'pickMachine', @@ -29,7 +30,11 @@ export const pickMachineSubSection = buildSubSection({ return machines.totalCount <= 5 }, - + defaultValue: (application: Application) => { + const machineList = application?.externalData.machinesList + .data as MachinesWithTotalCount + return machineList?.machines[0].id ?? '' + }, options: (application) => { const machineList = application?.externalData.machinesList .data as MachinesWithTotalCount diff --git a/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx b/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx index b2d8da4456eb..ed9c36f38bc4 100644 --- a/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx +++ b/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx @@ -461,7 +461,7 @@ export const CalculateShare: FC> = ({ title={m.netProperty} value={roundedValueToNumber(netTotal)} /> - + {deceasedWasInCohabitation && ( + ) : customField.id === 'taxFreeInheritance' && - currentHeir?.relation !== - PrePaidHeirsRelationTypes.SPOUSE ? null : ( + ((values.applicationFor === PREPAID_INHERITANCE && + currentHeir?.relation !== RelationSpouse) || + values.applicationFor === ESTATE_INHERITANCE) ? null : ( { return x.relation @@ -66,7 +67,7 @@ export const HeirsAndPartitionRepeater: FC< hasForeignCitizenship && birthDate ? intervalToDuration({ start: new Date(birthDate), end: new Date() }) ?.years - : kennitala.info(member.nationalId)?.age + : info(member.nationalId)?.age return ( (memberAge ?? 0) < 18 && (member?.nationalId || birthDate) && @@ -78,7 +79,7 @@ export const HeirsAndPartitionRepeater: FC< const hasEstateMemberUnder18withoutRep = values.estate?.estateMembers?.some( (member: EstateMember) => { const advocateAge = - member.advocate && kennitala.info(member.advocate.nationalId)?.age + member.advocate && info(member.advocate.nationalId)?.age return ( hasEstateMemberUnder18 && member?.advocate?.nationalId && @@ -106,34 +107,29 @@ export const HeirsAndPartitionRepeater: FC< return [true, null] }) - const { clearErrors } = useFormContext() - const externalData = application.externalData.syslumennOnEntry?.data as { relationOptions: string[] inheritanceReportInfos: Array } - const estateData = - answers.applicationFor === PREPAID_INHERITANCE - ? undefined - : getEstateDataFromApplication(application) - - const inheritanceTaxFreeLimit = - answers.applicationFor === PREPAID_INHERITANCE - ? 0 - : externalData?.inheritanceReportInfos?.[0]?.inheritanceTax - ?.taxExemptionLimit ?? DEFAULT_TAX_FREE_LIMIT - - const relations = - answers.applicationFor === PREPAID_INHERITANCE - ? PrePaidHeirsRelations.map((relation) => ({ - value: relation.value, - label: formatMessage(relation.label), - })) - : externalData?.relationOptions?.map((relation) => ({ - value: relation, - label: relation, - })) || [] + const estateData = isPrePaidApplication + ? undefined + : getEstateDataFromApplication(application) + + const inheritanceTaxFreeLimit = isPrePaidApplication + ? 0 + : externalData?.inheritanceReportInfos?.[0]?.inheritanceTax + ?.taxExemptionLimit ?? DEFAULT_TAX_FREE_LIMIT + + const relations = isPrePaidApplication + ? PrePaidHeirsRelations.map((relation) => ({ + value: formatMessage(relation.label), + label: formatMessage(relation.label), + })) + : externalData?.relationOptions?.map((relation) => ({ + value: relation, + label: relation, + })) || [] const error = ((errors as any)?.heirs?.data || (errors as any)?.heirs?.total) ?? [] @@ -177,11 +173,10 @@ export const HeirsAndPartitionRepeater: FC< const updateValues = useCallback( (updateIndex: string, value: number, index?: number) => { - const isPrePaid = answers.applicationFor === PREPAID_INHERITANCE const numValue = isNaN(value) ? 0 : value const percentage = numValue > 0 ? numValue / 100 : 0 const heirs = getValues()?.heirs?.data as EstateMember[] - let currentHeir = isPrePaid + let currentHeir = isPrePaidApplication ? heirs[index ?? 0] : (getValueViaPath(answers, updateIndex) as EstateMember) @@ -195,20 +190,15 @@ export const HeirsAndPartitionRepeater: FC< // currently we can only check if heir is spouse by relation string value... const spouse = (heirs ?? []).filter( - (heir) => - heir.enabled && - (heir.relation === 'Maki' || - heir.relation.toLowerCase() === 'spouse'), + (heir) => heir.enabled && heir.relation === RelationSpouse, ) let isSpouse = false // it is not possible to select more than one spouse but for now we will check for it anyway if (spouse.length > 0) { - if (isPrePaid) { - isSpouse = - currentHeir?.relation === 'Maki' || - currentHeir?.relation.toLowerCase() === 'spouse' + if (isPrePaidApplication) { + isSpouse = currentHeir?.relation === RelationSpouse } else { spouse.forEach((currentSpouse) => { isSpouse = @@ -217,7 +207,7 @@ export const HeirsAndPartitionRepeater: FC< } } - const netPropertyForExchange = isPrePaid + const netPropertyForExchange = isPrePaidApplication ? getPrePaidTotalValueFromApplication(application) : valueToNumber(getValueViaPath(answers, 'netPropertyForExchange')) @@ -268,11 +258,6 @@ export const HeirsAndPartitionRepeater: FC< initialLoad() }, [heirsRelations, initialLoad]) - useEffect(() => { - initialLoad() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - useEffect(() => { if (!hasEstateMemberUnder18) { clearErrors(heirAgeValidation) @@ -297,24 +282,21 @@ export const HeirsAndPartitionRepeater: FC< (estateData as any)?.inheritanceReportInfo?.heirs && !(application.answers as any)?.heirs?.hasModified ) { - // Keeping this in for now, it may not be needed, will find out later const heirsData = (estateData as any)?.inheritanceReportInfo?.heirs?.map( (heir: any) => { return { ...heir, - phone: heir.phone ? formatPhoneNumber(heir.phone) : '', //Remove all non-digit characters and keep the last 7 digits + phone: heir.phone ? formatPhoneNumber(heir.phone) : '', initial: true, enabled: true, } }, ) - // ran into a problem with "append", as it appeared to be getting called multiple times - // despite checking on the length of the fields - // so now using "replace" instead, for the initial setup + // ran into a problem with "append" as it appeared to be called multiple times + // using "replace" instead, for the initial setup replace(heirsData) setValue('heirs.hasModified', true) } - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( @@ -595,7 +577,6 @@ export const HeirsAndPartitionRepeater: FC< [] as JSX.Element[], )} {fields.map((member: GenericFormField, index) => { - console.log(error, 'errrrrr') if (member.initial) return null return ( diff --git a/libs/application/templates/inheritance-report/src/fields/HeirsOverview/index.tsx b/libs/application/templates/inheritance-report/src/fields/HeirsOverview/index.tsx index b46ca5a60094..c3fa6e5ec163 100644 --- a/libs/application/templates/inheritance-report/src/fields/HeirsOverview/index.tsx +++ b/libs/application/templates/inheritance-report/src/fields/HeirsOverview/index.tsx @@ -5,16 +5,27 @@ import { FieldBaseProps } from '@island.is/application/types' import { InheritanceReport } from '../../lib/dataSchema' import { m } from '../../lib/messages' import { formatCurrency } from '@island.is/application/ui-components' +import { format as formatNationalId } from 'kennitala' +import { + ESTATE_INHERITANCE, + PREPAID_INHERITANCE, + RelationSpouse, +} from '../../lib/constants' export const HeirsOverview: FC> = ({ application, }) => { - const heirs = (application.answers as InheritanceReport).heirs?.data + const { answers } = application + const heirs = (answers as InheritanceReport).heirs?.data const { formatMessage } = useLocale() return ( {heirs?.map((heir, index) => { + const showTaxFree = + answers.applicationFor === ESTATE_INHERITANCE || + (answers.applicationFor === PREPAID_INHERITANCE && + heir.relation === RelationSpouse) if (!heir.enabled) return null return ( @@ -25,7 +36,7 @@ export const HeirsOverview: FC> = ({ {formatMessage(m.nationalId)} - {heir.nationalId} + {formatNationalId(heir.nationalId ?? '')} {formatMessage(m.name)} @@ -55,24 +66,45 @@ export const HeirsOverview: FC> = ({ - - {formatMessage(m.taxFreeInheritance)} - - {formatCurrency(String(heir.taxFreeInheritance || '0'))} - - {formatMessage(m.inheritanceAmount)} {formatCurrency(String(heir.inheritance || '0'))} + {showTaxFree ? ( + + + {formatMessage(m.taxFreeInheritance)} + + + {formatCurrency(String(heir.taxFreeInheritance || '0'))} + + + ) : ( + + + {formatMessage(m.taxableInheritance)} + + + {formatCurrency(String(heir.taxableInheritance || '0'))} + + + )} - - - {formatMessage(m.taxableInheritance)} - - {formatCurrency(String(heir.taxableInheritance || '0'))} - - + + {showTaxFree && ( + + + {formatMessage(m.taxableInheritance)} + + + {formatCurrency(String(heir.taxableInheritance || '0'))} + + + )} {formatMessage(m.inheritanceTax)} @@ -90,7 +122,7 @@ export const HeirsOverview: FC> = ({ {formatMessage(m.advocateNationalId)} - {heir.advocate.nationalId} + {formatNationalId(heir.advocate.nationalId)} {formatMessage(m.advocateName)} diff --git a/libs/application/templates/inheritance-report/src/forms/form.ts b/libs/application/templates/inheritance-report/src/forms/form.ts index f2ea9423ec97..eff072326647 100644 --- a/libs/application/templates/inheritance-report/src/forms/form.ts +++ b/libs/application/templates/inheritance-report/src/forms/form.ts @@ -1,13 +1,5 @@ -import { - YES, - buildCheckboxField, - buildForm, - buildMultiField, - buildSection, - buildSubmitField, -} from '@island.is/application/core' -import { m } from '../lib/messages' -import { DefaultEvents, Form, FormModes } from '@island.is/application/types' +import { buildForm } from '@island.is/application/core' +import { Form, FormModes } from '@island.is/application/types' import { assets } from './sections/assets' import { debtsAndFuneralCost } from './sections/debtsAndFuneralCost' import { heirs } from './sections/heirs' @@ -19,7 +11,6 @@ import { preSelection } from './sections/applicationTypeSelection' import { prePaidHeirs } from './sections/prepaidInheritance/heirs' import { inheritanceExecutor } from './sections/prepaidInheritance/inheritanceExecutor' import { inheritance } from './sections/prepaidInheritance/inheritance' -import { prepaidOverview } from './sections/prepaidInheritance/overview' import { finalStep } from './sections/finalStep' import { prePaidApplicant } from './sections/prepaidInheritance/applicant' @@ -35,7 +26,6 @@ export const prepaidInheritanceForm: Form = buildForm({ inheritance, assets, prePaidHeirs, - prepaidOverview, finalStep, ], }) diff --git a/libs/application/templates/inheritance-report/src/forms/prerequisites.ts b/libs/application/templates/inheritance-report/src/forms/prerequisites.ts index 4553132dc521..52d7119c5e8a 100644 --- a/libs/application/templates/inheritance-report/src/forms/prerequisites.ts +++ b/libs/application/templates/inheritance-report/src/forms/prerequisites.ts @@ -22,12 +22,14 @@ export const getForm = (): Form => children: [ buildMultiField({ title: m.preDataCollectionApplicationFor, + description: m.preDataCollectionApplicationFoDescription, children: [ buildRadioField({ id: 'applicationFor', title: '', largeButtons: true, backgroundColor: 'blue', + required: true, options: [ { value: ESTATE_INHERITANCE, diff --git a/libs/application/templates/inheritance-report/src/forms/sections/applicationTypeSelection.ts b/libs/application/templates/inheritance-report/src/forms/sections/applicationTypeSelection.ts index bd94b718869a..10ffd1e7f6fd 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/applicationTypeSelection.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/applicationTypeSelection.ts @@ -48,7 +48,7 @@ export const preSelection = buildSection({ application.externalData.syslumennOnEntry?.data as { inheritanceReportInfos: Array } - ).inheritanceReportInfos[0].caseNumber + ).inheritanceReportInfos[0]?.caseNumber }, options: (application) => { return ( diff --git a/libs/application/templates/inheritance-report/src/forms/sections/assets.ts b/libs/application/templates/inheritance-report/src/forms/sections/assets.ts index 9b245f151b43..fa399ae78f89 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/assets.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/assets.ts @@ -33,7 +33,9 @@ export const assets = buildSection({ description: (application) => application.answers.applicationFor === PREPAID_INHERITANCE ? m.propertiesDescriptionPrePaid - : m.propertiesDescription + ' ' + m.continueWithoutAssets, + : m.propertiesDescription.defaultMessage + + ' ' + + m.continueWithoutAssets.defaultMessage, children: [ buildDescriptionField({ id: 'realEstateTitle', @@ -175,7 +177,9 @@ export const assets = buildSection({ id: 'vehicles', title: m.propertiesTitle, description: - m.propertiesDescription + ' ' + m.continueWithoutVehicles, + m.propertiesDescription.defaultMessage + + ' ' + + m.continueWithoutVehicles.defaultMessage, children: [ buildDescriptionField({ id: 'vehiclesTitle', @@ -310,7 +314,9 @@ export const assets = buildSection({ id: 'estateBankInfo', title: m.propertiesTitle, description: - m.propertiesDescription + ' ' + m.continueWithoutBankAccounts, + m.propertiesDescription.defaultMessage + + ' ' + + m.continueWithoutBankAccounts.defaultMessage, children: [ buildDescriptionField({ id: 'estateBankInfoTitle', @@ -463,7 +469,9 @@ export const assets = buildSection({ description: (application) => application.answers.applicationFor === PREPAID_INHERITANCE ? m.propertiesDescriptionPrePaid - : m.propertiesDescription + ' ' + m.continueWithoutBankAccounts, + : m.propertiesDescription.defaultMessage + + ' ' + + m.continueWithoutBankAccounts.defaultMessage, children: [ buildDescriptionField({ id: 'stocksTitle', @@ -549,7 +557,9 @@ export const assets = buildSection({ description: (application) => application.answers.applicationFor === PREPAID_INHERITANCE ? m.propertiesDescriptionPrePaid - : m.propertiesDescription + ' ' + m.continueWithoutBankAccounts, + : m.propertiesDescription.defaultMessage + + ' ' + + m.continueWithoutBankAccounts.defaultMessage, children: [ buildDescriptionField({ id: 'moneyTitle', @@ -630,7 +640,9 @@ export const assets = buildSection({ description: (application) => application.answers.applicationFor === PREPAID_INHERITANCE ? m.propertiesDescriptionPrePaid - : m.propertiesDescription + ' ' + m.continueWithoutBankAccounts, + : m.propertiesDescription.defaultMessage + + ' ' + + m.continueWithoutBankAccounts.defaultMessage, children: [ buildDescriptionField({ id: 'otherAssetsTitle', @@ -676,7 +688,6 @@ export const assets = buildSection({ buildSubSection({ id: 'assetOverview', title: m.assetOverview, - condition: (answers) => answers.applicationFor !== PREPAID_INHERITANCE, children: [ buildCustomField({ title: m.assetOverview, diff --git a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/heirs.ts b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/heirs.ts index 2bd8d8f92df7..49aa3ce4705e 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/heirs.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/heirs.ts @@ -8,6 +8,7 @@ import { buildTextField, } from '@island.is/application/core' import { m } from '../../../lib/messages' +import { prepaidOverview } from './overview' export const prePaidHeirs = buildSection({ id: 'heirs', @@ -117,5 +118,6 @@ export const prePaidHeirs = buildSection({ }), ], }), + prepaidOverview, ], }) diff --git a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts index 90377cf923b9..f66bfb20b3a6 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/inheritanceExecutor.ts @@ -9,19 +9,15 @@ import { YES, } from '@island.is/application/core' import { m } from '../../../lib/messages' -import { format as formatNationalId } from 'kennitala' -import { Application, UserProfile } from '@island.is/api/schema' -import { removeCountryCode } from '@island.is/application/ui-components' export const inheritanceExecutor = buildSection({ id: 'inheritanceExecutor', - title: 'Arflátar', + title: m.grantors, children: [ buildMultiField({ id: 'inheritanceExecutor', - title: 'Arflátar', - description: - 'Lorem ipsum foo bar beep boop meep morp lorem ipsum foo bar beep boop meep morp lorem ipsum foo bar beep boop meep morp.', + title: m.grantors, + description: m.grantorsDescription, children: [ buildDescriptionField({ id: 'description.executors.executor', diff --git a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/overview.ts b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/overview.ts index 1c8ade9c302f..511a00205550 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/overview.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/prepaidInheritance/overview.ts @@ -4,202 +4,173 @@ import { buildDividerField, buildKeyValueField, buildMultiField, - buildSection, buildSubSection, getValueViaPath, } from '@island.is/application/core' import { m } from '../../../lib/messages' import { formatCurrency } from '@island.is/application/ui-components' import { InheritanceReport } from '../../../lib/dataSchema' -import { roundedValueToNumber, valueToNumber } from '../../../lib/utils/helpers' +import { + roundedValueToNumber, + showTaxFreeInOverview, + valueToNumber, +} from '../../../lib/utils/helpers' import { calculateTotalAssets } from '../../../lib/utils/calculateTotalAssets' -export const prepaidOverview = buildSection({ +export const prepaidOverview = buildSubSection({ id: 'prepaidOverview', title: m.overview, children: [ - buildSubSection({ - id: 'assetOverview', - title: m.assetOverview, + buildMultiField({ + id: 'heirsOverview', + title: m.overviewHeirsTitle, + description: m.overviewHeirsDescription, children: [ + buildDividerField({}), + buildDescriptionField({ + id: 'overviewAssetsTitle', + title: m.properties, + titleVariant: 'h3', + space: 'gutter', + marginBottom: 'gutter', + }), + buildKeyValueField({ + label: m.netProperty, + display: 'flex', + value: ({ answers }) => + formatCurrency( + String(roundedValueToNumber(calculateTotalAssets(answers))), + ), + }), + buildDescriptionField({ + id: 'space', + title: '', + space: 'gutter', + }), + buildDividerField({}), buildCustomField({ - title: m.assetOverview, - description: m.assetOverviewDescription, - id: 'overviewAssets', + title: '', + id: 'overviewHeirs', doesNotRequireAnswer: true, - component: 'OverviewAssets', + component: 'HeirsOverview', }), - ], - }), - buildSubSection({ - id: 'heirsOverview', - title: m.overviewHeirsTitle, - children: [ - buildMultiField({ - id: 'heirsOverview', - title: m.overviewHeirsTitle, - description: m.overviewHeirsDescription, - children: [ - buildDescriptionField({ - id: 'overviewAssetsTitle', - title: m.properties, - titleVariant: 'h3', - space: 'gutter', - marginBottom: 'gutter', - }), - buildDividerField({}), - buildKeyValueField({ - label: m.totalValueOfAssets, - display: 'flex', - value: ({ answers }) => - formatCurrency( - String(roundedValueToNumber(calculateTotalAssets(answers))), - ), - }), - buildDescriptionField({ - id: 'space', - title: '', - space: 'gutter', - }), - buildDividerField({}), - buildDescriptionField({ - id: 'overviewHeirsTitle', - title: m.heirs, - titleVariant: 'h3', - space: 'gutter', - marginBottom: 'gutter', - }), - buildCustomField({ - title: '', - id: 'overviewHeirs', - doesNotRequireAnswer: true, - component: 'HeirsOverview', - }), - buildDividerField({}), - buildDescriptionField({ - id: 'overviewTotalInheritance', - title: m.overviewTotalInheritance, - titleVariant: 'h3', - space: 'gutter', - marginBottom: 'gutter', - }), - buildKeyValueField({ - label: m.heirsInheritanceRate, - display: 'flex', - value: ({ answers }) => - String(getValueViaPath(answers, 'heirs.total')), - }), - buildDescriptionField({ - id: 'heirs_space1', - title: '', - space: 'gutter', - }), - buildKeyValueField({ - label: m.inheritanceAmount, - display: 'flex', - value: ({ answers }) => { - const total = ( - answers as InheritanceReport - )?.heirs?.data?.reduce( - (sum, heir) => sum + valueToNumber(heir.inheritance), - 0, - ) + buildDividerField({}), + buildDescriptionField({ + id: 'overviewTotalInheritance', + title: m.overviewTotalInheritance, + titleVariant: 'h3', + space: 'gutter', + marginBottom: 'gutter', + }), + buildKeyValueField({ + label: m.heirsInheritanceRate, + display: 'flex', + value: ({ answers }) => + String(getValueViaPath(answers, 'heirs.total')), + }), + buildDescriptionField({ + id: 'heirs_space1', + title: '', + space: 'gutter', + }), + buildKeyValueField({ + label: m.inheritanceAmount, + display: 'flex', + value: ({ answers }) => { + const total = (answers as InheritanceReport)?.heirs?.data?.reduce( + (sum, heir) => sum + valueToNumber(heir.inheritance), + 0, + ) - return formatCurrency(String(total ?? '0')) - }, - }), - buildDescriptionField({ - id: 'heirs_space2', - title: '', - space: 'gutter', - }), - buildKeyValueField({ - label: m.taxFreeInheritance, - display: 'flex', - value: ({ answers }) => { - const total = ( - answers as InheritanceReport - )?.heirs?.data?.reduce( - (sum, heir) => sum + valueToNumber(heir.taxFreeInheritance), - 0, - ) + return formatCurrency(String(total ?? '0')) + }, + }), + buildDescriptionField({ + id: 'heirs_space2', + title: '', + space: 'gutter', + }), + buildKeyValueField({ + label: m.taxFreeInheritance, + display: 'flex', + condition: showTaxFreeInOverview, + value: ({ answers }) => { + const total = (answers as InheritanceReport)?.heirs?.data?.reduce( + (sum, heir) => sum + valueToNumber(heir.taxFreeInheritance), + 0, + ) - return formatCurrency(String(total ?? '0')) - }, - }), - buildDescriptionField({ - id: 'heirs_space3', - title: '', - space: 'gutter', - }), - buildKeyValueField({ - label: m.taxableInheritance, - display: 'flex', - value: ({ answers }) => { - const total = ( - answers as InheritanceReport - )?.heirs?.data?.reduce( - (sum, heir) => sum + valueToNumber(heir.taxableInheritance), - 0, - ) + return formatCurrency(String(total ?? '0')) + }, + }), + buildDescriptionField({ + id: 'heirs_space3', + title: '', + space: 'gutter', + condition: showTaxFreeInOverview, + }), + buildKeyValueField({ + label: m.taxableInheritance, + display: 'flex', + value: ({ answers }) => { + const total = (answers as InheritanceReport)?.heirs?.data?.reduce( + (sum, heir) => sum + valueToNumber(heir.taxableInheritance), + 0, + ) - return formatCurrency(String(total ?? '0')) - }, - }), - buildDescriptionField({ - id: 'heirs_space4', - title: '', - space: 'gutter', - }), - buildKeyValueField({ - label: m.inheritanceTax, - display: 'flex', - value: ({ answers }) => { - const total = ( - answers as InheritanceReport - )?.heirs?.data?.reduce( - (sum, heir) => sum + valueToNumber(heir.inheritanceTax), - 0, - ) + return formatCurrency(String(total ?? '0')) + }, + }), + buildDescriptionField({ + id: 'heirs_space4', + title: '', + space: 'gutter', + }), + buildKeyValueField({ + label: m.inheritanceTax, + display: 'flex', + value: ({ answers }) => { + const total = (answers as InheritanceReport)?.heirs?.data?.reduce( + (sum, heir) => sum + valueToNumber(heir.inheritanceTax), + 0, + ) - return formatCurrency(String(total ?? '0')) - }, - }), - buildDividerField({}), - buildDescriptionField({ - id: 'overviewAdditionalInfo', - title: m.heirAdditionalInfo, - titleVariant: 'h3', - space: 'gutter', - marginBottom: 'gutter', - }), - buildKeyValueField({ - label: m.info, - value: ({ answers }) => - getValueViaPath(answers, 'heirsAdditionalInfo'), - }), - buildDescriptionField({ - id: 'heirs_space5', - title: '', - space: 'gutter', - }), - buildKeyValueField({ - label: m.fileUploadOtherDocumentsPrePaid, - value: ({ answers }) => { - const files = getValueViaPath( - answers, - 'heirsAdditionalInfoFilesOtherDocuments', - ) - return files.map((file: any) => file.name).join(', ') - }, - }), - buildCustomField({ - title: '', - id: 'overviewPrint', - doesNotRequireAnswer: true, - component: 'PrintScreen', - }), - ], + return formatCurrency(String(total ?? '0')) + }, + }), + buildDividerField({}), + buildDescriptionField({ + id: 'overviewAdditionalInfo', + title: m.heirAdditionalInfo, + titleVariant: 'h3', + space: 'gutter', + marginBottom: 'gutter', + }), + buildKeyValueField({ + label: m.info, + value: ({ answers }) => + getValueViaPath(answers, 'heirsAdditionalInfo'), + }), + buildDescriptionField({ + id: 'heirs_space5', + title: '', + space: 'gutter', + }), + buildKeyValueField({ + label: m.fileUploadOtherDocumentsPrePaid, + value: ({ answers }) => { + const files = getValueViaPath( + answers, + 'heirsAdditionalInfoFilesOtherDocuments', + ) + return files.map((file: any) => file.name).join(', ') + }, + }), + buildCustomField({ + title: '', + id: 'overviewPrint', + doesNotRequireAnswer: true, + component: 'PrintScreen', }), ], }), diff --git a/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts b/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts index 8b31f6cfd59a..f6c0854ebefc 100644 --- a/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts +++ b/libs/application/templates/inheritance-report/src/lib/InheritanceReportTemplate.ts @@ -18,6 +18,7 @@ import { m } from './messages' import { inheritanceReportSchema } from './dataSchema' import { ApiActions, + ESTATE_INHERITANCE, InheritanceReportEvent, PREPAID_INHERITANCE, Roles, @@ -37,6 +38,10 @@ const InheritanceReportTemplate: ApplicationTemplate< ? m.prerequisitesTitle.defaultMessage + ' - ' + m.applicationNamePrepaid.defaultMessage + : answers.applicationFor === ESTATE_INHERITANCE + ? m.prerequisitesTitle.defaultMessage + + ' - ' + + m.applicationNameEstate.defaultMessage : m.prerequisitesTitle.defaultMessage, institution: m.institution, dataSchema: inheritanceReportSchema, @@ -129,10 +134,10 @@ const InheritanceReportTemplate: ApplicationTemplate< status: 'approved', progress: 1, lifecycle: EphemeralStateLifeCycle, - /*onEntry: defineTemplateApi({ + onEntry: defineTemplateApi({ action: ApiActions.completeApplication, throwOnError: true, - }),*/ + }), roles: [ { id: Roles.ESTATE_INHERITANCE_APPLICANT, diff --git a/libs/application/templates/inheritance-report/src/lib/constants.ts b/libs/application/templates/inheritance-report/src/lib/constants.ts index 7567ad223dff..796e0cf6dcb5 100644 --- a/libs/application/templates/inheritance-report/src/lib/constants.ts +++ b/libs/application/templates/inheritance-report/src/lib/constants.ts @@ -18,6 +18,8 @@ export enum PrePaidHeirsRelationTypes { OTHER = 'other', } +export const RelationSpouse = 'Maki' + export const PrePaidHeirsRelations = [ { value: PrePaidHeirsRelationTypes.SPOUSE, diff --git a/libs/application/templates/inheritance-report/src/lib/dataSchema.ts b/libs/application/templates/inheritance-report/src/lib/dataSchema.ts index 885c4487f192..8c0c5801cb48 100644 --- a/libs/application/templates/inheritance-report/src/lib/dataSchema.ts +++ b/libs/application/templates/inheritance-report/src/lib/dataSchema.ts @@ -9,6 +9,11 @@ import { } from './utils/helpers' import { m } from './messages' import { NO, YES } from '@island.is/application/core' +import { + ESTATE_INHERITANCE, + PREPAID_INHERITANCE, + RelationSpouse, +} from './constants' const deceasedShare = { deceasedShare: z.string().nonempty().optional(), @@ -167,6 +172,8 @@ export const inheritanceReportSchema = z.object({ }, ), + applicationFor: z.enum([ESTATE_INHERITANCE, PREPAID_INHERITANCE]), + /* assets */ assets: z.object({ realEstate: assetWithShare, @@ -567,8 +574,7 @@ export const inheritanceReportSchema = z.object({ (v) => { if (v.length > 0) { const count = v.filter( - (x) => - x.enabled && (x.relation === 'Maki' || x.relation === 'Spouse'), + (x) => x.enabled && x.relation === RelationSpouse, )?.length return count <= 1 } diff --git a/libs/application/templates/inheritance-report/src/lib/messages.ts b/libs/application/templates/inheritance-report/src/lib/messages.ts index 28081cad37bd..a7ebde5021bc 100644 --- a/libs/application/templates/inheritance-report/src/lib/messages.ts +++ b/libs/application/templates/inheritance-report/src/lib/messages.ts @@ -45,6 +45,12 @@ export const m = defineMessages({ defaultMessage: 'Tegund umsóknar', description: 'Get application for', }, + preDataCollectionApplicationFoDescription: { + id: 'ir.application:preDataCollectionApplicationFoDescription#markdown', + defaultMessage: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a ex magna. Aenean varius dui eget consectetur posuere. Pellentesque dignissim nunc id euismod porttitor. Curabitur ut ante pellentesque, rutrum purus in, vestibulum elit. Donec egestas lacus justo.', + description: 'Get application for', + }, preDataCollectionApplicationForPrepaid: { id: 'ir.application:preDataCollectionApplicationForPrepaid', defaultMessage: 'Fyrirframgreiddur arfur', @@ -66,6 +72,11 @@ export const m = defineMessages({ defaultMessage: 'Fyrirframgreiddur arfur', description: '', }, + applicationNameEstate: { + id: 'ir.application:applicationNameEstate', + defaultMessage: 'Dánarbú', + description: '', + }, institutionName: { id: 'ir.application:institution.name', defaultMessage: 'Sýslumenn', @@ -1463,6 +1474,16 @@ export const m = defineMessages({ defaultMessage: 'Skiptastjóri', description: '', }, + grantors: { + id: 'ir.application:grantors', + defaultMessage: 'Arflátar', + description: '', + }, + grantorsDescription: { + id: 'ir.application:grantorsDescription', + defaultMessage: 'Lorem ipsum foo bar beep boop meep morp.', + description: '', + }, grantor: { id: 'ir.application:grantor', defaultMessage: 'Arfláti', @@ -1685,12 +1706,12 @@ export const m = defineMessages({ description: '', }, includeSpousePrePaid: { - id: 'id.application:includeSpouse', + id: 'ir.application:includeSpouse', defaultMessage: 'Ráðstafa úr sameign hjúskaps', description: '', }, includeSpousePrePaidDescription: { - id: 'id.application:includeSpouseDescription', + id: 'ir.application:includeSpouseDescription', defaultMessage: 'Ef arfláti er í gift/ur og ráðstafa á úr sameign, þarf maki að vera með sem arfláti', description: '', diff --git a/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts b/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts index 108cbb84f09c..c5769bedce58 100644 --- a/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts +++ b/libs/application/templates/inheritance-report/src/lib/utils/helpers.ts @@ -8,9 +8,10 @@ import { import { InheritanceReportInfo } from '@island.is/clients/syslumenn' import { parsePhoneNumberFromString } from 'libphonenumber-js' import { MessageDescriptor } from 'react-intl' -import { boolean, ZodTypeAny } from 'zod' +import { ZodTypeAny } from 'zod' import { Answers } from '../../types' import { ESTATE_INHERITANCE } from '../constants' +import { InheritanceReport } from '../dataSchema' export const currencyStringToNumber = (str: string) => { if (!str) { @@ -89,15 +90,17 @@ export const getPrePaidTotalValueFromApplication = ( const money = valueToNumber( getValueViaPath(answers, 'assets.money.value', '0'), ) - const vehicleTotal = - getValueViaPath(answers, 'assets.vehicles.total', 0) ?? 0 + const stocksTotal = + getValueViaPath(answers, 'assets.stocks.total', 0) ?? 0 const realEstateTotal = getValueViaPath(answers, 'assets.realEstate.total', 0) ?? 0 - const otherTotal = + const otherAssetsTotal = getValueViaPath(answers, 'assets.otherAssets.total', 0) ?? 0 const bankAccountTotal = getValueViaPath(answers, 'assets.bankAccounts.total', 0) ?? 0 - return money + vehicleTotal + realEstateTotal + otherTotal + bankAccountTotal + return ( + money + stocksTotal + realEstateTotal + otherAssetsTotal + bankAccountTotal + ) } export const customZodError = ( @@ -201,3 +204,11 @@ export const shouldShowCustomSpouseShare = (answers: FormValue) => export const roundedValueToNumber = (value: unknown) => Math.round(valueToNumber(value)) + +export const showTaxFreeInOverview = (answers: FormValue) => { + const total = (answers as InheritanceReport)?.heirs?.data?.reduce( + (sum, heir) => sum + valueToNumber(heir.taxFreeInheritance), + 0, + ) + return !!total && total > 0 +} diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index 51eedf9ed822..945b9202707d 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -232,6 +232,7 @@ export interface CheckboxField extends BaseField { required?: boolean backgroundColor?: InputBackgroundColor onSelect?: ((s: string[]) => void) | undefined + spacing?: 0 | 1 | 2 } export interface DateField extends BaseField { diff --git a/libs/application/types/src/lib/Form.ts b/libs/application/types/src/lib/Form.ts index 808b7803c316..d0af617c7a7e 100644 --- a/libs/application/types/src/lib/Form.ts +++ b/libs/application/types/src/lib/Form.ts @@ -86,6 +86,7 @@ export interface FormItem extends TestSupport { condition?: Condition readonly type: string readonly title: FormText + readonly nextButtonText?: FormText } export interface Section extends FormItem { diff --git a/libs/application/ui-fields/src/lib/CheckboxFormField/CheckboxFormField.tsx b/libs/application/ui-fields/src/lib/CheckboxFormField/CheckboxFormField.tsx index acd393180915..136d978de23a 100644 --- a/libs/application/ui-fields/src/lib/CheckboxFormField/CheckboxFormField.tsx +++ b/libs/application/ui-fields/src/lib/CheckboxFormField/CheckboxFormField.tsx @@ -38,6 +38,7 @@ export const CheckboxFormField: FC> = ({ width, required, onSelect, + spacing, } = field const { formatMessage } = useLocale() @@ -76,6 +77,7 @@ export const CheckboxFormField: FC> = ({ } strong={strong} error={error} + spacing={spacing} options={finalOptions?.map( ({ label, subLabel, rightContent, tooltip, ...o }) => ({ ...o, diff --git a/libs/application/ui-shell/src/components/Screen.tsx b/libs/application/ui-shell/src/components/Screen.tsx index bc72e13c6a6a..850aa2b479a2 100644 --- a/libs/application/ui-shell/src/components/Screen.tsx +++ b/libs/application/ui-shell/src/components/Screen.tsx @@ -8,10 +8,13 @@ import React, { useState, } from 'react' import { ApolloError, useMutation } from '@apollo/client' -import { formatText, mergeAnswers } from '@island.is/application/core' +import { + coreMessages, + formatText, + mergeAnswers, +} from '@island.is/application/core' import { Application, - Answer, ExternalData, FormItemTypes, FormModes, @@ -19,6 +22,7 @@ import { Schema, BeforeSubmitCallback, Section, + FormText, } from '@island.is/application/types' import { Box, @@ -50,6 +54,7 @@ import FormExternalDataProvider from './FormExternalDataProvider' import { extractAnswersToSubmitFromScreen, findSubmitField } from '../utils' import ScreenFooter from './ScreenFooter' import RefetchContext from '../context/RefetchContext' +import { MessageDescriptor } from 'react-intl' type ScreenProps = { activeScreenIndex: number @@ -113,6 +118,7 @@ const Screen: FC> = ({ resolver({ formValue, context, formatMessage }), context: { dataSchema, formNode: screen }, }) + const [fieldLoadingState, setFieldLoadingState] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const refetch = useContext<() => void>(RefetchContext) @@ -258,6 +264,8 @@ const Screen: FC> = ({ const { width } = useWindowSize() const headerHeight = 85 + const nextButtonText = screen.nextButtonText ?? coreMessages.buttonNext + useEffect(() => { if (width < theme.breakpoints.md) { return setIsMobile(true) @@ -315,6 +323,7 @@ const Screen: FC> = ({ screen.type === FormItemTypes.REPEATER || screen.type === FormItemTypes.EXTERNAL_DATA_PROVIDER ) + return ( > = ({ - > = ({ submitField={submitField} loading={loading} canProceed={!isLoadingOrPending} + nextButtonText={nextButtonText} /> diff --git a/libs/application/ui-shell/src/components/ScreenFooter.tsx b/libs/application/ui-shell/src/components/ScreenFooter.tsx index 76bf164611a5..276f7a2c781a 100644 --- a/libs/application/ui-shell/src/components/ScreenFooter.tsx +++ b/libs/application/ui-shell/src/components/ScreenFooter.tsx @@ -7,6 +7,7 @@ import { FormModes, SubmitField, CallToAction, + FormText, } from '@island.is/application/types' import * as styles from './ScreenFooter.css' @@ -25,6 +26,7 @@ interface FooterProps { shouldLastScreenButtonSubmit?: boolean renderLastScreenBackButton?: boolean submitButtonDisabled?: boolean + nextButtonText?: FormText } type SubmitButton = Omit & { @@ -65,6 +67,7 @@ export const ScreenFooter: FC> = ({ renderLastScreenButton, renderLastScreenBackButton, submitButtonDisabled, + nextButtonText, }) => { const { formatMessage } = useLocale() const { userInfo: user } = useAuth() @@ -168,7 +171,9 @@ export const ScreenFooter: FC> = ({ type="submit" disabled={submitButtonDisabled} > - {formatMessage(coreMessages.buttonNext)} + {nextButtonText + ? formatText(nextButtonText, application, formatMessage) + : formatMessage(coreMessages.buttonNext)} )} diff --git a/libs/application/ui-shell/src/lib/FormShell.tsx b/libs/application/ui-shell/src/lib/FormShell.tsx index 0317de002a77..46d32069587f 100644 --- a/libs/application/ui-shell/src/lib/FormShell.tsx +++ b/libs/application/ui-shell/src/lib/FormShell.tsx @@ -1,5 +1,4 @@ import React, { FC, useEffect, useReducer, useState } from 'react' -import cn from 'classnames' import { Application, diff --git a/libs/clients/statistics/src/lib/statistics.service.ts b/libs/clients/statistics/src/lib/statistics.service.ts index b37d989ab770..4e2638cec32a 100644 --- a/libs/clients/statistics/src/lib/statistics.service.ts +++ b/libs/clients/statistics/src/lib/statistics.service.ts @@ -6,7 +6,8 @@ import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' import { LATEST_MEASUREMENT_KEY as LATEST_UV_MEASUREMENT_KEY, - MEASUREMENT_SERIES_KEY as UV_MEASUREMENT_SERIES_KEY, + MEASUREMENT_SERIES_PAST_72_HOURS_KEY as UV_MEASUREMENT_SERIES_PAST_72_HOURS_KEY, + MEASUREMENT_SERIES_PAST_YEAR_KEY as UV_MEASUREMENT_SERIES_PAST_YEAR_KEY, UltravioletRadiationClientService, } from '@island.is/clients/ultraviolet-radiation' @@ -42,8 +43,17 @@ export class StatisticsClientService { if (query.sourceDataKeys.includes(LATEST_UV_MEASUREMENT_KEY)) { promises.push(this.ultravioletRadiationService.getLatestMeasurement()) } - if (query.sourceDataKeys.includes(UV_MEASUREMENT_SERIES_KEY)) { - promises.push(this.ultravioletRadiationService.getMeasurementSeries()) + if ( + query.sourceDataKeys.includes(UV_MEASUREMENT_SERIES_PAST_72_HOURS_KEY) + ) { + promises.push( + this.ultravioletRadiationService.getMeasurementSeriesPast72Hours(), + ) + } + if (query.sourceDataKeys.includes(UV_MEASUREMENT_SERIES_PAST_YEAR_KEY)) { + promises.push( + this.ultravioletRadiationService.getMeasurementSeriesPastYear(), + ) } const [csvSourceData, ...rest] = await Promise.all(promises) diff --git a/libs/clients/ultraviolet-radiation/src/lib/ultraviolet-radiation.service.ts b/libs/clients/ultraviolet-radiation/src/lib/ultraviolet-radiation.service.ts index 7ed2db40ad54..5812cf9a17a8 100644 --- a/libs/clients/ultraviolet-radiation/src/lib/ultraviolet-radiation.service.ts +++ b/libs/clients/ultraviolet-radiation/src/lib/ultraviolet-radiation.service.ts @@ -7,8 +7,10 @@ import { export const LATEST_MEASUREMENT_KEY = 'icelandicRadiationSafetyAuthority.ultravioletRadiation.latestMeasurement' as const -export const MEASUREMENT_SERIES_KEY = - 'icelandicRadiationSafetyAuthority.ultravioletRadiation.measurementSeries' as const +export const MEASUREMENT_SERIES_PAST_72_HOURS_KEY = + 'icelandicRadiationSafetyAuthority.ultravioletRadiation.measurementSeriesPast72Hours' as const +export const MEASUREMENT_SERIES_PAST_YEAR_KEY = + 'icelandicRadiationSafetyAuthority.ultravioletRadiation.measurementSeriesPastYear' as const export const isValidMeasurement = ( item: InlineResponse2001BodyDataLatest | undefined, @@ -23,9 +25,8 @@ export class UltravioletRadiationClientService { async getLatestMeasurement(): Promise< StatisticSourceData > { - const response = await this.api.returnDailyUV() - const measurement = - response?.body?.dataLatest ?? response?.body?.dataAll?.at(-1) + const response = await this.api.returnHourlyUV() + const measurement = response?.body?.dataAll?.at(-1) if (!isValidMeasurement(measurement)) { return { data: { @@ -45,14 +46,29 @@ export class UltravioletRadiationClientService { } } - async getMeasurementSeries(): Promise< - StatisticSourceData + async getMeasurementSeriesPast72Hours(): Promise< + StatisticSourceData > { const response = await this.api.returnHourlyUV() const series = response.body?.dataAll?.filter(isValidMeasurement) ?? [] return { data: { - [MEASUREMENT_SERIES_KEY]: series.map((measurement) => ({ + [MEASUREMENT_SERIES_PAST_72_HOURS_KEY]: series.map((measurement) => ({ + header: String(Date.parse(measurement.time)), + value: measurement.uvVal, + })), + }, + } + } + + async getMeasurementSeriesPastYear(): Promise< + StatisticSourceData + > { + const response = await this.api.returnDailyUV() + const series = response.body?.dataAll?.filter(isValidMeasurement) ?? [] + return { + data: { + [MEASUREMENT_SERIES_PAST_YEAR_KEY]: series.map((measurement) => ({ header: String(Date.parse(measurement.time)), value: measurement.uvVal, })), diff --git a/libs/cms/src/lib/cms.elasticsearch.service.ts b/libs/cms/src/lib/cms.elasticsearch.service.ts index ec99f59ec227..8e90d69363c7 100644 --- a/libs/cms/src/lib/cms.elasticsearch.service.ts +++ b/libs/cms/src/lib/cms.elasticsearch.service.ts @@ -39,6 +39,8 @@ import { CustomPage } from './models/customPage.model' import { GetGenericListItemsInput } from './dto/getGenericListItems.input' import { GenericListItemResponse } from './models/genericListItemResponse.model' import { GetCustomSubpageInput } from './dto/getCustomSubpage.input' +import { GetGenericListItemBySlugInput } from './dto/getGenericListItemBySlug.input' +import { GenericListItem } from './models/genericListItem.model' @Injectable() export class CmsElasticsearchService { @@ -440,6 +442,15 @@ export class CmsElasticsearchService { ) } + async getGenericListItemBySlug( + input: GetGenericListItemBySlugInput, + ): Promise { + return this.getSingleDocumentTypeBySlug(getElasticsearchIndex(input.lang), { + slug: input.slug, + type: 'webGenericListItem', + }) + } + async getGenericListItems( input: GetGenericListItemsInput, ): Promise { diff --git a/libs/cms/src/lib/cms.resolver.ts b/libs/cms/src/lib/cms.resolver.ts index ebbbb2ce658e..4d47feba92e8 100644 --- a/libs/cms/src/lib/cms.resolver.ts +++ b/libs/cms/src/lib/cms.resolver.ts @@ -115,6 +115,8 @@ import { GenericListItemResponse } from './models/genericListItemResponse.model' import { GetGenericListItemsInput } from './dto/getGenericListItems.input' import { GenericList } from './models/genericList.model' import { GetCustomSubpageInput } from './dto/getCustomSubpage.input' +import { GetGenericListItemBySlugInput } from './dto/getGenericListItemBySlug.input' +import { GenericListItem } from './models/genericListItem.model' const defaultCache: CacheControlOptions = { maxAge: CACHE_CONTROL_MAX_AGE } @@ -667,6 +669,14 @@ export class CmsResolver { ): Promise { return this.cmsElasticsearchService.getGenericListItems(input) } + + @CacheControl(defaultCache) + @Query(() => GenericListItem, { nullable: true }) + getGenericListItemBySlug( + @Args('input') input: GetGenericListItemBySlugInput, + ): Promise { + return this.cmsElasticsearchService.getGenericListItemBySlug(input) + } } @Resolver(() => LatestNewsSlice) diff --git a/libs/cms/src/lib/dto/getGenericListItemBySlug.input.ts b/libs/cms/src/lib/dto/getGenericListItemBySlug.input.ts new file mode 100644 index 000000000000..6c0153bd1020 --- /dev/null +++ b/libs/cms/src/lib/dto/getGenericListItemBySlug.input.ts @@ -0,0 +1,14 @@ +import type { ElasticsearchIndexLocale } from '@island.is/content-search-index-manager' +import { Field, InputType } from '@nestjs/graphql' +import { IsString } from 'class-validator' + +@InputType() +export class GetGenericListItemBySlugInput { + @Field() + @IsString() + slug!: string + + @Field(() => String) + @IsString() + lang: ElasticsearchIndexLocale = 'is' +} diff --git a/libs/cms/src/lib/generated/contentfulTypes.d.ts b/libs/cms/src/lib/generated/contentfulTypes.d.ts index 15fbec8e8fa8..bd1046b15bec 100644 --- a/libs/cms/src/lib/generated/contentfulTypes.d.ts +++ b/libs/cms/src/lib/generated/contentfulTypes.d.ts @@ -1542,6 +1542,9 @@ export interface IGenericListFields { /** Search Input Placeholder */ searchInputPlaceholder: string + + /** Item Type */ + itemType?: 'Non-clickable' | 'Clickable' | undefined } /** A list of items which can be embedded into rich text */ @@ -1578,6 +1581,12 @@ export interface IGenericListItemFields { /** Card Intro */ cardIntro?: Document | undefined + + /** Content */ + content?: Document | undefined + + /** Slug */ + slug?: string | undefined } /** An item that belongs to a generic list */ diff --git a/libs/cms/src/lib/models/genericList.model.ts b/libs/cms/src/lib/models/genericList.model.ts index 0c5cd2b2b503..48622500262f 100644 --- a/libs/cms/src/lib/models/genericList.model.ts +++ b/libs/cms/src/lib/models/genericList.model.ts @@ -1,11 +1,20 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql' +import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql' import { ElasticsearchIndexLocale } from '@island.is/content-search-index-manager' import { SystemMetadata } from '@island.is/shared/types' import { CacheField } from '@island.is/nest/graphql' -import { IGenericList } from '../generated/contentfulTypes' +import { IGenericList, IGenericListFields } from '../generated/contentfulTypes' import { GenericListItemResponse } from './genericListItemResponse.model' import { GetGenericListItemsInput } from '../dto/getGenericListItems.input' +enum GenericListItemType { + NonClickable = 'NonClickable', + Clickable = 'Clickable', +} + +registerEnumType(GenericListItemType, { + name: 'GenericListItemType', +}) + @ObjectType() export class GenericList { @Field(() => ID) @@ -16,8 +25,16 @@ export class GenericList { @Field(() => String, { nullable: true }) searchInputPlaceholder?: string + + @CacheField(() => GenericListItemType, { nullable: true }) + itemType?: GenericListItemType } +const mapItemType = (itemType?: IGenericListFields['itemType']) => + itemType === 'Clickable' + ? GenericListItemType.Clickable + : GenericListItemType.NonClickable + export const mapGenericList = ({ fields, sys, @@ -31,4 +48,5 @@ export const mapGenericList = ({ page: 1, }, searchInputPlaceholder: fields.searchInputPlaceholder, + itemType: mapItemType(fields.itemType), }) diff --git a/libs/cms/src/lib/models/genericListItem.model.ts b/libs/cms/src/lib/models/genericListItem.model.ts index c8654a16ff24..d14c6ebc1a59 100644 --- a/libs/cms/src/lib/models/genericListItem.model.ts +++ b/libs/cms/src/lib/models/genericListItem.model.ts @@ -20,21 +20,29 @@ export class GenericListItem { @CacheField(() => [SliceUnion]) cardIntro: Array = [] + + @CacheField(() => [SliceUnion], { nullable: true }) + content?: Array + + @Field(() => String, { nullable: true }) + slug?: string } export const mapGenericListItem = ({ fields, sys, -}: IGenericListItem): GenericListItem => { - return { - id: sys.id, - genericList: fields.genericList - ? mapGenericList(fields.genericList) - : undefined, - title: fields.title ?? '', - date: fields.date || null, - cardIntro: fields.cardIntro - ? mapDocument(fields.cardIntro, sys.id + ':cardIntro') - : [], - } -} +}: IGenericListItem): GenericListItem => ({ + id: sys.id, + genericList: fields.genericList + ? mapGenericList(fields.genericList) + : undefined, + title: fields.title ?? '', + date: fields.date || null, + cardIntro: fields.cardIntro + ? mapDocument(fields.cardIntro, `${sys.id}:cardIntro`) + : [], + content: fields.content + ? mapDocument(fields.content, `${sys.id}:content`) + : [], + slug: fields.slug, +}) diff --git a/libs/cms/src/lib/search/importers/genericListItem.service.ts b/libs/cms/src/lib/search/importers/genericListItem.service.ts index 489020002589..6c8e4981c0c4 100644 --- a/libs/cms/src/lib/search/importers/genericListItem.service.ts +++ b/libs/cms/src/lib/search/importers/genericListItem.service.ts @@ -38,6 +38,18 @@ export class GenericListItemSyncService 2, ) + const tags: MappedData['tags'] = [] + + if (mapped.genericList) { + tags.push({ + type: 'referencedBy', + key: mapped.genericList.id, + }) + } + if (mapped.slug) { + tags.push({ type: 'slug', key: mapped.slug }) + } + return { _id: mapped.id, title: mapped.title, @@ -50,14 +62,7 @@ export class GenericListItemSyncService }), dateCreated: entry.sys.createdAt, dateUpdated: new Date().getTime().toString(), - tags: mapped.genericList - ? [ - { - type: 'referencedBy', - key: mapped.genericList.id, - }, - ] - : [], + tags, releaseDate: mapped.date, } } catch (error) {