From 1e9f320126e24404370bc9f6d5de9075f8006bc1 Mon Sep 17 00:00:00 2001 From: mcmcphillips Date: Tue, 3 Dec 2024 11:30:28 -0800 Subject: [PATCH] MAT-7792: Implement Function section --- src/CqlBuilderPanel/CqlBuilderPanel.tsx | 1 + .../expressionSection/ExpressionEditor.tsx | 26 ++++-- .../functionsSection/FunctionsSection.tsx | 5 +- .../functionBuilder/FunctionBuilder.test.tsx | 63 ++++++++++++++ .../functionBuilder/FunctionBuilder.tsx | 86 +++++++++++++++++-- 5 files changed, 164 insertions(+), 17 deletions(-) diff --git a/src/CqlBuilderPanel/CqlBuilderPanel.tsx b/src/CqlBuilderPanel/CqlBuilderPanel.tsx index 99ce7634..6d78cb86 100644 --- a/src/CqlBuilderPanel/CqlBuilderPanel.tsx +++ b/src/CqlBuilderPanel/CqlBuilderPanel.tsx @@ -254,6 +254,7 @@ export default function CqlBuilderPanel({ {activeTab === "functions" && ( { if (textAreaRef.current) { const lineCount = textAreaRef.current.editor.session.getLength(); - const newHeight = Math.max(lineCount * 20, 100) + "px"; + // newNeight should not exceed 180 + /* + Text entry control (with line numbers, but only starting with 1 line, expandable as more text is added to it, max height is 11 lines and then it scrolls) + https://jira.cms.gov/browse/MAT-7792 + */ + const maxHeight = 180; + const proposedNewHeight = Math.max(lineCount * 20, 100); + const newHeight = Math.min(maxHeight, proposedNewHeight) + "px"; setEditorHeight(newHeight); } }, [expressionEditorValue]); diff --git a/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx index d1f78d41..70e10650 100644 --- a/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx +++ b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx @@ -3,17 +3,19 @@ import "./Functions.scss"; import FunctionSectionNavTabs from "./FunctionSectionNavTabs"; import Functions from "./functions/Functions"; import FunctionBuilder from "./functionBuilder/FunctionBuilder"; - +import { CqlBuilderLookup } from "../../model/CqlBuilderLookup"; interface FunctionProps { canEdit: boolean; handleApplyFunction: Function; loading: boolean; + cqlBuilderLookupsTypes: CqlBuilderLookup; } export default function FunctionsSection({ canEdit, handleApplyFunction, loading, + cqlBuilderLookupsTypes, }: FunctionProps) { const [activeTab, setActiveTab] = useState("function"); @@ -30,6 +32,7 @@ export default function FunctionsSection({ )} {activeTab === "saved-functions" && } diff --git a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx index 6dc614ab..17e273d7 100644 --- a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx +++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx @@ -1,8 +1,10 @@ import * as React from "react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { describe, it } from "@jest/globals"; +import { within } from "@testing-library/dom"; import "@testing-library/jest-dom"; import FunctionBuilder from "./FunctionBuilder"; +import { cqlBuilderLookup } from "../../__mocks__/MockCqlBuilderLookupsTypes"; describe("CQL Function Builder Tests", () => { it("Should display name and comment fields", async () => { @@ -90,4 +92,65 @@ describe("CQL Function Builder Tests", () => { fireEvent.click(clearButton); expect(functionNameInput.value).toBe(""); }); + + it("Should open expression editor content on entry.", async () => { + render( + + ); + const functionNameInput = (await screen.findByTestId( + "function-name-text-input" + )) as HTMLInputElement; + expect(functionNameInput).toBeInTheDocument(); + expect(functionNameInput.value).toBe(""); + fireEvent.change(functionNameInput, { + target: { value: "IP" }, + }); + expect(functionNameInput.value).toBe("IP"); + + const definitionCommentTextBox = await screen.findByRole("textbox", { + name: "Comment", + }); + expect(definitionCommentTextBox).toBeInTheDocument(); + + expect( + screen.getByTestId("terminology-section-Expression Editor-sub-heading") + ).toBeInTheDocument(); + const typeInput = screen.getByTestId( + "type-selector-input" + ) as HTMLInputElement; + expect(typeInput).toBeInTheDocument(); + expect(typeInput.value).toBe(""); + + fireEvent.change(typeInput, { + target: { value: "Timing" }, + }); + expect(typeInput.value).toBe("Timing"); + + const nameAutoComplete = screen.getByTestId("name-selector"); + expect(nameAutoComplete).toBeInTheDocument(); + const nameComboBox = within(nameAutoComplete).getByRole("combobox"); + //name dropdown is populated with values based on type + await waitFor(() => expect(nameComboBox).toBeEnabled()); + + const nameDropDown = await screen.findByTestId("name-selector"); + fireEvent.keyDown(nameDropDown, { key: "ArrowDown" }); + + const nameOptions = await screen.findAllByRole("option"); + expect(nameOptions).toHaveLength(70); + + const insertBtn = screen.getByTestId("expression-insert-btn"); + expect(insertBtn).toBeInTheDocument(); + expect(insertBtn).toBeDisabled(); + + fireEvent.click(nameOptions[0]); + expect(insertBtn).toBeEnabled(); + + const applyBtn = screen.getByTestId("function-apply-btn"); + expect(applyBtn).toBeInTheDocument(); + expect(applyBtn).toBeEnabled(); + }); }); diff --git a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx index f56399f4..19693b23 100644 --- a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx +++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx @@ -1,7 +1,7 @@ import React, { useState, useRef } from "react"; import "twin.macro"; import "styled-components/macro"; -import { useFormik } from "formik"; +import { useFormik, FormikProvider } from "formik"; import { Button, TextArea, @@ -13,6 +13,9 @@ import ExpandingSection from "../../../common/ExpandingSection"; import { Checkbox, FormControlLabel } from "@mui/material"; import { Box } from "@mui/system"; import ConfirmationDialog from "../../common/ConfirmationDialog"; +import ExpressionEditor from "../../definitionsSection/expressionSection/ExpressionEditor"; +import { formatExpressionName } from "../../definitionsSection/definitionBuilder/DefinitionBuilder"; +import { CqlBuilderLookup } from "../../../model/CqlBuilderLookup"; export interface Funct { functionName?: string; @@ -21,6 +24,7 @@ export interface Funct { } export interface FunctionProps { + cqlBuilderLookupsTypes: CqlBuilderLookup; canEdit: boolean; handleApplyFunction: Function; handleFunctionEdit?: Function; @@ -34,6 +38,7 @@ export default function FunctionBuilder({ handleFunctionEdit, onClose, funct, + cqlBuilderLookupsTypes, }: FunctionProps) { const [argumentsEditorOpen, setArgumentsEditorOpen] = useState(false); @@ -42,17 +47,71 @@ export default function FunctionBuilder({ const textAreaRef = useRef(null); const [confirmationDialog, setConfirmationDialog] = useState(false); + const [expressionEditorValue, setExpressionEditorValue] = useState(""); + const [cursorPosition, setCursorPosition] = useState(null); + const [autoInsert, setAutoInsert] = useState(false); const formik = useFormik({ initialValues: { functionName: funct?.functionName || "", comment: funct?.comment || "", fluentFunction: funct?.fluentFunction || true, + type: "", + name: "", }, validationSchema: FunctionSectionSchemaValidator, enableReinitialize: true, - onSubmit: (values) => {}, + onSubmit: (values) => { + handleExpressionEditorInsert(values); + }, }); const { resetForm } = formik; + const handleExpressionEditorInsert = (values) => { + const formattedExpression = formatExpressionName(values); + let editorExpressionValue = expressionEditorValue; + let newCursorPosition = cursorPosition; + + if (cursorPosition && !autoInsert) { + // Insert at cursor position + const { row, column } = cursorPosition; + const lines = expressionEditorValue.split("\n"); + const currentLine = lines[row]; + lines[row] = + currentLine.slice(0, column) + + formattedExpression + + currentLine.slice(column); + editorExpressionValue = lines.join("\n"); + newCursorPosition = { + row, + column: column + formattedExpression.length, + } as unknown; + } else { + // Append to a new line + const lines = editorExpressionValue.split("\n"); + const newLineIndex = lines.length; + editorExpressionValue += + (editorExpressionValue ? "\n" : "") + formattedExpression; + newCursorPosition = { + row: newLineIndex, + column: formattedExpression.length, + }; + } + + setExpressionEditorValue(editorExpressionValue); + formik.setFieldValue("type", ""); + formik.setFieldValue("name", ""); + + textAreaRef.current.editor.setValue(editorExpressionValue, 1); + // set the cursor to the end of the inserted text + textAreaRef.current.editor.moveCursorTo( + newCursorPosition.row, + newCursorPosition.column + ); + textAreaRef.current.editor.clearSelection(); + // set autoInsert to true for next insertion + setAutoInsert(true); + // clear cursor position to allow the next item to auto-insert at the end + setCursorPosition(null); + }; return (
@@ -74,6 +133,12 @@ export default function FunctionBuilder({ error={Boolean(formik.errors.functionName)} helperText={formik.errors.functionName} {...formik.getFieldProps("functionName")} + onChange={(e) => { + formik.handleChange(e); + if (e.target.value && !expressionEditorOpen) { + setExpressionEditorOpen(true); + } + }} />
@@ -117,11 +182,18 @@ export default function FunctionBuilder({ children={<>} />
- } - /> + + +