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

Events v2: Implement optimistic file upload and FILE type input #8246

Merged
merged 38 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
edbe07a
implement optimistic file upload and FILE type input
rikukissa Dec 18, 2024
8593948
Merge branch 'develop' of github.com:opencrvs/opencrvs-core into even…
rikukissa Dec 18, 2024
6df0b40
fix broken offline record creation
rikukissa Dec 18, 2024
03d7be3
fix one type error
rikukissa Dec 18, 2024
1ea0f11
fix: update tests, add missing license header
makelicious Dec 18, 2024
7b0d269
fix: explicitly throw if cache not found
makelicious Dec 18, 2024
45a9879
fix type errors
rikukissa Dec 19, 2024
61c7c75
fix eslint errors
rikukissa Dec 19, 2024
45dab8e
fix review page
rikukissa Dec 19, 2024
78f2056
add signing for files fetched from events service
rikukissa Dec 19, 2024
3ed265f
Merge branch 'develop' of github.com:opencrvs/opencrvs-core into even…
rikukissa Dec 19, 2024
1561e2e
cleanup
rikukissa Dec 19, 2024
981a6f0
cleanup
rikukissa Dec 19, 2024
434293b
use undefined instead of null in FileInput as the default value of se…
rikukissa Dec 19, 2024
157cc71
remove default empty string value
rikukissa Dec 19, 2024
f850846
remove fallback output
rikukissa Dec 19, 2024
afc741a
fix linter error
rikukissa Dec 19, 2024
f935e7d
fix: clean up types and lint issues
makelicious Dec 19, 2024
67bd36c
fix review page exception
rikukissa Dec 20, 2024
5aee2e4
fix: add missing flush
makelicious Dec 20, 2024
23b8027
fix: update test mock name
makelicious Dec 20, 2024
3223186
Merge branch 'develop' into events-v2-file-input
makelicious Dec 20, 2024
125c78d
move debugger to top level of events v2
rikukissa Dec 20, 2024
f1f3527
Merge branch 'events-v2-file-input' of github.com:opencrvs/opencrvs-c…
rikukissa Dec 20, 2024
a6ad8f0
fix: add mock for files service call
makelicious Dec 20, 2024
a58902d
chore: add msw, mock documents api call
makelicious Dec 20, 2024
425bcef
fix: add missing license header
makelicious Dec 20, 2024
dfea202
refactor react query logic a bit
rikukissa Dec 20, 2024
3f2adc8
Merge branch 'events-v2-file-input' of github.com:opencrvs/opencrvs-c…
rikukissa Dec 20, 2024
048c7a6
fix: remove unused export
makelicious Dec 20, 2024
02550e7
fix: change cache invalidation order
makelicious Dec 20, 2024
c74bd85
Merge branch 'develop' into events-v2-file-input
makelicious Dec 20, 2024
9a46b31
fix: add flushPromises
makelicious Dec 20, 2024
095b2a1
fix: add flushPromises to top-level
makelicious Dec 20, 2024
6d5a048
Merge branch 'develop' of github.com:opencrvs/opencrvs-core into even…
rikukissa Jan 3, 2025
3958a5c
fix merge
rikukissa Jan 3, 2025
65abcd5
add types back
rikukissa Jan 3, 2025
6a54760
improve knip diff output
rikukissa Jan 3, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ jobs:
- name: Run knip on base branch
id: knip_base
run: |
npx knip --tags=-knipignore --no-exit-code --exports --reporter=markdown > knip_report.md
npx knip --tags=-knipignore --no-exit-code --exports --reporter=markdown | sed -E 's/ +/ /g' | sed -E 's/:[0-9]+:[0-9]+//' > knip_report.md
TOTAL=$(grep -oP '## [A-Za-z\s]+ \(\K[0-9]+' knip_report.md | awk '{sum+=$1} END {print sum}')
echo "Total $TOTAL issue(s) on base branch."
echo "total=${TOTAL}" >> $GITHUB_OUTPUT
Expand All @@ -192,7 +192,7 @@ jobs:
- name: Run knip on PR branch
id: knip_pr
run: |
npx knip --tags=-knipignore --no-exit-code --exports --reporter=markdown > knip_report.md
npx knip --tags=-knipignore --no-exit-code --exports --reporter=markdown | sed -E 's/ +/ /g' | sed -E 's/:[0-9]+:[0-9]+//' > knip_report.md
TOTAL=$(grep -oP '## [A-Za-z\s]+ \(\K[0-9]+' knip_report.md | awk '{sum+=$1} END {print sum}')
echo "Total $TOTAL issue(s) on PR branch."
echo "total=${TOTAL}" >> $GITHUB_OUTPUT
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ services:
- MONGO_URL=mongodb://mongo1/events
- ES_HOST=elasticsearch:9200
- COUNTRY_CONFIG_URL=http://countryconfig:3040/
- DOCUMENTS_URL=http://documents:9050

