Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to show additional questions #18

Merged
merged 22 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6aafe40
feat: add ability to show additional questions
bartjkdp Oct 18, 2024
df9bcb3
Merge remote-tracking branch 'origin/main' into feat/5-additional-que…
justiandevs Oct 22, 2024
155407d
Merge remote-tracking branch 'origin/main' into feat/5-additional-que…
justiandevs Oct 23, 2024
e7a8da5
Feat: technical setup for dynamic loading of questions, radio group s…
justiandevs Oct 23, 2024
5ee9f8a
Refactor: add TODO comment for removal of hardcoded marker, rename pr…
justiandevs Oct 23, 2024
1769eba
Feat: add radio group answers to API post signal call
justiandevs Oct 23, 2024
2807880
Feat: add extra_properties type to FormStoreState type, on page reloa…
justiandevs Oct 23, 2024
7980a30
Feat: add extra_properties type to FormStoreState type, on page reloa…
justiandevs Oct 23, 2024
a129298
Merge remote-tracking branch 'origin/feat/5-additional-questions' int…
justiandevs Oct 23, 2024
a354cc7
Fix: add extra check to decide if extra_properties exist
justiandevs Oct 23, 2024
b1c5656
Feat: add next prefetching
justiandevs Oct 23, 2024
48f735c
Feat: type assertion fix
justiandevs Oct 24, 2024
4442f03
Feat: TextField integration for dynamic form
justiandevs Oct 24, 2024
5682921
Feat: getValidators() function for react-hook-form
justiandevs Oct 24, 2024
d5c2217
Feat: show not required text in label if field is not required
justiandevs Oct 24, 2024
f68a53b
Feat: CheckboxInput for dynamic form
justiandevs Oct 24, 2024
74d8bdf
Refactor: move dynamic render field related code out of form
justiandevs Oct 24, 2024
0c1cb10
Feat: add subtitle to CheckBoxInput and RadioInput
justiandevs Oct 24, 2024
213ca65
Feat: add TextAreaInput to dynamic form
justiandevs Oct 24, 2024
fdb4b62
Feat: add PlainText field to Dynamic form
justiandevs Oct 24, 2024
5bcd39e
Chore: remove unnecessary TODO comment
justiandevs Oct 24, 2024
4bd87ae
Refactor: rename key to value in array of filter values
justiandevs Oct 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,083 changes: 1,080 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"react-hook-form": "7.53.0",
"react-icons": "4.12.0",
"react-map-gl": "7.1.7",
"react-markdown": "9.0.1",
"react-select": "5.8.1",
"tailwind-merge": "2.5.4",
"validator": "13.12.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,94 @@
'use client'

import { IncidentFormFooter } from '@/app/[locale]/incident/components/IncidentFormFooter'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/Form'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslations } from 'next-intl'
import { useEffect, useState } from 'react'
import { fetchAdditionalQuestions } from '@/services/additional-questions'
import { useFormStore } from '@/store/form_store'
import { IncidentFormFooter } from '@/app/[locale]/incident/components/IncidentFormFooter'
import { useStepperStore } from '@/store/stepper_store'
import { useRouter } from '@/routing/navigation'
import { LocationMap } from '@/components/ui/LocationMap'
import { Button } from '@/components/ui/Button'
import { MapDialog } from '@/app/[locale]/incident/add/components/MapDialog'
import { useEffect } from 'react'
import { useFormStore } from '@/store/form_store'
import { PublicQuestion } from '@/types/form'
import { RenderDynamicFields } from '@/app/[locale]/incident/add/components/questions/RenderDynamicFields'

