diff --git a/docs/assets/diagram.svg b/docs/assets/diagram.svg index 6f66f48fea..ba240422ab 100644 --- a/docs/assets/diagram.svg +++ b/docs/assets/diagram.svg @@ -1,4 +1,4 @@ - + - OktaKibana \ No newline at end of file + OktaKibana \ No newline at end of file diff --git a/docs/docs/services/api.md b/docs/docs/services/api.md index dc048d0676..5e78460668 100644 --- a/docs/docs/services/api.md +++ b/docs/docs/services/api.md @@ -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. diff --git a/src/packages/shared-types/forms.ts b/src/packages/shared-types/forms.ts new file mode 100644 index 0000000000..b9d3de993f --- /dev/null +++ b/src/packages/shared-types/forms.ts @@ -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 = FieldArrayPath +> = { + control: Control; + name: TFieldArrayName; + fields: RHFSlotProps[]; + groupNamePrefix?: string; + appendText?: string; +}; + +export type FieldGroupProps< + T extends FieldValues, + TFieldArrayName extends FieldArrayPath = FieldArrayPath +> = { + control: Control; + 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; +} diff --git a/src/packages/shared-types/index.ts b/src/packages/shared-types/index.ts index e7e882693e..675bb55661 100644 --- a/src/packages/shared-types/index.ts +++ b/src/packages/shared-types/index.ts @@ -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"; diff --git a/src/packages/shared-types/inputs.ts b/src/packages/shared-types/inputs.ts new file mode 100644 index 0000000000..022a9da035 --- /dev/null +++ b/src/packages/shared-types/inputs.ts @@ -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 & { + className?: string; + classNames?: any; + showOutsideDays?: boolean; +}; + +export type DatePickerProps = { + date: Date | undefined; + onChange: (date: Date | undefined) => void; +}; + +export interface InputProps + extends React.InputHTMLAttributes { + 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 { + charcount?: "simple" | "limited"; + charcountstyling?: string; +} diff --git a/src/packages/shared-types/user.ts b/src/packages/shared-types/user.ts index ae39bac554..48922698a8 100644 --- a/src/packages/shared-types/user.ts +++ b/src/packages/shared-types/user.ts @@ -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", +}; diff --git a/src/packages/shared-utils/index.ts b/src/packages/shared-utils/index.ts index 9e2cecc21c..431d5bf46b 100644 --- a/src/packages/shared-utils/index.ts +++ b/src/packages/shared-utils/index.ts @@ -1,3 +1,4 @@ export * from "./user-helper"; export * from "./s3-url-parser"; export * from "./rai-helper" +export * from "./regex" diff --git a/src/packages/shared-utils/regex.test.ts b/src/packages/shared-utils/regex.test.ts new file mode 100644 index 0000000000..c0866e0ec2 --- /dev/null +++ b/src/packages/shared-utils/regex.test.ts @@ -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) + + }); +}) \ No newline at end of file diff --git a/src/packages/shared-utils/regex.ts b/src/packages/shared-utils/regex.ts new file mode 100644 index 0000000000..22a08336f4 --- /dev/null +++ b/src/packages/shared-utils/regex.ts @@ -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; +}; diff --git a/src/services/api/handlers/forms.ts b/src/services/api/handlers/forms.ts new file mode 100644 index 0000000000..9d8fe21f89 --- /dev/null +++ b/src/services/api/handlers/forms.ts @@ -0,0 +1,101 @@ +import { response } from "../libs/handler"; +import * as fs from "fs"; +import { APIGatewayEvent } from "aws-lambda"; +import { convertRegexToString } from "shared-utils"; + +export const forms = async (event: APIGatewayEvent) => { + try { + const formId = event.queryStringParameters?.formId?.toLocaleUpperCase(); + let formVersion = event.queryStringParameters?.formVersion; + + if (!formId) { + return response({ + statusCode: 400, + body: JSON.stringify({ error: "File ID was not provided" }), + }); + } + + const filePath = getFilepathForIdAndVersion(formId, formVersion); + + if (!filePath) { + return response({ + statusCode: 404, + body: JSON.stringify({ + error: "No file was found with provided formId and formVersion", + }), + }); + } + + const jsonData = await fs.promises.readFile(filePath, "utf-8"); + + if (!jsonData) { + return response({ + statusCode: 404, + body: JSON.stringify({ + error: `File found for ${formId}, but it's empty`, + }), + }); + } + + if (!formVersion) formVersion = getMaxVersion(formId); + + try { + const formObj = await import(`/opt/${formId}/v${formVersion}.js`); + + if (formObj?.form) { + const cleanedForm = convertRegexToString(formObj.form); + return response({ + statusCode: 200, + body: cleanedForm, + }); + } + } catch (importError) { + console.error("Error importing module:", importError); + return response({ + statusCode: 500, + body: JSON.stringify({ + error: importError.message + ? importError.message + : "Internal server error", + }), + }); + } + } catch (error) { + console.error("Error:", error); + return response({ + statusCode: 502, + body: JSON.stringify({ + error: error.message ? error.message : "Internal server error", + }), + }); + } +}; + +export function getMaxVersion(formId: string) { + const files = fs.readdirSync(`/opt/${formId}`); + if (!files) return undefined; + const versionNumbers = files?.map((fileName: string) => { + const match = fileName.match(/^v(\d+)\./); + if (match) { + return parseInt(match[1], 10); + } + return 1; + }); + return Math.max(...versionNumbers).toString(); +} + +export function getFilepathForIdAndVersion( + formId: string, + formVersion: string | undefined +): string | undefined { + if (formId && formVersion) { + return `/opt/${formId}/v${formVersion}.js`; + } + + const maxVersion = getMaxVersion(formId); + + if (!maxVersion) return undefined; + return `/opt/${formId}/v${maxVersion}.js`; +} + +export const handler = forms; diff --git a/src/services/api/handlers/tests/forms.test.ts b/src/services/api/handlers/tests/forms.test.ts new file mode 100644 index 0000000000..d4ea9ab2f3 --- /dev/null +++ b/src/services/api/handlers/tests/forms.test.ts @@ -0,0 +1,94 @@ +import * as fs from "fs"; +import { it, describe, expect, vi } from "vitest"; +import { forms } from "../forms"; +import { APIGatewayProxyEvent } from "aws-lambda/trigger/api-gateway-proxy"; + +describe("Forms Lambda Tests", () => { + it("should return 400 with error message if formId is not provided", async () => { + const event = { + body: JSON.stringify({}), + } as APIGatewayProxyEvent; + const result = await forms(event); + + expect(result?.statusCode).toBe(400); + expect(JSON.parse(result?.body as string)).toEqual({ + error: "File ID was not provided", + }); + }); + + it("should return 500 with error message if filePath is not found", async () => { + const event = { + body: JSON.stringify({ formId: "test", formVersion: "1" }), + } as APIGatewayProxyEvent; + const result = await forms(event); + + expect(result?.statusCode).toBe(500); + expect(JSON.parse(result?.body as string)).toEqual({ + error: "ENOENT: no such file or directory, open '/opt/test/v1.js'", + }); + }); + + it("should return 200 with JSON data if everything is valid", async () => { + vi.spyOn(fs.promises, "readFile").mockResolvedValue( + JSON.stringify({ key: "value" }) + ); + + const event = { + body: JSON.stringify({ formId: "testform", formVersion: "1" }), + } as APIGatewayProxyEvent; + const result = await forms(event); + + expect(result?.statusCode).toBe(200); + expect(result?.headers["Content-Type"]).toBe("application/json"); + }); + + it("should return 500 with a custom error message for other internal errors", async () => { + vi.spyOn(fs.promises, "readFile").mockRejectedValue( + new Error("Internal Server Error Message") + ); + + const event = { + body: JSON.stringify({ formId: "testform", formVersion: "1" }), + } as APIGatewayProxyEvent; + + const result = await forms(event); + + expect(result?.statusCode).toBe(500); + expect(JSON.parse(result?.body as string)).toEqual({ + error: "Internal Server Error Message", + }); + }); + + it("should return the correct JSON data for different file versions", async () => { + vi.spyOn(fs.promises, "readFile").mockImplementation(async (filePath) => { + const filePathString = filePath.toString(); + if (filePathString.includes("/opt/testform/v1.js")) { + return Buffer.from(JSON.stringify({ version: "1", data: "v1 data" })); + } else { + return Buffer.from(JSON.stringify({ version: "2", data: "v2 data" })); + } + }); + + const eventV1 = { + body: JSON.stringify({ formId: "testform", formVersion: "1" }), + } as APIGatewayProxyEvent; + const eventV2 = { + body: JSON.stringify({ formId: "testform", formVersion: "2" }), + } as APIGatewayProxyEvent; + + const resultV1 = await forms(eventV1); + const resultV2 = await forms(eventV2); + + expect(resultV1?.statusCode).toBe(200); + expect(resultV2?.statusCode).toBe(200); + + expect(JSON.parse(resultV1?.body as string)).toEqual({ + version: "1", + data: "v1 data", + }); + expect(JSON.parse(resultV2?.body as string)).toEqual({ + version: "2", + data: "v2 data", + }); + }); +}); diff --git a/src/services/api/layers/ABP1/v1.ts b/src/services/api/layers/ABP1/v1.ts new file mode 100644 index 0000000000..59637fd088 --- /dev/null +++ b/src/services/api/layers/ABP1/v1.ts @@ -0,0 +1,1323 @@ +import { FormSchema } from "shared-types"; + +const ABP1: FormSchema = { + header: "ABP 1: Alternative Benefit Plan populations", + sections: [ + { + title: "Population identification", + form: [ + { + description: + "Identify and define the population that will participate in the Alternative Benefit Plan.", + slots: [ + { + rhf: "Input", + name: "alt_benefit_plan_population_name", + label: "Alternative Benefit Plan population name", + rules: { + required: "* Required", + }, + props: { placeholder: "enter name" }, + }, + ], + }, + { + description: + "Identify eligibility groups that are included in the Alternative Benefit Plan's population and that may contain individuals that meet any targeting criteria used to further define the population.", + slots: [ + { + rhf: "FieldArray", + name: "eligibility_groups", + props: { + appendText: "Add group", + }, + fields: [ + { + rhf: "Select", + name: "eligibility_group", + rules: { + required: "* Required", + }, + label: "Eligibility group", + props: { + sort: "ascending", + className: "min-w-[300px]", + options: [ + { + label: "Parents and Other Caretaker Relatives", + value: "parents_caretaker_relatives", + }, + { + label: "Transitional Medical Assistance", + value: "transitional_medical_assist", + }, + { + label: "Extended Medicaid Due to Earnings", + value: "extend_medicaid_earnings", + }, + { + label: + "Extended Medicaid Due to Spousal Support Collections", + value: "extend_medicaid_spousal_support_collect", + }, + { + label: "Pregnant Women", + value: "pregnant_women", + }, + { + label: "Deemed Newborns", + value: "deemed_newborns", + }, + { + label: "Infants and Children under Age 19", + value: "infants_children_under_19", + }, + { + label: + "Children with Title IV-E Adoption Assistance, Foster Care or Guardianship Care", + value: + "children_title_IV-E_adoption_assist_foster_guardianship_care", + }, + { + label: "Former Foster Care Children", + value: "former_foster_children", + }, + { + label: "Adult Group", + value: "adult_group", + }, + { + label: "SSI Beneficiaries", + value: "ssi_beneficiaries", + }, + { + label: + "Aged, Blind and Disabled Individuals in 209(b) States", + value: "aged_blind_disabled_individuals_209b_states", + }, + { + label: + "Individuals Receiving Mandatory State Supplements", + value: + "individuals_receiving_mandatory_state_supplements", + }, + { + label: "Individuals Who Are Essential Spouses", + value: "essential_spouses", + }, + { + label: "Institutionalized Individuals Eligible in 1973", + value: "institutionalized_eligible_1973", + }, + { + label: "Blind or Disabled Individuals Eligible in 1937", + value: "blind_disabled_eligible_1937", + }, + { + label: + "Individuals Who Lost Eligibility for SSI/SSP Due to an Increase in OASDI Benefits in 1972", + value: + "lost_eligibility_SSI_SSP_increase_in_OASDI_benefits_1972", + }, + { + label: + "Individuals Eligible for SSI/SSP but for OASDI COLA increases since April, 1977", + value: + "eligible_SSI_SSP_but_for_OASDI_COLA_increases_April_1977", + }, + { + label: + "Disabled Widows and Widowers Ineligible for SSI due to Increase in OASDI", + value: + "disabled_widows_ineligible_SSI_due_to_increase_OASDI", + }, + { + label: + "Disabled Widows and Widowers Ineligible for SSI due to Early Receipt of Social Security", + value: + "disabled_widows_ineligible_SSI_due_to_early_receipt_social_security", + }, + { + label: "Working Disabled under 1619(b)", + value: "working_disabled_under_1619b", + }, + { + label: "Disabled Adult Children", + value: "disabled_adult_children", + }, + { + label: "Qualified Medicare Beneficiaries", + value: "qualified_medicare_beneficiaries", + }, + { + label: "Qualified Disabled and Working Individuals", + value: "qualified_disabled_working_individuals", + }, + { + label: "Specified Low Income Medicare Beneficiaries", + value: "spec_low_income_medicare_beneficiaries", + }, + { + label: "Qualifying Individuals", + value: "qualifying_individuals", + }, + { + label: + "Optional Coverage of Parents and Other Caretaker Relatives", + value: "opt_coverage_parents_other_caretaker_relatives", + }, + { + label: + "Reasonable Classifications of Individuals under Age 21", + value: "reasonable_class_under_21", + }, + { + label: "Children with Non-IV-E Adoption Assistance", + value: "children_Non-IV-E_adoption_assistance", + }, + { + label: "Independent Foster Care Adolescents", + value: "independent_foster_care_adolescents", + }, + { + label: "Optional Targeted Low Income Children", + value: "opt_targeted_low_income_children", + }, + { + label: + "Individuals Electing COBRA Continuation Coverage", + value: "individuals_electing_COBRA_cont_converage", + }, + { + label: + "Certain Individuals Needing Treatment for Breast or Cervical Cancer", + value: + "individuals_need_treatment_for_breasts_cervical_cancer", + }, + { + label: "Individuals with Tuberculosis", + value: "tuberculosis", + }, + { + label: + "Aged, Blind or Disabled Individuals Eligible for but Not Receiving Cash", + value: + "aged_blind_disabled_eligible_but_not_receiving_cash", + }, + { + label: + "Individuals Eligible for Cash except for Institutionalization", + value: "eligible_cash_except_for_institutionalization", + }, + { + label: + "Individuals Receiving Home and Community Based Services under Institutional Rules", + value: + "receiving_home_community_services_under_inst_rule", + }, + { + label: + "Optional State Supplement - 1634 States and SSI Criteria States with 1616 Agreements", + value: + "opt_state_supp_1634_states_SSI_criteria_states_1616_agreements", + }, + { + label: + "Optional State Supplement - 209(b) States and SSI Criteria States without 1616 Agreements", + value: + "opt_state_supp_209b_states_SSI_criteria_states_without_1616_agreements", + }, + { + label: + "Institutionalized Individuals Eligible under a Special Income Level", + value: "inst_eligible_under_special_income_level", + }, + { + label: "Individuals Receiving Hospice Care", + value: "hospice_care", + }, + { + label: "Qualified Disabled Children under Age 19 ", + value: "qualified_disabled_children_under_19", + }, + { + label: "Poverty Level Aged or Disabled", + value: "poverty_level_aged_disabled", + }, + { + label: "Work Incentives Eligibility Group", + value: "work_incentives_eligibility_group", + }, + { + label: "Ticket to Work Basic Group", + value: "ticket_work_basic_group", + }, + { + label: "Ticket to Work Medical Improvements Group", + value: "ticket_work_medical_imp_group", + }, + { + label: + "Family Opportunity Act Children with Disabilities", + value: "family_opportunity_act_children_disabilities", + }, + { + label: "Medically Needy Pregnant Women", + value: "med_needy_pregnant_women", + }, + { + label: "Medically Needy Children under Age 18", + value: "med_needy_children_under_18", + }, + { + label: "Medically Needy Children Age 18 through 20", + value: "med_needy_age_18_through_20", + }, + { + label: "Medically Needy Parents and Other Caretakers", + value: "med_needy_parents_caretakers", + }, + { + label: "Medically Needy Aged, Blind or Disabled", + value: "med_needy_aged_blind_disabled", + }, + { + label: + "Medically Needy Blind or Disabled Individuals Eligible in 1973", + value: "med_needy_blind_disabled_eligible_1973", + }, + ], + }, + }, + { + rhf: "Select", + name: "mandatory_voluntary", + label: "Mandatory or Voluntary", + rules: { + required: "* Required", + }, + props: { + className: "w-[200px]", + options: [ + { + label: "Mandatory", + value: "mandatory", + }, + { + label: "Voluntary", + value: "voluntary", + }, + ], + }, + }, + ], + }, + ], + }, + { + description: + "Is enrollment available for all individuals in these eligibility groups?", + slots: [ + { + rhf: "Select", + name: "is_enrollment_available", + rules: { + required: "* Required", + }, + props: { + className: "w-[150px]", + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + }, + ], + }, + ], + }, + { + title: "Targeting criteria", + dependency: { + // example of conditionally hidden section + conditions: [ + { + name: "is_enrollment_available", + type: "expectedValue", + expectedValue: "no", + }, + ], + effect: { type: "show" }, + }, + form: [ + { + description: "Targeting criteria (check all that apply)", + slots: [ + { + rhf: "Checkbox", + name: "target_criteria", + rules: { + required: "* Required", + }, + props: { + options: [ + { + value: "income_standard", + label: "Income standard", + form: [ + { + description: "Income standard target", + slots: [ + { + rhf: "Radio", + name: "income_target", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: + "Households with income at or below the standard", + value: "income_target_below", + }, + { + label: + "Households with income above the standard", + value: "income_target_above", + }, + ], + }, + }, + ], + }, + { + description: "Income standard definition", + slots: [ + { + rhf: "Radio", + name: "income_definition", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "A percentage", + value: "income_definition_percentage", + slots: [ + { + rhf: "Radio", + name: "income_definition_percentage", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "Federal poverty level", + value: "federal_poverty_level", + slots: [ + { + rhf: "Input", + props: { + icon: "%", + }, + rules: { + pattern: { + value: /^-?\d*\.?\d+$/, + message: + "Must be a percentage", + }, + required: "* Required", + }, + name: "federal_poverty_level_percentage", + label: + "Enter the federal poverty level percentage", + }, + ], + }, + { + 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: /^-?\d*\.?\d+$/, + message: + "Must be a percentage", + }, + required: "* Required", + }, + }, + ], + }, + { + label: "Other", + value: "other", + slots: [ + { + rhf: "Input", + name: "other_percentage", + label: + "Enter the other percentage", + props: { + icon: "%", + }, + rules: { + pattern: { + value: /^-?\d*\.?\d+$/, + message: + "Must be a percentage", + }, + required: "* Required", + }, + }, + { + rhf: "Textarea", + name: "other_describe", + label: "Describe:", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + }, + ], + }, + { + label: "A specific amount", + value: "income_definition_specific", + slots: [ + { + rhf: "Radio", + name: "income_definition_specific", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "Statewide standard", + value: "statewide_standard", + form: [ + { + slots: [ + { + rhf: "FieldArray", + name: "income_definition_specific_statewide", + fields: [ + { + rhf: "Input", + label: "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: + /^\d*\.?\d+$/, + message: + "Must be a positive numerical value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be a positive number, maximum of two decimals, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + { + slots: [ + { + rhf: "Checkbox", + name: "is_incremental_amount_statewide_std", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount_statewide_std", + props: { + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + label: "Standard varies by region", + value: "region_standard", + form: [ + { + slots: [ + { + rhf: "FieldGroup", + name: "income_definition_specific_statewide_group_region", + props: { + appendText: "Add Region", + removeText: + "Remove Region", + }, + fields: [ + { + rhf: "Input", + name: "name_of_region", + label: "Region Name", + rules: { + required: + "* Required", + }, + }, + { + rhf: "Textarea", + name: "region_description", + label: "Description", + rules: { + required: + "* Required", + }, + }, + { + rhf: "FieldArray", + name: "income_definition_region_statewide_arr", + props: { + appendText: + "Add household size", + }, + fields: [ + { + rhf: "Input", + label: + "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: + /^\d*\.?\d+$/, + message: + "Must be a positive numerical value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: + "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + { + rhf: "Checkbox", + name: "is_incremental_amount", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount", + props: { + icon: "$", + }, + rules: { + pattern: + { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + { + label: + "Standard varies by living arrangement", + value: "living_standard", + + form: [ + { + slots: [ + { + rhf: "FieldGroup", + name: "income_definition_specific_statewide_group_liv_arrange", + props: { + appendText: + "Add Living Arrangement", + removeText: + "Remove living arrangement", + }, + fields: [ + { + rhf: "Input", + name: "name_of_living_arrangement", + label: + "Name of living arrangement", + rules: { + required: + "* Required", + }, + }, + { + rhf: "Textarea", + name: "living_arrangement_description", + label: "Description", + rules: { + required: + "* Required", + }, + }, + { + rhf: "FieldArray", + name: "income_definition_specific_statewide_arr", + props: { + appendText: + "Add household size", + }, + fields: [ + { + rhf: "Input", + label: + "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: + /^\d*\.?\d+$/, + message: + "Must be a positive numerical value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: + "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + { + rhf: "Checkbox", + name: "is_incremental_amount", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount", + props: { + icon: "$", + }, + rules: { + pattern: + { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + { + label: + "Standard varies in some other way", + value: "other_standard", + + form: [ + { + slots: [ + { + rhf: "FieldGroup", + name: "income_definition_specific_statewide_group_other", + props: { + appendText: + "Add some other way", + removeText: + "Remove some other way", + }, + fields: [ + { + rhf: "Input", + name: "name_of_group", + label: "Name", + rules: { + required: + "* Required", + }, + }, + { + rhf: "Textarea", + name: "group_description", + label: "Description", + rules: { + required: + "* Required", + }, + }, + { + rhf: "FieldArray", + name: "income_definition_specific_statewide_arr", + props: { + appendText: + "Add household size", + }, + fields: [ + { + rhf: "Input", + label: + "Household Size", + name: "household_size", + props: { + placeholder: + "enter size", + className: + "w-[300px]", + }, + rules: { + pattern: { + value: + /^\d*\.?\d+$/, + message: + "Must be a positive numerical value", + }, + required: + "* Required", + }, + }, + { + rhf: "Input", + name: "standard", + label: + "Standard ($)", + props: { + className: + "w-[200px]", + placeholder: + "enter amount", + icon: "$", + }, + rules: { + pattern: { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + { + rhf: "Checkbox", + name: "is_incremental_amount", + props: { + options: [ + { + label: + "There is an additional incremental amount.", + value: "yes", + form: [ + { + slots: [ + { + rhf: "Input", + label: + "Incremental amount ($)", + name: "dollar_incremental_amount", + props: { + icon: "$", + }, + rules: { + pattern: + { + value: + /^\d*(?:\.\d{1,2})?$/, + message: + "Must be all numbers, no commas. e.g. 1234.56", + }, + required: + "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + value: "health", + label: + "Disease, condition, diagnosis, or disorder (check all that apply)", + slots: [ + { + rhf: "Checkbox", + name: "health_conditions", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "Physical disability", + value: "physical_disability", + }, + { + label: "Brain Injury", + value: "brain_injury", + }, + { + label: "HIV/AIDS", + value: "hiv_aids", + }, + { + label: "Medically frail", + value: "med_frail", + }, + { + label: "Technology dependent", + value: "technology_dependent", + }, + { label: "Autism", value: "autism" }, + { + label: "Developmental disability", + value: "dev_disability", + }, + { + label: "Intellectual disability", + value: "int_disability", + }, + { + label: "Mental illness", + value: "mental_illness", + }, + { + label: "Substance use disorder", + value: "substance_use_dis", + }, + { label: "Diabetes", value: "diabetes" }, + { label: "Heart disease", value: "heart_dis" }, + { label: "Asthma", value: "asthma" }, + { label: "Obesity", value: "obesity" }, + { + label: + "Other disease, condition, diagnosis, or disorder", + value: "other", + slots: [ + { + rhf: "Textarea", + name: "other_description", + label: "Describe", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + }, + ], + }, + { + label: "Other targeting criteria", + value: "other_targeting_criteria", + slots: [ + { + rhf: "Textarea", + name: "other_targeting_criteria_description", + label: "Describe", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + title: "Geographic Area", + form: [ + { + description: + "Will the Alternative Benefit Plan population include individuals from the entire state/territory?", + slots: [ + { + rhf: "Select", + name: "is_geographic_area", + props: { + className: "w-[150px]", + options: [ + { label: "Yes", value: "yes" }, + { label: "No", value: "no" }, + ], + }, + rules: { + required: "* Required", + }, + }, + ], + }, + { + description: "Method of geographic variation", + dependency: { + conditions: [ + { + name: "is_geographic_area", + type: "expectedValue", + expectedValue: "no", + }, + ], + effect: { type: "show" }, + }, + slots: [ + { + rhf: "Radio", + name: "geographic_variation", + rules: { + required: "* Required", + }, + props: { + options: [ + { + label: "By county", + value: "by_county", + form: [ + { + description: "Specify counties", + slots: [ + { + name: "specify_counties", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + { + label: "By region", + value: "by_region", + form: [ + { + description: "Specify regions", + slots: [ + { + name: "specify_regions", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + { + label: "By city or town", + value: "by_city_town", + form: [ + { + description: "Specify cities or towns", + slots: [ + { + name: "specify_cities_towns", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + { + label: "Other geographic area", + value: "other", + form: [ + { + description: "Specify other geographic area", + slots: [ + { + name: "specify_other", + rhf: "Textarea", + rules: { + required: "* Required", + }, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + { + title: "Additional information", + form: [ + { + description: + "Any other information the state/territory wishes to provide about the population (optional)", + // "Other information related to selection of the Section 1937 coverage option and the base benchmark plan (optional)", + slots: [ + { + name: "additional_information", + rhf: "Textarea", + }, + ], + }, + ], + }, + // { + // title: "Testing Alt Layouts", + // form: [ + // { + // description: "A test of horizontal layouts with no slot styles", + // wrapperStyling: "flex flex-wrap gap-6", + // slots: [ + // { + // name: "example1_1", + // label: "Example 1.1", + // rhf: "Input", + // }, + // { + // name: "example1_2", + // label: "Example 1.2", + // rhf: "Input", + // }, + // { + // name: "example1_3", + // label: "Example 1.3", + // rhf: "Input", + // }, + // ], + // }, + // { + // description: "A test of horizontal layouts with slot styles", + // wrapperStyling: "flex flex-wrap gap-6", + // slots: [ + // { + // name: "example2_1", + // label: "Example 2.1", + // rhf: "Input", + // props: { + // className: "w-80", + // }, + // }, + // { + // name: "example2_2", + // label: "Example 2.2", + // rhf: "Input", + // props: { + // className: "w-30", + // }, + // }, + // { + // name: "example2_3", + // label: "Example 2.3", + // rhf: "Input", + // props: { + // className: "w-120", + // }, + // }, + // ], + // }, + // ], + // }, + ], +}; + +export const form = ABP1; diff --git a/src/services/api/package.json b/src/services/api/package.json index 94a6529cda..61b7c2770b 100644 --- a/src/services/api/package.json +++ b/src/services/api/package.json @@ -28,6 +28,7 @@ }, "version": "0.0.0", "scripts": { + "build": "tsc", "lint": "eslint '**/*.{ts,js}'" } } diff --git a/src/services/api/serverless.yml b/src/services/api/serverless.yml index 680b735c0c..aee04474bf 100644 --- a/src/services/api/serverless.yml +++ b/src/services/api/serverless.yml @@ -6,6 +6,8 @@ plugins: - "@stratiformdigital/serverless-iam-helper" - "@stratiformdigital/serverless-s3-security-helper" - serverless-scriptable-plugin + - serverless-plugin-scripts + provider: name: aws runtime: nodejs18.x @@ -64,23 +66,31 @@ custom: scriptable: hooks: package:compileEvents: ./handlers/repack.js + scripts: + hooks: + package:initialize: | + yarn build params: master: + formsProvisionedConcurrency: 2 searchProvisionedConcurrency: 4 itemProvisionedConcurrency: 2 getAttachmentUrlProvisionedConcurrency: 2 submitProvisionedConcurrency: 2 val: + formsProvisionedConcurrency: 2 searchProvisionedConcurrency: 4 itemProvisionedConcurrency: 2 getAttachmentUrlProvisionedConcurrency: 2 submitProvisionedConcurrency: 2 production: + formsProvisionedConcurrency: 5 searchProvisionedConcurrency: 10 itemProvisionedConcurrency: 5 getAttachmentUrlProvisionedConcurrency: 5 submitProvisionedConcurrency: 5 default: + formsProvisionedConcurrency: 0 searchProvisionedConcurrency: 0 itemProvisionedConcurrency: 0 getAttachmentUrlProvisionedConcurrency: 0 @@ -218,7 +228,31 @@ functions: - Ref: SecurityGroup subnetIds: >- ${self:custom.vpc.privateSubnets} - provisionedConcurrency: ${param:submitProvisionedConcurrency} # reuse submit's concurrency + provisionedConcurrency: ${param:submitProvisionedConcurrency} + forms: + handler: handlers/forms.handler + layers: + - !Ref FormsLambdaLayer + maximumRetryAttempts: 0 + environment: + region: ${self:provider.region} + osDomain: ${param:osDomain} + events: + - http: + path: /forms + method: get + cors: true + authorizer: aws_iam + vpc: + securityGroupIds: + - Ref: SecurityGroup + subnetIds: >- + ${self:custom.vpc.privateSubnets} + provisionedConcurrency: ${param:searchProvisionedConcurrency} +layers: + forms: + path: layers + description: Lambda Layer for forms function resources: Resources: ApiGateway400ErrorCount: diff --git a/src/services/api/tsconfig.json b/src/services/api/tsconfig.json index 3884eb2003..3e4f76053c 100644 --- a/src/services/api/tsconfig.json +++ b/src/services/api/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { "target": "ES2016", - "moduleResolution": "node" - } + "moduleResolution": "node", + "module": "commonjs", + "allowSyntheticDefaultImports": true + }, + "include": ["./layers/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/src/services/ui/index.html b/src/services/ui/index.html index 7455ba647e..c0cb95a847 100644 --- a/src/services/ui/index.html +++ b/src/services/ui/index.html @@ -9,6 +9,10 @@ href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> + CMS MAKO diff --git a/src/services/ui/package.json b/src/services/ui/package.json index 5de0f4a9b5..885a3ac4ac 100644 --- a/src/services/ui/package.json +++ b/src/services/ui/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.6", @@ -43,6 +44,7 @@ "@tanstack/react-query": "^4.29.1", "@tanstack/react-query-devtools": "^4.29.5", "@types/file-saver": "^2.0.5", + "ajv-errors": "^3.0.0", "aws-amplify": "^5.2.5", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", diff --git a/src/services/ui/src/api/index.ts b/src/services/ui/src/api/index.ts index 7778f91a6c..9db0383485 100644 --- a/src/services/ui/src/api/index.ts +++ b/src/services/ui/src/api/index.ts @@ -1,4 +1,5 @@ export * from "./useSearch"; +export * from "./useGetForm"; export * from "./useGetItem"; export * from "./getAttachmentUrl"; export * from "./useGetPackageActions"; diff --git a/src/services/ui/src/api/useGetForm.ts b/src/services/ui/src/api/useGetForm.ts new file mode 100644 index 0000000000..fb09f80b18 --- /dev/null +++ b/src/services/ui/src/api/useGetForm.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; +import { API } from "aws-amplify"; +import { ReactQueryApiError } from "shared-types"; +import { FormSchema } from "shared-types"; +import { reInsertRegex } from "shared-utils"; + +export const getForm = async ( + formId: string, + formVersion?: string +): Promise => { + const form = await API.get("os", "/forms", { + queryStringParameters: { formId, formVersion }, + }); + + return reInsertRegex(form); +}; + +export const useGetForm = (formId: string, formVersion?: string) => { + return useQuery([formId, formVersion], () => + getForm(formId, formVersion) + ); +}; diff --git a/src/services/ui/src/components/BreadCrumb/BreadCrumb.tsx b/src/services/ui/src/components/BreadCrumb/BreadCrumb.tsx index bbd3787738..ef3cb8bda4 100644 --- a/src/services/ui/src/components/BreadCrumb/BreadCrumb.tsx +++ b/src/services/ui/src/components/BreadCrumb/BreadCrumb.tsx @@ -61,7 +61,7 @@ export const BreadCrumb = ({ {showSeperator && {seperator}} {active && ( - + {children} )} diff --git a/src/services/ui/src/components/Cards/OptionCard.test.tsx b/src/services/ui/src/components/Cards/OptionCard.test.tsx index 78af99e2f4..62ae3f023a 100644 --- a/src/services/ui/src/components/Cards/OptionCard.test.tsx +++ b/src/services/ui/src/components/Cards/OptionCard.test.tsx @@ -49,11 +49,11 @@ describe("OptionCard Component System", () => { expect(innerWrapper.className.includes("bg-slate-100")).toBeTruthy(); expect(innerWrapper.className.includes("bg-white")).toBeFalsy(); }); - test("title is rendered as an h3 and styled", () => { + test("title is rendered as an h2 and styled", () => { renderOptionCard(false); - const header = screen.getByRole("heading", { level: 3 }); + const header = screen.getByRole("heading", { level: 2 }); expect(header).toHaveTextContent("Test Card Title"); - expect(header).toHaveClass("text-lg text-sky-600 font-bold my-2"); + expect(header).toHaveClass("text-lg text-sky-700 font-bold my-2"); }); test("description is rendered", () => { renderOptionCard(false); diff --git a/src/services/ui/src/components/Cards/OptionCard.tsx b/src/services/ui/src/components/Cards/OptionCard.tsx index 6c4963d1f1..61d9fc23ab 100644 --- a/src/services/ui/src/components/Cards/OptionCard.tsx +++ b/src/services/ui/src/components/Cards/OptionCard.tsx @@ -45,10 +45,10 @@ export const OptionCard = ({ } hover:bg-sky-100`} >
-

{title}

+

{title}

{description}

- + diff --git a/src/services/ui/src/components/Inputs/button.tsx b/src/services/ui/src/components/Inputs/button.tsx index 51677f4531..e69a796fff 100644 --- a/src/services/ui/src/components/Inputs/button.tsx +++ b/src/services/ui/src/components/Inputs/button.tsx @@ -14,7 +14,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: - "border-2 border-primary text-primary font-bold hover:text-accent-foreground", + "border border-primary text-primary font-bold hover:bg-primary/10", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", diff --git a/src/services/ui/src/components/Inputs/calendar.tsx b/src/services/ui/src/components/Inputs/calendar.tsx index 0a626733a8..080244a827 100644 --- a/src/services/ui/src/components/Inputs/calendar.tsx +++ b/src/services/ui/src/components/Inputs/calendar.tsx @@ -1,15 +1,9 @@ import * as React from "react"; +import { CalendarProps } from "shared-types"; import { DayPicker } from "react-day-picker"; - import { cn } from "@/lib/utils"; import { buttonVariants } from "./button"; -export type CalendarProps = React.ComponentProps & { - className?: string; - classNames?: any; - showOutsideDays?: boolean; -}; - function Calendar({ className, classNames, diff --git a/src/services/ui/src/components/Inputs/checkbox.tsx b/src/services/ui/src/components/Inputs/checkbox.tsx index 5e20915a6b..9fc81980c7 100644 --- a/src/services/ui/src/components/Inputs/checkbox.tsx +++ b/src/services/ui/src/components/Inputs/checkbox.tsx @@ -17,7 +17,7 @@ const Checkbox = React.forwardRef< ref={ref} id={props.label} className={cn( - "peer h-5 w-5 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", + "peer h-7 w-7 my-2 shrink-0 border-black border-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground", className )} {...props} @@ -25,7 +25,7 @@ const Checkbox = React.forwardRef< - +
diff --git a/src/services/ui/src/components/Inputs/date-picker.tsx b/src/services/ui/src/components/Inputs/date-picker.tsx index f85c207a99..0f4661cf39 100644 --- a/src/services/ui/src/components/Inputs/date-picker.tsx +++ b/src/services/ui/src/components/Inputs/date-picker.tsx @@ -3,17 +3,12 @@ import * as React from "react"; import { format } from "date-fns"; import { Calendar as CalendarIcon } from "lucide-react"; - +import { DatePickerProps } from "shared-types"; import { cn } from "@/lib/utils"; import { Button } from "@/components/Inputs/button"; import { Calendar } from "@/components/Inputs/calendar"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/Popover"; -type DatePickerProps = { - date: Date | undefined; - onChange: (date: Date | undefined) => void; -}; - export const DatePicker = ({ date, onChange }: DatePickerProps) => { const [isCalendarOpen, setIsCalendarOpen] = React.useState(false); diff --git a/src/services/ui/src/components/Inputs/form.tsx b/src/services/ui/src/components/Inputs/form.tsx index f3ae142c85..9fb1c6aea4 100644 --- a/src/services/ui/src/components/Inputs/form.tsx +++ b/src/services/ui/src/components/Inputs/form.tsx @@ -95,7 +95,7 @@ const FormLabel = React.forwardRef< return (
); } ); + Input.displayName = "Input"; export { Input }; diff --git a/src/services/ui/src/components/Inputs/label.tsx b/src/services/ui/src/components/Inputs/label.tsx index 7c9606448a..a851368006 100644 --- a/src/services/ui/src/components/Inputs/label.tsx +++ b/src/services/ui/src/components/Inputs/label.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + "text-base leading-normal peer-disabled:cursor-not-allowed peer-disabled:opacity-70" ); const Label = React.forwardRef< diff --git a/src/services/ui/src/components/Inputs/radio-group.tsx b/src/services/ui/src/components/Inputs/radio-group.tsx index 891de3f1a5..d456a6598d 100644 --- a/src/services/ui/src/components/Inputs/radio-group.tsx +++ b/src/services/ui/src/components/Inputs/radio-group.tsx @@ -1,5 +1,4 @@ import * as React from "react"; -import { CheckIcon } from "@radix-ui/react-icons"; import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; import { cn } from "@/lib/utils"; @@ -12,7 +11,7 @@ const RadioGroup = React.forwardRef< >(({ className, ...props }, ref) => { return ( @@ -25,18 +24,18 @@ const RadioGroupItem = React.forwardRef< React.ComponentPropsWithoutRef & { className?: string; } ->(({ className, children, ...props }, ref) => { +>(({ className, ...props }, ref) => { return ( - +
); diff --git a/src/services/ui/src/components/Inputs/select.tsx b/src/services/ui/src/components/Inputs/select.tsx index 39de1d001d..853cc0fa40 100644 --- a/src/services/ui/src/components/Inputs/select.tsx +++ b/src/services/ui/src/components/Inputs/select.tsx @@ -5,9 +5,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"; import { cn } from "@/lib/utils"; const Select = SelectPrimitive.Root; - const SelectGroup = SelectPrimitive.Group; - const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< @@ -19,7 +17,7 @@ const SelectTrigger = React.forwardRef< & { className?: string }, - React.ComponentPropsWithoutRef & { - className?: string; - } + SwitchProps >(({ className, ...props }, ref) => ( { + (props.onChange as any)?.(value); + props.onCheckedChange?.(value); + }} ref={ref} > {} +import { TextareaProps } from "shared-types"; const Textarea = React.forwardRef( ({ className, ...props }, ref) => { + const strLn = (typeof props?.value === "string" && props.value.length) || 0; return ( -