Skip to content

Commit

Permalink
feat(webforms): Init webforms (#201)
Browse files Browse the repository at this point in the history
* build out a little more

* Use Merriweather for section headers

* Resolve radio button spacing issues

* Update full title for font testing

* Fix trash icon spacing

* initial field groups and unregistering

* feat: remove option for groups and fix shared field

- remove option for groups greater than 1
- fix fields pointing to the same form spot
- should unregister off till render issue fixed
- add separator

* remove logs

* Resolve font size and spacing issues

* Add border and top gradient to form

* Add top padding after adding border/gradient

* feat(webform): doc initializer

* feat(webform): doc initializer

* feat(webform): doc initializer

* add the rest of abp 1 data

* minor name tweaks

* add scroll for select to account for quantity

* Create useGetForm

* Test useGetForm

* Export useGetForm

* add accessibility issues

* Temporarily disable authorizer

* Temporarily hard-code formId

* Wrap returns in response

* Temp. disable 400 response

* Update index.tsx

* Update index.tsx

* Re-enable error 400 response

* Update index.tsx

* Update index.tsx

* WIP: Receive params instead of a body for GET req

* WIP: Use query params

* Cleanup

* Cleanup

* Reenable AWS IAM roles for forms endpoint

* Use formID variable as query key

* Add a TODO reminder to use the Document type

* continue to run accessibilities

* feat(webform): doc validator

* feat(webform): doc validator

* fix(OY-25433): WIP filter changes

* Fix routing issue

* add type

* working through date logic

* Update FilterableDateRange.tsx

* Ensure text size is consistent across form and with Figma

* Labels should not be bold by default according to designs

* Adjust leading to better reflect design

* Adjust form title spacing to match design

* Update ABP1 copy to match Figma

* add route back to temp form

* push calendar logic

* feat(misc-inputs): Handle non standard numerical inputs

* little cleanup

* updated ui logic

* adjust date clearing logic

* add unregister effect in Slot comp

* Add required attribute to abp1

* fix: change switch to radio with children

* fix accessibility and race condition

* add end of day logic

* fix: change to singular checkbox with child

* fix: remove fieldArray-fieldGroup undefined error

* fix: remove default Select and Radio options

* fix: select auto-sized for values

* Update FilterableDateRange.tsx

* Update FilterableDateRange.tsx

* fix: fields no longer sharing names

* fix: account for sub-group groups

* fix test

* fix end of day logic in range

* first pass through Padma feedback

* text changes

* clean logic

* fix: name mismatch

* add char limit to fields and text changes

* text changes

* trying new logic with new object

* add get logic

* types

* adjust types

* adjust types

* Update index.tsx

* type check

* Update initializer.ts

* Update index.tsx

* Update index.tsx

* Add a Register button for logged-out users

* fix: switch now changing values in form

* Add webforms page and conditional header link

* feat: add select sorting

* testing logic

* Update forms.ts

* fix logic

* running type logic

* Update forms.ts

* adjust build

* add build logic

* Update serverless.yml

* Update serverless.yml

* Update tsconfig.json

* Update package.json

* Update tsconfig.json

* Set up basic route for user profile

* Add Profile route to ROUTES

* add logic

* Update forms.ts

* Update forms.ts

* nodenext

* log

* Update package.json

* adjust logic

* Update serverless.yml

* Update serverless.yml

* Update serverless.yml

* Update serverless.yml

* Update serverless.yml

* fix(char-limit): remove character limit in abp1

* Update serverless.yml

* Update serverless.yml

* Update serverless.yml

* change file

* Update forms.ts

* .js

* Update forms.ts

* next

* Update forms.ts

* Update serverless.yml

* itteratting

* itteratting

* Update v1.ts

* Update tsconfig.json

* Update package.json

* Update forms.ts

* Update forms.ts

* json

* Update package.json

* Update tsconfig.json

* adding logic

* cleanup

* Clean up logic

* update webforms link check

* Move form into Webform component dir

* Improve spacing

* Remove Access box for now

* Add alert banner

* add save draft

* Move form schema types to shared folder

* Cleanup

* run logic

* Additional cleanup

* Update index.tsx

* pra disclosure

* Update package.json

* Add isAuth to webform link condition

* adjust logic with new webforms pull

* Updated label per design feedback

* Set up structure for dropdown menu

* Add links to dropdown

* Update menu colors and layout

* Add an arrow to indicate dropdown menu

* add data check

* Improve layout to better match design

* Parmaeterize the url to idm

* removed nulls

* bump

* guarentee formschema

* Add a nicer default

* add logic in case of no data

* rename footer and temp remove back button

* footer rename and relocation

* Use self-closing tag

* Remove cruft

* Parameterize idm url

* one more

* Remove unused consts

* Add parameterized IDM link

* Move rolesDescriptions to user.ts

* Improve My Account menu styling

* Store role descriptions as object and remove function

* Get role description strings from shared types

* Simplify type

* Update src/services/ui/src/components/Layout/index.tsx

Co-authored-by: Benjamin Paige <[email protected]>

* Improve a11y

* Restore top level H1

* coonvert form regex for json transmission

* small fix

* var name change

* Prettier

* add new diagram

* smaller svg

* adjust a thing

---------

Co-authored-by: Gavin St. Ours <[email protected]>
Co-authored-by: Daniel Belcher <[email protected]>
Co-authored-by: Paul Kim <[email protected]>
Co-authored-by: Paul Kim <[email protected]>
Co-authored-by: James Dinh <[email protected]>
Co-authored-by: James Dinh <[email protected]>
Co-authored-by: Daniel Belcher <[email protected]>
Co-authored-by: Mike Dial <[email protected]>
  • Loading branch information
9 people authored Dec 1, 2023
1 parent 6c53fda commit 87bc5f3
Show file tree
Hide file tree
Showing 75 changed files with 3,553 additions and 563 deletions.
4 changes: 2 additions & 2 deletions docs/assets/diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/docs/services/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ The api service deploys a lambda-backed API Gateway that is used by the frontend

The largest component of the api service is the API Gateway itself. This is a standard deployment of a regional, REST API Gateway. We do not apply custom certificates or DNS names to the api gateway endpoint (yet); instead, our application uses the amazon generated SSL endpoint.

There are three endpoints on the api. Each is guarded by AWS IAM, meaning that while the API Gateway is publicly available, the API will not forward your request to the backing lambda unless you provide valid credentials obtained through AWS Cognito. This way, only users with an account that we can authenticate may successfully call endpoints. The three endpoints are:
There are four endpoints on the api. Each is guarded by AWS IAM, meaning that while the API Gateway is publicly available, the API will not forward your request to the backing lambda unless you provide valid credentials obtained through AWS Cognito. This way, only users with an account that we can authenticate may successfully call endpoints. The four endpoints are:
- /search (POST): This endpoint accepts search queries from clients in the form of OpenSearch Query DSL queries. Once the query is received, the lambda adds extra query filters to ensure fine grain auth. This works by looking up the user making the call in Cognito, determining what type of user (cms or state) is making the call, determining what states that user has access to (if appropriate), and modifying the query in a way that will only return results for those states. By design, the only thing the search endpoint adds is related to authentication; the rest of the query building is left to the frontend for faster and more flexible development.
- /item (POST): The item endpoint is used to fetch details for exactly one record. While you can form a query to do this and use the search endpoint, the item endpoint is for convenience. Simply make a post call containing the ID of the desired record to the item endpoint, and the record will be returned. Note that fine grain auth is still enforced in an identical way to search, whereby you will only obtain results for that ID if you should have access to that ID.
- /getAttachmentUrl (POST): This endpoint is used to generate a presigned url for direct client downloading of S3 data, enforcing fine grain auth along the way. This is how we securely allow download of submission attachment data. From the details page, a user may click a file to download. Once clicked, their client makes a post to /getAttachmentUrl with the attachment metadata. The lambda function determines if the caller should or should not have access based on identical logic as the other endpoints (the UI would not display something they cannot download, but this guards against bad actors). If access is allowed, the lambda function generates a presigned url good for 60 seconds and returns it to the client browser, at which point files are downloaded automatically.
- /forms (GET): This endpoint function serves as the backend for handling forms and their associated data. This function provides various features, including retrieving form data, validating access, and serving the requested form content. The request to this endpoint must include a formId in the request body. Optionally, you can include a formVersion parameter. If you access this endpoint with formId without specifying formVersion, it will return the latest version. Form schemas are stored in a Lambda layer. Each form is organized in its directory, and each version is stored within that directory. The Lambda layer is located in the "opt" directory when deployed to aws. To access a specific version of a form with a formId, use the following URL structure: /opt/${formId}/v${formVersion}.json. The JSON form schemas are versioned and stored in Git under the "api/layers" directory.

All endpoints and backing functions interact with the OpenSearch data layer. As such, and because OpenSearch is deployed within a VPC, all lambda functions of the api service are VPC based. The functions share a security group that allows outbound traffic.

Expand Down
141 changes: 141 additions & 0 deletions src/packages/shared-types/forms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
Control,
FieldArrayPath,
FieldValues,
RegisterOptions,
} from "react-hook-form";
import {
CalendarProps,
InputProps,
RadioProps,
SelectProps,
SwitchProps,
TextareaProps,
} from "shared-types";

