diff --git a/libs/application/templates/official-journal-of-iceland/src/components/additions/Additions.tsx b/libs/application/templates/official-journal-of-iceland/src/components/additions/Additions.tsx new file mode 100644 index 000000000000..fc94f0a5f275 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/additions/Additions.tsx @@ -0,0 +1,233 @@ +import { useState } from 'react' +import { convertNumberToRoman, getAddition, isAddition } from '../../lib/utils' +import { additionSchema } from '../../lib/dataSchema' +import { + Stack, + Inline, + RadioButton, + Box, + Button, + Text, +} from '@island.is/island-ui/core' +import { HTMLText } from '@island.is/regulations-tools/types' +import { z } from 'zod' +import { + DEBOUNCE_INPUT_TIMER, + DEFAULT_ADDITIONS_COUNT, + MAXIMUM_ADDITIONS_COUNT, +} from '../../lib/constants' +import { HTMLEditor } from '../htmlEditor/HTMLEditor' +import { useApplication } from '../../hooks/useUpdateApplication' +import { getValueViaPath } from '@island.is/application/core' +import { useFormContext } from 'react-hook-form' +import { InputFields, OJOIApplication } from '../../lib/types' +import set from 'lodash/set' +import debounce from 'lodash/debounce' +import { useLocale } from '@island.is/localization' +import { attachments } from '../../lib/messages' + +type Props = { + application: OJOIApplication +} + +type Addition = z.infer[number] + +export const Additions = ({ application }: Props) => { + const [asRoman, setAsRoman] = useState(false) + + const { formatMessage: f } = useLocale() + const { setValue } = useFormContext() + const { updateApplication, application: currentApplication } = useApplication( + { + applicationId: application.id, + }, + ) + + const getAdditions = () => { + const additions = getValueViaPath( + currentApplication ? currentApplication.answers : application.answers, + InputFields.advert.additions, + ) + + return isAddition(additions) + ? additions + : [getAddition(DEFAULT_ADDITIONS_COUNT, false)] + } + + const onRemoveAddition = (index: number) => { + const filtered = additions.filter((_, i) => i !== index) + const mapped = filtered.map((addition, i) => { + if (addition.type !== 'html') return addition + + const title = f(attachments.additions.title, { + index: asRoman ? convertNumberToRoman(i + 1) : i + 1, + }) + + return { + ...addition, + title: title, + } + }) + + const currentAnswers = structuredClone(currentApplication.answers) + + const updatedAnswers = set( + currentAnswers, + InputFields.advert.additions, + mapped, + ) + + setValue(InputFields.advert.additions, mapped) + updateApplication(updatedAnswers) + } + + const onRomanChange = (val: boolean) => { + const handleTitleChange = (addition: Addition, i: number) => { + if (addition.type !== 'html') return addition + + const title = f(attachments.additions.title, { + index: asRoman ? convertNumberToRoman(i + 1) : i + 1, + }) + return { + ...addition, + title: title, + } + } + + const currentAnswers = structuredClone(currentApplication.answers) + const updatedAdditions = additions.map(handleTitleChange) + + const updatedAnswers = set( + currentAnswers, + InputFields.advert.additions, + updatedAdditions, + ) + + setAsRoman(val) + setValue(InputFields.advert.additions, updatedAdditions) + updateApplication(updatedAnswers) + } + + const onAddAddition = () => { + const currentAnswers = structuredClone(currentApplication.answers) + let currentAdditions = getValueViaPath( + currentAnswers, + InputFields.advert.additions, + ) + + if (!isAddition(currentAdditions)) { + currentAdditions = [] + } + + // TS not inferring the type after the check above + if (isAddition(currentAdditions)) { + const newAddition = getAddition(additions.length + 1, asRoman) + + const updatedAdditions = [...currentAdditions, newAddition] + const updatedAnswers = set( + currentAnswers, + InputFields.advert.additions, + updatedAdditions, + ) + + setValue(InputFields.advert.additions, updatedAdditions) + updateApplication(updatedAnswers) + } + } + + const onAdditionChange = (index: number, value: string) => { + const currentAnswers = structuredClone(currentApplication.answers) + const updatedAdditions = additions.map((addition, i) => + i === index ? { ...addition, content: value } : addition, + ) + + const updatedAnswers = set( + currentAnswers, + InputFields.advert.additions, + updatedAdditions, + ) + + setValue(InputFields.advert.additions, updatedAdditions) + updateApplication(updatedAnswers) + } + + const debouncedAdditionChange = debounce( + onAdditionChange, + DEBOUNCE_INPUT_TIMER, + ) + + const additionChangeHandler = (index: number, value: string) => { + debouncedAdditionChange.cancel() + debouncedAdditionChange(index, value) + } + + const additions = getAdditions() + + return ( + + {f(attachments.inputs.radio.title.label)} + + onRomanChange(false)} + /> + onRomanChange(true)} + /> + + + {additions.map((addition, additionIndex) => { + const currentAddition = additions.at(additionIndex) + + const defaultValue = currentAddition?.content || '' + return ( + + + {addition.title} + + additionChangeHandler(additionIndex, value) + } + /> + + + + ) + })} + + + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx b/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx index f2e56191bbd5..f8cb1367ec92 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/HTMLEditor.tsx @@ -13,6 +13,7 @@ type Props = { onChange?: (value: HTMLText) => void error?: string readOnly?: boolean + controller?: boolean } export const HTMLEditor = ({ @@ -24,6 +25,7 @@ export const HTMLEditor = ({ onChange, hideWarnings, readOnly = false, + controller = true, }: Props) => { const [initialValue, setInitalValue] = useState(value) const valueRef = useRef(() => value) @@ -42,7 +44,7 @@ export const HTMLEditor = ({ throw new Error('Not implemented') } - return ( + return controller ? ( + ) : ( + <> + {title && ( + + {title} + + )} + + { + onChange && onChange(valueRef.current()) + }} + onBlur={() => { + onChange && onChange(valueRef.current()) + }} + /> + + {error &&
{error}
} + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx index c2bd139df826..8969e0d0b52d 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx @@ -1,9 +1,11 @@ import { OJOIFieldBaseProps } from '../lib/types' -import { Box, InputFileUpload } from '@island.is/island-ui/core' +import { Box, Button, InputFileUpload, Stack } from '@island.is/island-ui/core' import { useFileUpload } from '../hooks/useFileUpload' import { ALLOWED_FILE_TYPES, ApplicationAttachmentType } from '../lib/constants' import { useLocale } from '@island.is/localization' import { attachments } from '../lib/messages/attachments' +import { useState } from 'react' +import { Additions } from '../components/additions/Additions' export const Attachments = ({ application }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() @@ -12,22 +14,43 @@ export const Attachments = ({ application }: OJOIFieldBaseProps) => { attachmentType: ApplicationAttachmentType.ADDITIONS, }) + const [asAddition, setAsAddition] = useState(true) + return ( - - - + + + + + {!asAddition ? ( + + ) : ( + + )} + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts b/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts index 4ab3c407842b..e05b15dcaf66 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts @@ -79,3 +79,5 @@ export const MAXIMUM_REGULAR_SIGNATURE_COUNT = 3 export const MINIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT = 2 export const DEFAULT_COMMITTEE_SIGNATURE_MEMBER_COUNT = 2 export const MAXIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT = 10 +export const MAXIMUM_ADDITIONS_COUNT = 10 +export const DEFAULT_ADDITIONS_COUNT = 1 diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts b/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts index 7c626cb47416..f617921467b5 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts @@ -17,6 +17,17 @@ export const memberItemSchema = z }) .partial() +export const additionSchema = z.array( + z + .object({ + id: z.string().optional(), + title: z.string().optional(), + content: z.string().optional(), + type: z.enum(['html', 'file']).optional(), + }) + .partial(), +) + export const membersSchema = z.array(memberItemSchema).optional() export const regularSignatureItemSchema = z @@ -59,6 +70,7 @@ const advertSchema = z categories: z.array(z.string()).optional(), channels: z.array(channelSchema).optional(), message: z.string().optional(), + additions: additionSchema.optional(), }) .partial() diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/attachments.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/attachments.ts index a6da88237d2c..0046056207a4 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/attachments.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/attachments.ts @@ -26,6 +26,35 @@ export const attachments = { description: 'Heading of the attachments section', }, }), + buttons: defineMessages({ + asDocument: { + id: 'ojoi.application:attachments.buttons.additionType.asDocument', + defaultMessage: 'Bæta við viðaukum', + description: 'Label of the button to add documents', + }, + asAttachment: { + id: 'ojoi.application:attachments.buttons.additionType.asAttachment', + defaultMessage: 'Hlaða upp skjölum', + description: 'Label of the button to upload attachments', + }, + removeAddition: { + id: 'ojoi.application:attachments.buttons.removeAddition', + defaultMessage: 'Fjarlægja viðauka', + description: 'Label of the button to remove an addition', + }, + addAddition: { + id: 'ojoi.application:attachments.buttons.addAddition', + defaultMessage: 'Bæta við viðauka', + description: 'Label of the button to add an addition', + }, + }), + additions: defineMessages({ + title: { + id: 'ojoi.application:attachments.additions.title', + defaultMessage: 'Viðauki {index}', + description: 'Title of the additions section', + }, + }), inputs: { fileUpload: defineMessages({ header: { @@ -46,17 +75,24 @@ export const attachments = { }, }), radio: { - additions: defineMessages({ + title: defineMessages({ + label: { + id: 'ojoi.application:attachments.radio.title.label', + defaultMessage: 'Viðaukar eða fylgiskjöl', + description: 'Label of the radio buttons', + }, + }), + numeric: defineMessages({ label: { id: 'ojoi.application:attachments.radio.additions.label', - defaultMessage: 'Viðaukar (1, 2, 3..)', + defaultMessage: 'Viðauki (1, 2, 3..)', description: 'Label of the additions radio button', }, }), - documents: defineMessages({ + roman: defineMessages({ label: { id: 'ojoi.application:attachments.radio.documents.label', - defaultMessage: 'Fylgiskjöl (I, II, III..)', + defaultMessage: 'Viðauki (I, II, III..)', description: 'Label of the documents radio button', }, }), diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/types.ts b/libs/application/templates/official-journal-of-iceland/src/lib/types.ts index 4ade26b040e2..ccae26abdb38 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/types.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/types.ts @@ -21,6 +21,7 @@ export const InputFields = { channels: 'advert.channels', message: 'advert.message', involvedPartyId: 'advert.involvedPartyId', + additions: 'advert.additions', }, [Routes.SIGNATURE]: { regular: 'signatures.regular', diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts b/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts index a3d9084231e0..2e6f17b53156 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/utils.ts @@ -2,6 +2,7 @@ import addDays from 'date-fns/addDays' import addYears from 'date-fns/addYears' import { z } from 'zod' import { + additionSchema, committeeSignatureSchema, memberItemSchema, partialSchema, @@ -15,6 +16,7 @@ import format from 'date-fns/format' import is from 'date-fns/locale/is' import { SignatureTypes, OJOI_DF, FAST_TRACK_DAYS } from './constants' import { MessageDescriptor } from 'react-intl' +import { v4 as uuid } from 'uuid' export const countDaysAgo = (date: Date) => { const now = new Date() @@ -69,6 +71,16 @@ export const getEmptyMember = () => ({ below: '', }) +export const getAddition = ( + index: number, + roman = true, +): z.infer[number] => ({ + id: uuid(), + title: roman ? `Viðauki ${convertNumberToRoman(index)}` : `Viðauki ${index}`, + content: '', + type: 'html', +}) + export const getRegularSignature = ( signatureCount: number, memberCount: number, @@ -118,6 +130,11 @@ export const getSignatureDefaultValues = (signature: any, index?: number) => { return { institution: signature.institution, date: signature.date } } +export const isAddition = ( + addition: unknown, +): addition is z.infer => + additionSchema.safeParse(addition).success + export const isRegularSignature = ( any: unknown, ): any is z.infer => @@ -351,3 +368,8 @@ export const base64ToBlob = (base64: string, mimeType = 'application/pdf') => { const byteCharacters = Buffer.from(base64, 'base64') return new Blob([byteCharacters], { type: mimeType }) } + +export const convertNumberToRoman = (num: number) => { + const roman = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'] + return roman[num - 1] +} diff --git a/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx b/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx index d55719e6588e..1e4f66f0cdf0 100644 --- a/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx @@ -11,6 +11,7 @@ import { Controller, useFormContext } from 'react-hook-form' import { getErrorViaPath } from '@island.is/application/core' import { AnswerOption, + DEFAULT_ADDITIONS_COUNT, DEFAULT_COMMITTEE_SIGNATURE_MEMBER_COUNT, DEFAULT_REGULAR_SIGNATURE_COUNT, DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT, @@ -18,7 +19,11 @@ import { SignatureTypes, } from '../lib/constants' import { useApplication } from '../hooks/useUpdateApplication' -import { getRegularSignature, getCommitteeSignature } from '../lib/utils' +import { + getRegularSignature, + getCommitteeSignature, + getAddition, +} from '../lib/utils' import set from 'lodash/set' import { useEffect } from 'react' @@ -43,6 +48,11 @@ export const RequirementsScreen = ({ */ useEffect(() => { let currentAnswers = structuredClone(application.answers) + + currentAnswers = set(currentAnswers, InputFields.advert.additions, [ + getAddition(DEFAULT_ADDITIONS_COUNT, false), + ]) + currentAnswers = set(currentAnswers, InputFields.signature.regular, [ ...getRegularSignature( DEFAULT_REGULAR_SIGNATURE_COUNT,