Skip to content

Commit

Permalink
fix(form): improve form notice box
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Sep 26, 2024
1 parent b1d7957 commit 7ce3eb4
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 45 deletions.
57 changes: 13 additions & 44 deletions src/components/form/DefaultFormContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getSectionPath } from '../../lib'
import { ModelSection } from '../../types'
import { StandardFormSection, StandardFormActions } from '../standardForm'
import classes from './DefaultFormContents.module.css'
import { DefaultFormErrorNotice } from './DefaultFormErrorNotice'

export function DefaultEditFormContents({
children,
Expand All @@ -15,37 +16,22 @@ export function DefaultEditFormContents({
children: React.ReactNode
section: ModelSection
}) {
const { submitting, submitError } = useFormState({
subscription: { submitting: true, submitError: true },
const { submitting } = useFormState({
subscription: { submitting: true },
})

const formErrorRef = useRef<HTMLDivElement | null>(null)
const navigate = useNavigate()

const listPath = `/${getSectionPath(section)}`
useEffect(() => {
if (submitError) {
formErrorRef.current?.scrollIntoView({ behavior: 'smooth' })
}
}, [submitError])

return (
<>
<div className={classes.form}>{children}</div>
{submitError && (
<div className={classes.form}>
{children}

<StandardFormSection>
<div ref={formErrorRef}>
<NoticeBox
error
title={i18n.t(
'Something went wrong when submitting the form'
)}
>
{submitError}
</NoticeBox>
</div>
<DefaultFormErrorNotice />
</StandardFormSection>
)}
</div>
<div className={classes.formActions}>
<StandardFormActions
cancelLabel={i18n.t('Cancel')}
Expand All @@ -65,37 +51,20 @@ export function DefaultNewFormContents({
children: React.ReactNode
section: ModelSection
}) {
const { submitting, submitError } = useFormState({
subscription: { submitting: true, submitError: true },
const { submitting } = useFormState({
subscription: { submitting: true },
})

const formErrorRef = useRef<HTMLDivElement | null>(null)
const navigate = useNavigate()

const listPath = `/${getSectionPath(section)}`
useEffect(() => {
if (submitError) {
formErrorRef.current?.scrollIntoView({ behavior: 'smooth' })
}
}, [submitError])

return (
<div className={classes.form}>
{children}
{submitError && (
<StandardFormSection>
<div ref={formErrorRef}>
<NoticeBox
error
title={i18n.t(
'Something went wrong when submitting the form'
)}
>
{submitError}
</NoticeBox>
</div>
</StandardFormSection>
)}
<StandardFormSection>
<DefaultFormErrorNotice />
</StandardFormSection>
<StandardFormActions
cancelLabel={i18n.t('Exit without saving')}
submitLabel={i18n.t('Create {{modelName}} ', {
Expand Down
3 changes: 3 additions & 0 deletions src/components/form/DefaultFormErrorNotice.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.noticeBox {
max-width: 640px;
}
138 changes: 138 additions & 0 deletions src/components/form/DefaultFormErrorNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import i18n from '@dhis2/d2-i18n'
import { NoticeBox } from '@dhis2/ui'
import React, { useEffect, useRef } from 'react'
import { useFormState } from 'react-final-form'
import css from './DefaultFormErrorNotice.module.css'
import { FormState } from 'final-form'

Check failure on line 6 in src/components/form/DefaultFormErrorNotice.tsx

View workflow job for this annotation

GitHub Actions / lint

`final-form` import should occur before import of `react`

const formStateSubscriptions = {
errors: true,
submitError: true,
submitFailed: true,
hasValidationErrors: true,
hasSubmitErrors: true,
dirtySinceLastSubmit: true,
}

type FormStateErrors = Pick<
FormState<unknown>,
keyof typeof formStateSubscriptions
>

export function DefaultFormErrorNotice() {
const partialFormState: FormStateErrors = useFormState({
subscription: formStateSubscriptions,
})
// only show after trying to submit
if (
!partialFormState.submitFailed ||
(partialFormState.submitFailed && partialFormState.dirtySinceLastSubmit)
) {
return null
}

if (partialFormState.hasValidationErrors) {
return <ValidationErrors formStateErrors={partialFormState} />
}

if (partialFormState.hasSubmitErrors) {
return <ServerSubmitError formStateErrors={partialFormState} />
}
return null
}

const ValidationErrors = ({
formStateErrors,
}: {
formStateErrors: FormStateErrors
}) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
ref.current.scrollIntoView({ behavior: 'smooth' })
}
}, [])
return (
<div ref={ref}>
<NoticeBox
className={css.noticeBox}
warning
title={i18n.t('Validation errors')}
>
<p>
{i18n.t(
'Some fields have validation errors. Please fix them before saving.'
)}
</p>
{formStateErrors.errors && (
<ErrorList errors={formStateErrors.errors} />
)}
</NoticeBox>
</div>
)
}

const ErrorList = ({ errors }: { errors: Record<string, string> }) => {
const labels = getFieldLabelsBestEffort()

return (
<ul style={{ padding: '0 16px' }}>
{Object.entries(errors).map(([key, value]) => {
return (
<li key={key} style={{ display: 'flex', gap: '8px' }}>
<span
style={{
fontWeight: '600',
}}
>
{labels.get(key) || key}:
</span>
<span>{value}</span>
</li>
)
})}
</ul>
)
}

const ServerSubmitError = ({
formStateErrors,
}: {
formStateErrors: FormStateErrors
}) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
ref.current.scrollIntoView({ behavior: 'smooth' })
}
}, [])
return (
<div ref={ref}>
<NoticeBox
error
title={i18n.t('Something went wrong when submitting the form')}
>
<p>{formStateErrors.submitError}</p>
</NoticeBox>
</div>
)
}

/**
* We don't have a good way to get the translated labels, so for now
* we get these from the DOM. This is a best-effort approach.
* TODO: Find a better way to get the labels, eg. by wrapping Field components
* in a generic component that can register fields with metadata such as labels.
*/
const getFieldLabelsBestEffort = () => {
const labels = Array.from(document.getElementsByTagName('label'))
.filter((elem) => elem.htmlFor)
.map((elem) => {
const fieldName = elem.htmlFor
const label = elem.firstChild?.textContent
?.replace('(required)', '')
.trim()
return [fieldName, label] as const
})
return new Map(labels)
}
4 changes: 3 additions & 1 deletion src/pages/categories/form/CategoryFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ export const CategoryFormFields = () => {

<StandardFormSection>
<StandardFormSectionTitle>
{i18n.t('Category options')}
<label htmlFor={'categoryOptions'}>
{i18n.t('Category options')}
</label>
</StandardFormSectionTitle>
<StandardFormSectionDescription>
{i18n.t(
Expand Down

0 comments on commit 7ce3eb4

Please sign in to comment.