From 5ea4195a51beb8fce2880a5ff6c7fb5dd4785e08 Mon Sep 17 00:00:00 2001 From: mcmcphillips Date: Tue, 3 Dec 2024 11:30:28 -0800 Subject: [PATCH 1/9] MAT-7792: Implement Function section --- src/CqlBuilderPanel/CqlBuilderPanel.tsx | 2 +- .../expressionSection/ExpressionEditor.tsx | 26 ++++-- .../functionsSection/FunctionsSection.tsx | 1 + .../functionBuilder/FunctionBuilder.test.tsx | 61 ++++++++++++++ .../functionBuilder/FunctionBuilder.tsx | 84 +++++++++++++++++-- 5 files changed, 158 insertions(+), 16 deletions(-) diff --git a/src/CqlBuilderPanel/CqlBuilderPanel.tsx b/src/CqlBuilderPanel/CqlBuilderPanel.tsx index 3511c188..9c82fc26 100644 --- a/src/CqlBuilderPanel/CqlBuilderPanel.tsx +++ b/src/CqlBuilderPanel/CqlBuilderPanel.tsx @@ -256,12 +256,12 @@ export default function CqlBuilderPanel({ {activeTab === "functions" && ( )} diff --git a/src/CqlBuilderPanel/definitionsSection/expressionSection/ExpressionEditor.tsx b/src/CqlBuilderPanel/definitionsSection/expressionSection/ExpressionEditor.tsx index 54d19a34..c841a795 100644 --- a/src/CqlBuilderPanel/definitionsSection/expressionSection/ExpressionEditor.tsx +++ b/src/CqlBuilderPanel/definitionsSection/expressionSection/ExpressionEditor.tsx @@ -33,6 +33,15 @@ interface ExpressionsProps { setAutoInsert: Function; } +export const availableTypes = [ + "Parameters", + "Definitions", + "Functions", + "Fluent Functions", + "Timing", + "Pre-Defined Functions", +]; + export default function ExpressionEditor(props: ExpressionsProps) { const { canEdit, @@ -45,14 +54,6 @@ export default function ExpressionEditor(props: ExpressionsProps) { setAutoInsert, } = props; const [namesOptions, setNamesOptions] = useState([]); - const availableTypes = [ - "Parameters", - "Definitions", - "Functions", - "Fluent Functions", - "Timing", - "Pre-Defined Functions", - ]; const [editorHeight, setEditorHeight] = useState("100px"); const formik: any = useFormikContext(); @@ -95,7 +96,14 @@ export default function ExpressionEditor(props: ExpressionsProps) { useEffect(() => { 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 602df0f3..995a428b 100644 --- a/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx +++ b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx @@ -84,6 +84,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 01cdcb5a..9a37237b 100644 --- a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx +++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx @@ -10,6 +10,7 @@ import { describe, it } from "@jest/globals"; import "@testing-library/jest-dom"; import FunctionBuilder from "./FunctionBuilder"; import userEvent from "@testing-library/user-event"; +import { cqlBuilderLookup } from "../../__mocks__/MockCqlBuilderLookupsTypes"; describe("CQL Function Builder Tests", () => { it("Should display name and comment fields", async () => { @@ -295,4 +296,64 @@ describe("CQL Function Builder Tests", () => { "No Results were found" ); }); + 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 0409ab08..91d8e101 100644 --- a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx +++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx @@ -14,6 +14,9 @@ import { Checkbox, FormControlLabel } from "@mui/material"; import { Box } from "@mui/system"; import ConfirmationDialog from "../../common/ConfirmationDialog"; import ArgumentSection from "../argumentSection/ArgumentSection"; +import ExpressionEditor from "../../definitionsSection/expressionSection/ExpressionEditor"; +import { formatExpressionName } from "../../definitionsSection/definitionBuilder/DefinitionBuilder"; +import { CqlBuilderLookup } from "../../../model/CqlBuilderLookup"; export interface Funct { functionName?: string; @@ -23,6 +26,7 @@ export interface Funct { } export interface FunctionProps { + cqlBuilderLookupsTypes: CqlBuilderLookup; canEdit: boolean; handleApplyFunction: Function; handleFunctionEdit?: Function; @@ -36,6 +40,7 @@ export default function FunctionBuilder({ handleFunctionEdit, onClose, funct, + cqlBuilderLookupsTypes, }: FunctionProps) { const [argumentsEditorOpen, setArgumentsEditorOpen] = useState(false); @@ -44,18 +49,72 @@ 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, functionsArguments: funct?.functionsArguments || [], + 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); + }; const addArgumentToFunctionsArguments = (fn) => { const newArgs = [...formik.values.functionsArguments, fn]; @@ -91,6 +150,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); + } + }} /> @@ -143,11 +208,18 @@ export default function FunctionBuilder({
- } - /> + + +
.MuiTabs-root { background-color: #ededed; margin-left: 16px; + > .MuiTabs-scroller { + overflow: auto !important; + } } } > .panel-content { From b3b60aa9a1f0c0520bc7c8c02c8c0db9ab314d50 Mon Sep 17 00:00:00 2001 From: mcmcphillips Date: Thu, 5 Dec 2024 14:25:40 -0800 Subject: [PATCH 5/9] MAT-7792: partial --- .../functionBuilder/FunctionBuilder.tsx | 21 +++++-------------- .../functionBuilder/functiontoapply.ts | 10 +++++++++ 2 files changed, 15 insertions(+), 16 deletions(-) create mode 100644 src/CqlBuilderPanel/functionsSection/functionBuilder/functiontoapply.ts diff --git a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx index 7600004f..af39a8b8 100644 --- a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx +++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx @@ -217,25 +217,14 @@ export default function FunctionBuilder({