const IncidentQuestionsLocationForm = () => {
const t = useTranslations('describe-add.form')
const { updateForm, formState } = useFormStore()
export const IncidentQuestionsLocationForm = () => {
const { formState: formStoreState, updateForm } = useFormStore()
const [loading, setLoading] = useState<boolean>(true)
const [additionalQuestions, setAdditionalQuestions] = useState<
PublicQuestion[]
>([])
const { addOneStep, setLastCompletedStep } = useStepperStore()
const router = useRouter()
const marker = formState.coordinates!

const incidentQuestionAndLocationFormSchema = z.object({
map: z.object({
lng: z.number().min(0.00000001, t('errors.location_required')),
lat: z.number().min(0.00000001, t('errors.location_required')),
}),
})

const form = useForm<z.infer<typeof incidentQuestionAndLocationFormSchema>>({
resolver: zodResolver(incidentQuestionAndLocationFormSchema),
defaultValues: {
map: {
lng: formState.coordinates[0],
lat: formState.coordinates[1],
},
},
})

const {
setValue,
register,
handleSubmit,
formState: { errors },
} = form
} = useForm()

useEffect(() => {
router.prefetch('/incident/contact')
}, [router])

useEffect(() => {
if (marker[0] !== 0 && marker[1] !== 0) {
setValue('map', { lng: marker[0], lat: marker[1] })
const appendAdditionalQuestions = async () => {
try {
const additionalQuestions = (await fetchAdditionalQuestions(
formStoreState.main_category,
formStoreState.sub_category
)) as unknown as PublicQuestion[]

setAdditionalQuestions(additionalQuestions)
setLoading(false)
} catch (e) {
console.error('Could not fetch additional questions', e)
setLoading(false)
}
}
}, [marker])

const onSubmit = (
values: z.infer<typeof incidentQuestionAndLocationFormSchema>
) => {
appendAdditionalQuestions()
}, [formStoreState.main_category, formStoreState.sub_category])

const onSubmit = (data: any) => {
const questionKeys = Object.keys(data)
const questionsToSubmit = additionalQuestions.filter(
(question) =>
questionKeys.includes(question.key) &&
data[question.key] !== null &&
data[question.key] !== ''
)

const answers = questionsToSubmit.map((question) => {
const id = data[question.key]
const checkboxAnswers: string[] = Array.isArray(id)
? id.filter((value: any) => value !== false && value !== 'empty')
: []

// If checkboxAnswers has a length, map over them to return a list of answer objects
const answer =
checkboxAnswers.length > 0
? checkboxAnswers.map((answerId) => ({
id: answerId,
label: question.meta.values[answerId],
info: '',
}))
: question.meta?.values?.[id]
? {
id: id,
label: question.meta.values[id],
info: '',
}
: data[question.key]

return {
id: question.key,
label: question.meta.label,
category_url: `/signals/v1/public/terms/categories/${formStoreState.sub_category}/sub_categories/${formStoreState.main_category}`,
answer,
}
})

updateForm({
...formState,
coordinates: [values.map.lng, values.map.lat],
...formStoreState,
extra_properties: answers,
})

setLastCompletedStep(2)
Expand All @@ -71,46 +98,23 @@ const IncidentQuestionsLocationForm = () => {
}

return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-8 items-start"
>
<FormField
name={'map'}
control={form.control}
render={({ field, formState: { errors } }) => (
<FormItem className="w-full relative">
<div>
<FormLabel>{t('add_map_heading')}</FormLabel>
<FormMessage customError={errors.map?.lng} />
</div>
<FormControl className="w-full bg-red-400 relative">
<>
<LocationMap />
{/* TODO: I can not find the reason why not every element inside this dialog is focusable */}
<MapDialog
marker={marker}
trigger={
<Button
className="absolute top-1/2 mt-5 -translate-y-1/2 left-1/2 -translate-x-1/2 border-none"
type="button"
>
{t('add_choose_location_button')}
</Button>
}
/>
</>
</FormControl>
</FormItem>
)}
/>
<IncidentFormFooter />
</form>
</Form>
</div>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-8 items-start"
>
{additionalQuestions.length ? (
<RenderDynamicFields
data={additionalQuestions}
register={register}
errors={errors}
/>
) : loading ? (
/* TODO: Implement nice loading state */
<p>Laden...</p>
) : (
<p>TODO: Laat hier een LocationSelect zien</p>
)}
<IncidentFormFooter />
</form>
)
}

export { IncidentQuestionsLocationForm }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { QuestionField } from '@/types/form'

interface AssetSelectProps extends Omit<QuestionField, 'register' | 'errors'> {}

export const AssetSelect = ({ field }: AssetSelectProps) => {
return <p>{field.key}</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { QuestionField } from '@/types/form'
import { useTranslations } from 'next-intl'
import { useFormStore } from '@/store/form_store'
import { getValidators } from '@/lib/utils/form-validator'
import React from 'react'

interface CheckboxInputProps extends QuestionField {}

export const CheckboxInput = ({
field,
register,
errors,
}: CheckboxInputProps) => {
const t = useTranslations('general.errors')
const { formState } = useFormStore()

const errorMessage = errors[field.key]?.message as string

// Check if the user has already answered a specific question.
// Returns true if an answer exists where the id is same as the value key.
// This is used to determine if the 'defaultChecked' property of a checkbox input should be set.
const getDefaultValueCheckboxInput = (id: string, key: string) => {
const extraProperties = formState.extra_properties.filter(
(question) => question.id === id
)

if (!extraProperties.length) {
return false
}

if (Array.isArray(extraProperties[0].answer)) {
return (
extraProperties[0].answer.filter((answer) => answer?.id === key)
.length > 0
)
}

return false
}

return (
<fieldset aria-invalid={!!errorMessage}>
<legend>
{field.meta.label}{' '}
<span> {field.required ? '' : `(${t('not_required_short')})`}</span>
{field.meta.subtitle && <span>{field.meta.subtitle}</span>}
</legend>
{errorMessage && (
<p
id={`${field.key}-error`}
aria-live="assertive"
style={{ color: 'red' }}
>
{errorMessage}
</p>
)}
{Object.keys(field.meta.values).map((key: string) => (
<div key={key}>
<input
{...register(`${field.key}.${key}`, getValidators(field, t))}
type="checkbox"
id={`${field.key}-${key}`}
value={key}
aria-describedby={errorMessage ? `${field.key}-error` : undefined}
defaultChecked={getDefaultValueCheckboxInput(field.key, key)}
/>
<label htmlFor={`${field.key}-${key}`}>
{field.meta.values[key]}
</label>
</div>
))}
</fieldset>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LocationMap } from '@/components/ui/LocationMap'
import { MapDialog } from '@/app/[locale]/incident/add/components/MapDialog'
import { Button } from '@/components/ui/Button'

export const LocationSelect = (props: any) => {
return (
<div className="relative">
<LocationMap />
{/* TODO: I can not find the reason why not every element inside this dialog is focusable */}
<MapDialog
marker={props.marker}
trigger={
<Button
className="absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 border-none"
type="button"
>
Kies locatie
</Button>
}
/>
</div>
)
}
15 changes: 15 additions & 0 deletions src/app/[locale]/incident/add/components/questions/PlainText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { QuestionField } from '@/types/form'
import Markdown from 'react-markdown'

interface PlainTextProps extends Omit<QuestionField, 'register' | 'errors'> {}

export const PlainText = ({ field }: PlainTextProps) => {
// TODO: Discuss if alert is the only used PlainText type in Signalen, style Markdown
return field.meta.value ? (
<div className="bg-red-100 rounded-lg p-4">
<Markdown>{field.meta.value}</Markdown>
Robbert marked this conversation as resolved.
Show resolved Hide resolved
</div>
) : (
<></>
)
}
Loading