export interface FormSchema {
header: string;
sections: Section[];
}

export type RHFSlotProps = {
name: string;
label?: string;
labelStyling?: string;
groupNamePrefix?: string;
description?: string;
dependency?: DependencyRule;
rules?: RegisterOptions;
} & {
[K in keyof RHFComponentMap]: {
rhf: K;
props?: RHFComponentMap[K];
fields?: K extends "FieldArray"
? RHFSlotProps[]
: K extends "FieldGroup"
? RHFSlotProps[]
: never;
};
}[keyof RHFComponentMap];

export type RHFOption = {
label: string;
value: string;
form?: FormGroup[];
slots?: RHFSlotProps[];
};

export type RHFComponentMap = {
Input: InputProps & {
label?: string;
description?: string;
};
Textarea: TextareaProps;
Switch: SwitchProps;
Select: SelectProps & { sort?: "ascending" | "descending" };
Radio: RadioProps & {
options: RHFOption[];
};
DatePicker: CalendarProps;
Checkbox: {
options: RHFOption[];
};
FieldArray: {
appendText?: string;
};
FieldGroup: {
appendText?: string;
removeText?: string;
};
};

export type FormGroup = {
description?: string;
slots: RHFSlotProps[];
wrapperStyling?: string;
dependency?: DependencyRule;
};

