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 6 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
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
112 changes: 74 additions & 38 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,37 @@
*
* 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,
IFormFieldValue,
IFormSectionData,
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,
FileFieldValue
} from '@opencrvs/commons/client'
import {
Field,
FieldProps,
Expand All @@ -26,30 +53,7 @@ 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'

const fadeIn = keyframes`
from { opacity: 0; }
Expand All @@ -68,7 +72,7 @@ interface GeneratedInputFieldProps {
fieldDefinition: FieldConfig
fields: FieldConfig[]
values: IFormSectionData
setFieldValue: (name: string, value: IFormFieldValue) => void
setFieldValue: (name: string, value: FieldValue) => void
onClick?: () => void
onChange: (e: React.ChangeEvent) => void
onBlur: (e: React.FocusEvent) => void
Expand Down Expand Up @@ -133,6 +137,11 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
intl.formatMessage(fieldDefinition.placeholder)
}

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

if (fieldDefinition.type === DATE) {
return (
<InputField {...inputFieldProps}>
Expand Down Expand Up @@ -166,17 +175,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,6 +188,13 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
</InputField>
)
}
if (fieldDefinition.type === 'FILE') {
return (
<InputField {...inputFieldProps}>
<FileInput {...inputProps} onChange={handleFileChange} />
</InputField>
)
}
return <div>Unsupported field type {fieldDefinition.type}</div>
}
)
Expand Down Expand Up @@ -328,13 +333,25 @@ 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) => {
const newField = { ...field }
newField.id = field.id.replaceAll('.', FIELD_SEPARATOR)
return newField
})

return (
<section>
{fields.map((field) => {
Expand Down Expand Up @@ -391,6 +408,25 @@ class FormSectionComponent extends React.Component<AllProps> {
}
}

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document this very carefully why we do this, it's so annoying :(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup :D


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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import * as React from 'react'
import styled from 'styled-components'
import { IFileValue, IAttachmentValue } from '@client/forms'
import { Spinner } from '@opencrvs/components/lib/Spinner'
import { ISelectOption } from '@opencrvs/components/lib/Select'
import { Link } from '@opencrvs/components/lib/Link/Link'
import { Icon } from '@opencrvs/components/lib/Icon/Icon'
import { Button } from '@opencrvs/components/lib/Button/Button'

const Wrapper = styled.div`
max-width: 100%;
& > *:last-child {
margin-bottom: 8px;
border-bottom: 1.5px solid ${({ theme }) => theme.colors.grey100};
}
`
const Container = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
border-top: 1.5px solid ${({ theme }) => theme.colors.grey100};
height: 48px;
padding: 0px 10px;
`

const SpinnerContainer = styled(Spinner)`
margin-right: 6px;
`

const Label = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
svg {
flex-shrink: 0;
}
`

type Props = {
id?: string
documents?: IFileValue[] | null
processingDocuments?: Array<{ label: string }>
attachment?: IAttachmentValue
label?: string
onSelect: (document: IFileValue | IAttachmentValue) => void
dropdownOptions?: ISelectOption[]
onDelete?: (image: IFileValue | IAttachmentValue) => void
inReviewSection?: boolean
}

export const DocumentListPreview = ({

Check failure on line 67 in packages/client/src/v2-events/components/forms/inputs/FileInput/DocumentListPreview.tsx

View workflow job for this annotation

GitHub Actions / test (packages/client)

Expected a function declaration
id,
documents,
processingDocuments,
attachment,
label,
onSelect,
dropdownOptions,
onDelete,
inReviewSection
}: Props) => {
const getFormattedLabelForDocType = (docType: string) => {

Check failure on line 78 in packages/client/src/v2-events/components/forms/inputs/FileInput/DocumentListPreview.tsx

View workflow job for this annotation

GitHub Actions / test (packages/client)

Expected a function declaration
const matchingOptionForDocType =
dropdownOptions &&
dropdownOptions.find((option) => option.value === docType)
return matchingOptionForDocType && matchingOptionForDocType.label
}
return (
<Wrapper id={`preview-list-${id}`}>
{documents &&
documents.map((document: IFileValue, key: number) => (
<Container key={`preview_${key}`}>
<Label>
<Icon color="grey600" name="Paperclip" size="large" />
<Link
id={`document_${(document.optionValues[1] as string).replace(
/\s/g,
''
)}_link`}
key={key}
onClick={(_) => onSelect(document)}
>
<span>
{(inReviewSection &&
dropdownOptions &&
dropdownOptions[key]?.label) ||
getFormattedLabelForDocType(
document.optionValues[1] as string
) ||
(document.optionValues[1] as string)}
</span>
</Link>
</Label>
{onDelete && (
<Button
id="preview_delete"
type="icon"
size="small"
aria-label="Delete attachment"
onClick={() => onDelete(document)}
>
<Icon color="red" name="Trash" size="small" />
</Button>
)}
</Container>
))}
{processingDocuments &&
processingDocuments.map(({ label }) => (
<Container key={label}>
<Label>
<Icon color="grey400" name="Paperclip" size="large" />
<Link disabled={true} key={label}>
<span>{getFormattedLabelForDocType(label) || label}</span>
</Link>
</Label>
<SpinnerContainer size={20} id={`document_${label}_processing`} />
</Container>
))}

{attachment && label && (
<Container>
<Label>
<Icon color="grey600" name="Paperclip" size="medium" />
<Link onClick={(_) => onSelect(attachment)}>
<span>{getFormattedLabelForDocType(label) || label}</span>
</Link>
</Label>
<Button
id="preview_delete"
type="icon"
size="small"
aria-label="Delete attachment"
onClick={() => onDelete && onDelete(attachment)}
>
<Icon color="red" name="Trash" size="small" />
</Button>
</Container>
)}
</Wrapper>
)
}
Loading
Loading