Skip to content

Commit

Permalink
MAT-7792: Implement Function section
Browse files Browse the repository at this point in the history
  • Loading branch information
mcmcphillips committed Dec 3, 2024
1 parent f498c01 commit 1e9f320
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/CqlBuilderPanel/CqlBuilderPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export default function CqlBuilderPanel({

{activeTab === "functions" && (
<FunctionsSection
cqlBuilderLookupsTypes={cqlBuilderLookupsTypes}
canEdit={canEdit}
handleApplyFunction={handleApplyFunction}
loading={loading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();

Expand Down Expand Up @@ -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]);
Expand Down
5 changes: 4 additions & 1 deletion src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("function");

Expand All @@ -30,6 +32,7 @@ export default function FunctionsSection({
<FunctionBuilder
canEdit={canEdit}
handleApplyFunction={handleApplyFunction}
cqlBuilderLookupsTypes={cqlBuilderLookupsTypes}
/>
)}
{activeTab === "saved-functions" && <Functions />}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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(
<FunctionBuilder
canEdit={true}
handleApplyFunction={jest.fn()}
cqlBuilderLookupsTypes={cqlBuilderLookup}
/>
);
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();
});
});
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -21,6 +24,7 @@ export interface Funct {
}

export interface FunctionProps {
cqlBuilderLookupsTypes: CqlBuilderLookup;
canEdit: boolean;
handleApplyFunction: Function;
handleFunctionEdit?: Function;
Expand All @@ -34,6 +38,7 @@ export default function FunctionBuilder({
handleFunctionEdit,
onClose,
funct,
cqlBuilderLookupsTypes,
}: FunctionProps) {
const [argumentsEditorOpen, setArgumentsEditorOpen] =
useState<boolean>(false);
Expand All @@ -42,17 +47,71 @@ export default function FunctionBuilder({
const textAreaRef = useRef(null);
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(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 (
<div>
Expand All @@ -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);
}
}}
/>
</div>
<Box sx={{ marginTop: "22px" }}>
Expand Down Expand Up @@ -117,11 +182,18 @@ export default function FunctionBuilder({
children={<></>}
/>
<div style={{ marginTop: "36px" }} />
<ExpandingSection
title="Expression Editor"
showHeaderContent={expressionEditorOpen}
children={<></>}
/>
<FormikProvider value={formik}>
<ExpressionEditor
canEdit={canEdit}
expressionEditorOpen={expressionEditorOpen}
cqlBuilderLookupsTypes={cqlBuilderLookupsTypes}
textAreaRef={textAreaRef}
expressionEditorValue={expressionEditorValue}
setExpressionEditorValue={setExpressionEditorValue}
setCursorPosition={setCursorPosition}
setAutoInsert={setAutoInsert}
/>
</FormikProvider>
<div className="form-actions">
<Button
id="clear-function-btn"
Expand Down

0 comments on commit 1e9f320

Please sign in to comment.