export interface Section {
title: string;
form: FormGroup[];
dependency?: DependencyRule;
}

export interface Document {
header: string;
sections: Section[];
}

export type FieldArrayProps<
T extends FieldValues,
TFieldArrayName extends FieldArrayPath<T> = FieldArrayPath<T>
> = {
control: Control<T, unknown>;
name: TFieldArrayName;
fields: RHFSlotProps[];
groupNamePrefix?: string;
appendText?: string;
};

export type FieldGroupProps<
T extends FieldValues,
TFieldArrayName extends FieldArrayPath<T> = FieldArrayPath<T>
> = {
control: Control<T, unknown>;
name: TFieldArrayName;
fields: RHFSlotProps[];
appendText?: string;
removeText?: string;
groupNamePrefix?: string;
};

type ConditionRules =
| {
type: "valueExists" | "valueNotExist";
}
| {
type: "expectedValue";
expectedValue: unknown;
};

type Condition = { name: string } & ConditionRules;

type Effects =
| {
type: "show" | "hide";
}
| {
type: "setValue";
newValue: unknown;
};

export interface DependencyRule {
conditions: Condition[];
effect: Effects;
}

export interface DependencyWrapperProps {
name?: string;
dependency?: DependencyRule;
}
2 changes: 2 additions & 0 deletions src/packages/shared-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from "./actions";
export * from "./attachments";
export * from "./authority";
export * from "./action-types/withdraw-record";
export * from "./forms";
export * from "./inputs";
45 changes: 45 additions & 0 deletions src/packages/shared-types/inputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { DayPicker } from "react-day-picker";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as SelectPrimitive from "@radix-ui/react-select";

