-
Notifications
You must be signed in to change notification settings - Fork 65
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
Adding file attachment support #128
Adding file attachment support #128
Conversation
…rodidev/openmrs-form-engine-lib into RFE-add-file-rendering-support
@kajambiya kindly point out the variable in question |
@arodidev have you tested this out in edit mode? If so, does it initialize the field value as expected? |
Not yet, will look into this. |
export function dataURItoFile(dataURI: string) { | ||
const byteString = atob(dataURI.split(',')[1]); | ||
const mimeString = dataURI | ||
.split(',')[0] | ||
.split(':')[1] | ||
.split(';')[0]; | ||
|
||
// write the bytes of the string to a typed array | ||
const buffer = new Uint8Array(byteString.length); | ||
|
||
for (let i = 0; i < byteString.length; i++) { | ||
buffer[i] = byteString.charCodeAt(i); | ||
} | ||
|
||
const blob = new Blob([buffer], { type: mimeString }); | ||
return { blob, mimeString }; | ||
} | ||
|
||
export function fileToBase64(fileObject) { | ||
return new Promise((resolve, reject) => { | ||
const reader = new FileReader(); | ||
|
||
reader.onloadend = () => { | ||
const base64String = reader.result; | ||
resolve(base64String); | ||
}; | ||
|
||
reader.onerror = error => { | ||
reject(error); | ||
}; | ||
|
||
reader.readAsDataURL(fileObject); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we plan on referencing these functions within business logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will remove, had used them in a prior implementation.
const myInitVal = useMemo(() => { | ||
const initialValuesObject = encounterContext.initValues; | ||
const attachmentValue = Object.keys(initialValuesObject) | ||
.filter((key) => key === question.id) | ||
.reduce((cur, key) => { | ||
return Object.assign(cur, { [key]: initialValuesObject[key] }); | ||
}, {}); | ||
return attachmentValue; | ||
}, [encounterContext]); | ||
|
||
const attachmentValue = useMemo(() => { | ||
const firstValue = Object?.values(myInitVal)[0]; | ||
if (firstValue) { | ||
const attachment = createGalleryEntry(firstValue?.[0]); | ||
return attachment; | ||
} | ||
}, [myInitVal]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By design, a typical visual component isn't supposed to know how to pick these values from the encounter context. We delegate these tasks to Field Handlers. This was intended to minimize coupling (UI with business logic) and ensure a separation of concerns. Just for consistency, I think you wanna introduce a FileSubmissionHandler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense, will implement.
.filter(field => isEmpty(field.value)) | ||
.forEach(field => { | ||
.filter((field) => isEmpty(field.value)) | ||
.filter((field) => field.questionOptions.rendering !== 'file') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you follow the handler patterns, this change wouldn't be necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted
useEffect(() => { | ||
const emptyValues = { | ||
checkbox: [], | ||
toggle: false, | ||
default: '', | ||
}; | ||
const attachmentFields = formFields.filter((field) => field.questionOptions.rendering === 'file'); | ||
|
||
if (attachmentFields.length) { | ||
if (encounter) { | ||
Promise.all( | ||
attachmentFields.map((field) => { | ||
return formFieldHandlers[field.type]?.getInitialValue(encounter, field, formFields); | ||
}), | ||
).then((responses) => { | ||
responses.forEach((responseValue, index) => { | ||
const eachField = attachmentFields[index]; | ||
|
||
const filteredResponseValue = responseValue['results'].filter( | ||
(eachResponse) => eachResponse.comment === eachField.id, | ||
); | ||
|
||
initialValues[eachField.id] = isEmpty(responseValue) | ||
? emptyValues[eachField.questionOptions.rendering] ?? emptyValues.default | ||
: filteredResponseValue; | ||
|
||
setInitialValues({ ...initialValues }); | ||
}); | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, let's put this in a handler
src/utils/common-utils.ts
Outdated
export function createGalleryEntry(data: AttachmentResponse): Attachment { | ||
const attachmentUrl = '/ws/rest/v1/attachment'; | ||
return { | ||
id: data.uuid, | ||
src: `${window.openmrsBase}${attachmentUrl}/${data.uuid}/bytes`, | ||
title: data.comment, | ||
description: '', | ||
dateTime: formatDate(new Date(data.dateTime), { | ||
mode: 'wide', | ||
}), | ||
bytesMimeType: data.bytesMimeType, | ||
bytesContentFamily: data.bytesContentFamily, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's a gallery entry? Do we have such a domain defined within the library?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Picked this implementation from esm-patient-attachments-app, which converts the attachment response into a usable attachment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I understand, within the context of that esm, it makes sense to use the term "gallery" because there is an actual gallery but we don't have one here. Can you revise the naming?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
.forEach((field) => { | ||
return saveAttachment( | ||
encounter?.patient.uuid, | ||
field, | ||
field?.questionOptions.concept, | ||
new Date().toISOString(), | ||
encounter?.uuid, | ||
ac, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again as I suggested before, can we resolve these in parallel?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, will implement
useEffect(() => { | ||
if (tempInitialValues) { | ||
setInitValues(tempInitialValues); | ||
} | ||
}, [tempInitialValues]); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean the useEffect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm asking because when introducing new rendering types most of the time we don't have reason to touch this level of code as it's already abstracted at a high level, that's assuming the standard patterns are followed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once the initial values are loaded from useInitialValues hook, I am passing down the initial values to the field via context, hence this snippet which updates the state variable holding the initial values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, you don't need this change if you properly follow the standard patterns. Kindly consider introducing a handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do, thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we also have a session to discuss this further? There are some areas I need clarity on.
@samuelmale this PR needs to be merged. We will work on the handler next sprint (in two weeks), can you please undo your change request ? |
Requirements
Summary
This PR adds support for file attachment capture and submission to the attachments endpoint, via upload or via camera capture.
Screenshots
screen-recording-2023-10-08-at-135526_uv2wAQ9I.mp4
Related Issue
Other