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

Generated intake profile fields #672

Merged
merged 40 commits into from
Jun 9, 2024
Merged

Generated intake profile fields #672

merged 40 commits into from
Jun 9, 2024

Conversation

erikguntner
Copy link
Collaborator

@erikguntner erikguntner commented Apr 3, 2024

Closes: #610

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 profile app/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.

View layer:

  • app/src/views/IntakeProfile/index.tsx:
    • Gets the profileId and groupId from the URL. The profileId is used to fetch the corresponding profile and fields from the server. The groupId is used to determine which field group/section to render.
    • useGetProfileQuery makes a get request using the profileId to return the given profile
    • useGetAnswersQuery makes a get request using the userId to return all answers associated with a user. This is using a fixed id and returns an empty array for now.
    • buildValidationSchema uses the fieldGroups 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 return fieldGroups and answers 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 by react-router that is replaced with the FieldGroupList component. More on that below. This Outlet also provides a context, which we use to pass values down to the FieldGroupList.
    • 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.
  • app/src/views/IntakeProfile/constants/index.ts
    • fieldGroupBuilder and fieldBuilder are helper functions that generate random field groups and fields.
    • createInitialValues creates an object of initial values for Formik. 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 to createInitialValues which is fieldGroupId.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 using required_if property.
    • 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 the fieldGroups and groupId to find the right fieldGroup and iterates over its field. Each field is passed to the RenderFields 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)

Resources

The data model is heavily inspired by Typeform's API with a few tweaks:

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)"
    }
Loading

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.

Screen.Recording.2024-04-08.at.11.46.19.AM.mov

@@ -0,0 +1,287 @@
/* eslint-disable */
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This file is generated by MSW to integrate with the browser. Don't worry about reviewing it.

@Joshua-Douglas Joshua-Douglas mentioned this pull request Apr 19, 2024
7 tasks
@erikguntner erikguntner marked this pull request as ready for review April 23, 2024 23:22
Copy link
Member

@JpadillaCoding JpadillaCoding left a comment

Choose a reason for hiding this comment

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

Made a suggestion to help with input data validation, but apart from that this PR looks great and a solid foundation for our profile fields. Thanks!

@JpadillaCoding
Copy link
Member

@erikguntner I agree the validation issue is to be it's own ticket. I don't see any other issues to address and this should be ready to merge 👍🏽

@erikguntner erikguntner merged commit 9e80e79 into main Jun 9, 2024
2 checks passed
@erikguntner erikguntner deleted the feat/generated-profile branch June 9, 2024 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Guest application implementation
2 participants