export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
className?: string;
classNames?: any;
showOutsideDays?: boolean;
};

export type DatePickerProps = {
date: Date | undefined;
onChange: (date: Date | undefined) => void;
};

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
icon?: string;
}

export type RadioProps = React.ComponentPropsWithoutRef<
typeof RadioGroupPrimitive.Root
> & {
className?: string;
};

export type SelectProps = React.ComponentPropsWithoutRef<
typeof SelectPrimitive.Root
> & {
options: { label: string; value: any }[];
className?: string;
};

export type SwitchProps = React.ComponentPropsWithoutRef<
typeof SwitchPrimitives.Root
> & {
className?: string;
};

export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
charcount?: "simple" | "limited";
charcountstyling?: string;
}
7 changes: 7 additions & 0 deletions src/packages/shared-types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ export const CMS_READ_ONLY_ROLES = [
];

export const STATE_ROLES = [UserRoles.STATE_SUBMITTER];

export const RoleDescriptionStrings: { [key: string]: string } = {
[UserRoles.CMS_READ_ONLY]: "Read Only",
[UserRoles.CMS_REVIEWER]: "Reviewer",
[UserRoles.HELPDESK]: "Helpdesk",
[UserRoles.STATE_SUBMITTER]: "State Submitter",
};
1 change: 1 addition & 0 deletions src/packages/shared-utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./user-helper";
export * from "./s3-url-parser";
export * from "./rai-helper"
export * from "./regex"
43 changes: 43 additions & 0 deletions src/packages/shared-utils/regex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { it, describe, expect } from "vitest";
import { convertRegexToString, reInsertRegex } from "./regex";

const testRegex = /^-?\d*\.?\d+$/g

const testForm = {
label: "SSI federal benefit amount",
value: "ssi_federal_benefit_amount",
slots: [
{
rhf: "Input",
name: "ssi_federal_benefit_percentage",
label: "Enter the SSI Federal Benefit Rate percentage",
props: {
icon: "%",
},
rules: {
pattern: {
value: testRegex,
message: "Must be a percentage",
},
required: "* Required",
},
},
],
};

describe("form regex", () => {
it("conversion logic should work", () => {
const result = convertRegexToString(testForm)
const val = result.slots[0].rules.pattern.value

const jsonVal = JSON.stringify(val)
expect(jsonVal).toBeTypeOf('string')

const parsedVal = JSON.parse(jsonVal)

const restoredRegex = new RegExp(parsedVal[1], parsedVal[2]);
expect(restoredRegex).toEqual(testRegex)
expect(reInsertRegex(result)).toEqual(testForm)

});
})
44 changes: 44 additions & 0 deletions src/packages/shared-utils/regex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const convertRegexToString = (obj: any) => {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];

if (value instanceof RegExp) {
// Convert RegExp to string
const str = value.toString();
// save it in this weird array thing
obj[key] = /\/(.*)\/(.*)/.exec(str);
} else if (typeof value === "object" && value !== null) {
// Recursively process nested objects
obj[key] = convertRegexToString(value);
}
}
}

return obj;
};

export const reInsertRegex = (obj: any) => {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === "object" && obj[key] !== null) {
// If the current property is an object, recursively call the function
obj[key] = reInsertRegex(obj[key]);

// Check if the current object has a property "pattern" with a "value" key
if (
obj[key].hasOwnProperty("pattern") &&
typeof obj[key].pattern === "object" &&
obj[key].pattern.hasOwnProperty("value")
) {
// if its a pattern.value replace the value's value with a regex from the weird array thing
obj[key].pattern.value = new RegExp(
obj[key].pattern.value[1],
obj[key].pattern.value[2]
);
}
}
}
}
return obj;
};
Loading

0 comments on commit 87bc5f3

Please sign in to comment.