# User facing services
workflow:
Expand Down
164 changes: 105 additions & 59 deletions packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,32 @@
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/

/* eslint-disable */
import { InputField } from '@client/components/form/InputField'
import { DATE, PARAGRAPH, TEXT } from '@client/forms'
import { IAdvancedSearchFormState } from '@client/search/advancedSearch/utils'
import { DateField } from '@opencrvs/components/lib/DateField'
import { Text } from '@opencrvs/components/lib/Text'
import { TextInput } from '@opencrvs/components/lib/TextInput'
import * as React from 'react'

import styled, { keyframes } from 'styled-components'
import {
evalExpressionInFieldDefinition,
getConditionalActionsForField,
getDependentFields,
handleInitialValue,
hasInitialValueDependencyInfo
} from './utils'
import { Errors, getValidationErrorsForForm } from './validation'

import {
FieldConfig,
FieldValue,
FieldValueByType,
FileFieldValue
} from '@opencrvs/commons/client'
import {
Field,
FieldProps,
Expand All @@ -26,30 +48,8 @@ import {
MessageDescriptor,
useIntl
} from 'react-intl'
import { FieldConfig } from '@opencrvs/commons'
import { TextInput } from '@opencrvs/components/lib/TextInput'
import { Text } from '@opencrvs/components/lib/Text'
import { DateField } from '@opencrvs/components/lib/DateField'
import { IAdvancedSearchFormState } from '@client/search/advancedSearch/utils'
import {
DATE,
HIDDEN,
IFormFieldValue,
IFormSectionData,
PARAGRAPH,
TEXT
} from '@client/forms'
import { InputField } from '@client/components/form/InputField'
import {
evalExpressionInFieldDefinition,
flatten,
getConditionalActionsForField,
getDependentFields,
handleInitialValue,
hasInitialValueDependencyInfo,
unflatten
} from './utils'
import { Errors, getValidationErrorsForForm } from './validation'
import { FileInput } from './inputs/FileInput/FileInput'
import { ActionFormData } from '@opencrvs/commons'

const fadeIn = keyframes`
from { opacity: 0; }
Expand All @@ -64,27 +64,27 @@ const FormItem = styled.div<{
ignoreBottomMargin ? '0px' : '22px'};
`

interface GeneratedInputFieldProps {
fieldDefinition: FieldConfig
interface GeneratedInputFieldProps<FieldType extends FieldConfig> {
fieldDefinition: FieldType
fields: FieldConfig[]
values: IFormSectionData
setFieldValue: (name: string, value: IFormFieldValue) => void
values: ActionFormData
setFieldValue: (name: string, value: FieldValue | undefined) => void
onClick?: () => void
onChange: (e: React.ChangeEvent) => void
onBlur: (e: React.FocusEvent) => void
resetDependentSelectValues: (name: string) => void
value: IFormFieldValue
value: FieldValueByType[FieldType['type']]
touched: boolean
error: string
formData: IFormSectionData
formData: ActionFormData
disabled?: boolean
onUploadingStateChanged?: (isUploading: boolean) => void
requiredErrorMessage?: MessageDescriptor
setFieldTouched: (name: string, isTouched?: boolean) => void
}

const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
({
const GeneratedInputField = React.memo(
<FieldType extends FieldConfig>({
fieldDefinition,
onChange,
onBlur,
Expand All @@ -99,7 +99,7 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
requiredErrorMessage,
fields,
values
}) => {
}: GeneratedInputFieldProps<FieldType>) => {
const intl = useIntl()

const inputFieldProps = {
Expand Down Expand Up @@ -133,6 +133,12 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
intl.formatMessage(fieldDefinition.placeholder)
}

const handleFileChange = React.useCallback(
(value: FileFieldValue | undefined) =>
setFieldValue(fieldDefinition.id, value),
[fieldDefinition.id, setFieldValue]
)

if (fieldDefinition.type === DATE) {
return (
<InputField {...inputFieldProps}>
Expand Down Expand Up @@ -166,17 +172,6 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
)
}

if (fieldDefinition.type === HIDDEN) {
const { error, touched, ...allowedInputProps } = inputProps

return (
<input
type="hidden"
{...allowedInputProps}
value={inputProps.value as string}
/>
)
}
if (fieldDefinition.type === TEXT) {
return (
<InputField {...inputFieldProps}>
Expand All @@ -190,13 +185,25 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
</InputField>
)
}
if (fieldDefinition.type === 'FILE') {
const value = formData[fieldDefinition.id] as FileFieldValue
return (
<InputField {...inputFieldProps}>
<FileInput
{...inputProps}
value={value}
onChange={handleFileChange}
/>
</InputField>
)
}
return <div>Unsupported field type {fieldDefinition.type}</div>
}
)

GeneratedInputField.displayName = 'MemoizedGeneratedInputField'

type FormData = Record<string, IFormFieldValue>
type FormData = Record<string, FieldValue>

const mapFieldsToValues = (fields: FieldConfig[], formData: FormData) =>
fields.reduce((memo, field) => {
Expand All @@ -211,15 +218,15 @@ interface ExposedProps {
id: string
fieldsToShowValidationErrors?: FieldConfig[]
setAllFieldsDirty: boolean
onChange: (values: IFormSectionData) => void
formData: Record<string, IFormFieldValue>
onChange: (values: ActionFormData) => void
formData: Record<string, FieldValue>
onSetTouched?: (func: ISetTouchedFunction) => void
requiredErrorMessage?: MessageDescriptor
onUploadingStateChanged?: (isUploading: boolean) => void
initialValues?: IAdvancedSearchFormState
}

type AllProps = ExposedProps & IntlShapeProps & FormikProps<IFormSectionData>
type AllProps = ExposedProps & IntlShapeProps & FormikProps<ActionFormData>

class FormSectionComponent extends React.Component<AllProps> {
componentDidUpdate(prevProps: AllProps) {
Expand Down Expand Up @@ -289,7 +296,7 @@ class FormSectionComponent extends React.Component<AllProps> {

setFieldValuesWithDependency = (
fieldName: string,
value: IFormFieldValue
value: FieldValue | undefined
) => {
const updatedValues = cloneDeep(this.props.values)
set(updatedValues, fieldName, value)
Expand Down Expand Up @@ -328,13 +335,24 @@ class FormSectionComponent extends React.Component<AllProps> {
}

render() {
const { values, fields, setFieldTouched, touched, intl, formData } =
this.props
const {
values,
fields: fieldsWithDotIds,
setFieldTouched,
touched,
intl,
formData
} = this.props

const language = this.props.intl.locale

const errors = this.props.errors as unknown as Errors

const fields = fieldsWithDotIds.map((field) => ({
...field,
id: field.id.replaceAll('.', FIELD_SEPARATOR)
}))

return (
<section>
{fields.map((field) => {
Expand All @@ -348,7 +366,7 @@ class FormSectionComponent extends React.Component<AllProps> {

const conditionalActions: string[] = getConditionalActionsForField(
field,
{ ...formData, ...values }
{ $form: values, $now: new Date().toISOString().split('T')[0] }
)

if (conditionalActions.includes('hide')) {
Expand All @@ -374,7 +392,11 @@ class FormSectionComponent extends React.Component<AllProps> {
error={isFieldDisabled ? '' : error}
fields={fields}
formData={formData}
touched={flatten(touched)[field.id] || false}
touched={
makeFormikFieldIdsOpenCRVSCompatible(touched)[
field.id
] || false
}
values={values}
onUploadingStateChanged={
this.props.onUploadingStateChanged
Expand All @@ -391,26 +413,50 @@ class FormSectionComponent extends React.Component<AllProps> {
}
}

/*
* Formik has a feature that automatically nests all form keys that have a dot in them.
* Because our form field ids can have dots in them, we temporarily transform those dots
* to a different character before passing the data to Formik. This function unflattens
*/
const FIELD_SEPARATOR = '____'
function makeFormFieldIdsFormikCompatible<T>(data: Record<string, T>) {
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key.replaceAll('.', FIELD_SEPARATOR),
value
])
)
}

function makeFormikFieldIdsOpenCRVSCompatible<T>(data: Record<string, T>) {
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key.replaceAll(FIELD_SEPARATOR, '.'),
value
])
)
}

export const FormFieldGenerator: React.FC<ExposedProps> = (props) => {
const intl = useIntl()

const nestedFormData = unflatten(props.formData)
const nestedFormData = makeFormFieldIdsFormikCompatible(props.formData)

const onChange = (values: IFormSectionData) => {
props.onChange(flatten(values))
const onChange = (values: ActionFormData) => {
props.onChange(makeFormikFieldIdsOpenCRVSCompatible(values))
}

const initialValues = unflatten<IFormFieldValue>(
const initialValues = makeFormFieldIdsFormikCompatible<FieldValue>(
props.initialValues ?? mapFieldsToValues(props.fields, nestedFormData)
)

return (
<Formik<IFormSectionData>
<Formik<ActionFormData>
initialValues={initialValues}
validate={(values) =>
getValidationErrorsForForm(
props.fields,
flatten(values),
makeFormikFieldIdsOpenCRVSCompatible(values),
props.requiredErrorMessage
)
}
Expand Down
Loading
Loading