HomeUniteUs
HomeUniteUs copied to clipboard
Generated intake profile fields
This PR outlines a basic approach to rendering intake profiles based on objects defined in the database. This isn't a complete example, but I think it allows us to visualize further development of the data model to help fit our needs.
I accidentally created this branch off of the fix-mocking-errors
branch, so I apologize for the added noise. If you would like to view only the relative changes use this link since it filters out the other files.
What changes did you make?
As I mentioned there is quite a bit of noise in this PR so I wanted to highlight some of the most relevant files and what they are doing.
Data layer:
-
app/src/services/profile.ts
:- Contains the types and API definitions of the endpoints for retrieving profiles and answers.
-
app/src/utils/test/db/profile.ts
:- Contains an array of the two intake profiles that can be rendered. You can see what the required format for the profiles might look like
-
app/src/utils/test/browser.ts
:- This file defines the handlers to be mocked/intercepted in a browser environment. You can that this functionality is only enabled in a development environment and we can exclude routes that we don't want to check.
-
app/src/utils/test/handlers/profile.ts
:- This file contains the mocked handlers for the endpoints used in
app/src/services/profile.ts
. You can see that the endpoint/api/profile/:profileId
returns the profileapp/src/utils/test/db/profile.ts
based on the id passed in as an argument. The endpoint/api/profile/answers/:userId
returns an empty object for now since I have to define and answers array, but the components have been tested with both an empty answers array and a partial answers array. - This is an example of how we can the Mock Service Worker library to mock out API requests in a browser environment during development when the endpoints are not readily available. These mocks can then be used for tests as well.
- This file contains the mocked handlers for the endpoints used in
View layer:
-
app/src/views/IntakeProfile/index.tsx
:- Gets the
profileId
andgroupId
from the URL. TheprofileId
is used to fetch the corresponding profile and fields from the server. ThegroupId
is used to determine which field group/section to render. -
useGetProfileQuery
makes a get request using theprofileId
to return the given profile -
useGetAnswersQuery
makes a get request using theuserId
to return all answers associated with a user. This is using a fixed id and returns an empty array for now. -
buildValidationSchema
uses thefieldGroups
from the returned profile and the groupId to build a validation schema for the group. More on the details of this below. -
createInitialValues
uses the returnfieldGroups
andanswers
to create an object used as the initial values for Formik. More details on this below. - Provides General layout for the profile view.
- Creates a Formik context that handles the storage of all initial values and validation schemas. As well as the propagation of form values and errors to child components.
- Iterates over the
fieldGroups
and renders a list of links on the sidebar that update the URL with the associated fieldGroup id. This determines which fieldGroup's fields to render. - An
Outlet
component is used which is a placeholder component provided byreact-router
that is replaced with theFieldGroupList
component. More on that below. ThisOutlet
also provides a context, which we use to pass values down to theFieldGroupList
. -
onSubmit
merges the updated answers with the existing answers objects, and if there are no validation errors, will create an alert with the answers to be submitted.
- Gets the
-
app/src/views/IntakeProfile/constants/index.ts
-
fieldGroupBuilder
andfieldBuilder
are helper functions that generate random field groups and fields. -
createInitialValues
creates an object of initial values forFormik
. The object is this shape:-
{ [fieldGroupId]: { [fieldId]: answer .... } .... }
- You'll see that most of the field components have a name property of
fieldGroupId.fieldId
. This is how Formik handles nested data structures. - It also utilizes a function
fieldDefaultValue
. This returns a default value based on the field type if an answer doesn't exist.
-
-
buildValidationSchema
creates a validation schema for the field group with a similar structure tocreateInitialValues
which isfieldGroupId.fieldId = schema
.- This utilizes another function
createFieldValidationSchema
which creates a schema at the field level that takes the schema based on the field type and merges it with any further requirements. This is not fully fleshed out and needs more work, but shows how it would work for required fields and fields that have a constraint necessary for being required usingrequired_if
property.
- This utilizes another function
- I think most of the complexity of this approach lies in these two functions. However, I think the complexity is offset by some of the benefits listed below.
-
- Created the
IntakeProfileGroups
components which render all the fields in a field group using a switch statement to render the required field based on the field type. Many of these are basic field types that could be seen throughout the profile. -
app/src/views/IntakeProfile/hooks/useFieldGroups.ts
Generates field groups and answers but is replaced by the profiles service. Could probably be removed. -
app/src/components/IntakeProfile/IntakeProfileGroups.tsx
Contains two components:-
FieldGroupList
uses thefieldGroups
andgroupId
to find the rightfieldGroup
and iterates over its field. Each field is passed to theRenderFields
component which uses a switch statement to render the corresponding field based on the field type. I think there are opportunities for refactoring here to make things a bit cleaner.
-
-
views/constanst/intakeProfile
contains sets of types describing the view model, a set of helper functions to build groups and fields, as well as validation schemas for each field type. -
app/src/components/IntakeProfile/AdditionaGuestsField.tsx
I wanted to see what it was like adding a more complex field to this implementation and found it wasn't too bad.
Rationale behind the changes?
I wanted to explore what was possible when using a data model that describes the intake profile to generate the necessary fields.
Pros:
- The primary benefit of this approach is that we only need to write this once and it can be used for all profiles. All we have to do is fetch the predefined profile by its id and merge that information with the fetched answers.
- Less development work overall
- Is closer to our goal of having this work for any organization. If you can imagine this app being used by many different host home organizations. Instead of having to manually build out each intake profile, we just need to define the structure, build out any custom components and validations we don't already support and it's ready to go.
Cons:
- Some added complexity and if the design changes drastically could be difficult to refactor.
- This covers some of the more complicated cases, but not all. However, I'm confident we can find solutions with the groundwork laid out.
- Will require good documentation in case there is turnover so new members can understand the system.
What did you learn or can share that is new?(optional)
- Passing values to
Outlet
components: https://reactrouter.com/en/main/components/outlet - Handling nested
Formik
fields: https://formik.org/docs/guides/arrays
Resources
The data model is heavily inspired by Typeform's API with a few tweaks:
- https://www.typeform.com/developers/responses/JSON-response-explanation/
- https://www.typeform.com/developers/create/reference/create-form/
Data Model
erDiagram
INTAKE_PROFILE ||--|{ FIELD_GROUP : contains
INTAKE_PROFILE {
string id PK
string name
}
TYPE {
string id PK
string type "long_text, short_text, multiple_choice, yes_no, email, phone_number"
}
FIELD_GROUP ||--|{ FIELD : has
FIELD_GROUP {
string id PK
string profile_id FK
int order
string title
}
FIELD ||--|| PROPERTIES : has
FIELD ||--|| VALIDATIONS : has
FIELD ||--|| TYPE : has
FIELD ||--|| ANSWER : has
FIELD {
string id PK
string field_group_id FK
string type_id FK
int order
string title
}
PROPERTIES ||--|{ CHOICES : "can contain"
PROPERTIES {
string id PK
string field_id FK
string description "all"
boolean randomize "multiple_choice, dropdown"
boolean alphabetical_order "multiple_choice, dropdown"
boolean allow_multiple_selection "multiple_choice"
boolean allow_other_choice "multiple_choice"
}
VALIDATIONS {
string id PK
string field_id FK
boolean is_required "all"
int max_characters "short_text, long_text"
}
CHOICES {
string id PK
string properties_id FK
string label
}
ANSWER {
string id PK
string field_id FK
int user_id FK
jsonb value "unsure of the best way to define this type since it needs to cover a many different data types (e.g)"
}
Something that I believe is worth considering is whether we need to store the INTAKE_PROFILE
, FIELD_GROUP
, FIELD
along with the PROPERTIES
, and VALIDATIONS
in the database at all. These only describe the intake profile and are not created, updated, or deleted by users. An alternative might be to define the two intake profiles in a more readable file format (like yaml) that could be read and parsed on both ends for their necessary purposes. These files could then act as a source of truth for the shape of the intake profiles and we wouldn't have to go through updating the database if you wanted to change the structure of the profile. Also, since the data model is still very much in flux it would make it easier to make updates to it. I think this would cut down on the complexity of the backend data model and allow to store just the information related to the users.
Example Field Group
{
id: '7767',
title: 'Personal Information',
fields: [
{
id: '3029',
title: 'What is your email?',
type: 'email',
properties: {},
validations: {},
},
{
id: '2584',
title:
'What is your work history?',
type: 'long_text',
properties: {},
validations: {},
},
{
id: '3271',
title:
'What is your phone number?.',
type: 'number',
properties: {},
validations: {},
},
],
},
}
Screenshots of Proposed Changes Of The Website (if any, please do not screen shot code changes)
This video shows profiels 1 and 2 being rendered along with validation.
https://github.com/hackforla/HomeUniteUs/assets/27253583/6bb044fe-648b-4687-9802-ade3de1d4ea9