diff --git a/src/CqlBuilderPanel/CqlBuilderPanel.tsx b/src/CqlBuilderPanel/CqlBuilderPanel.tsx
index 227dfca0..fda312e0 100644
--- a/src/CqlBuilderPanel/CqlBuilderPanel.tsx
+++ b/src/CqlBuilderPanel/CqlBuilderPanel.tsx
@@ -247,6 +247,7 @@ export default function CqlBuilderPanel({
loading={loading}
cql={measureStoreCql}
isCQLUnchanged={isCQLUnchanged}
+ resetCql={resetCql}
/>
)}
diff --git a/src/CqlBuilderPanel/functionsSection/EditFunctionDialog.test.tsx b/src/CqlBuilderPanel/functionsSection/EditFunctionDialog.test.tsx
new file mode 100644
index 00000000..1f206713
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/EditFunctionDialog.test.tsx
@@ -0,0 +1,28 @@
+import * as React from "react";
+import { parseArgumentsFromLogicString } from "./EditFunctionDialog";
+
+describe("parseArgumentsFromLogicString", () => {
+ test("Can parse out empty arguments", () => {
+ const logicString = `define function "Empty Arguments" (): true`;
+ const result = parseArgumentsFromLogicString(logicString);
+ expect(result).toEqual([]);
+ });
+ test("Can parse out multiple arguments", () => {
+ const logicString2 = `define function "Function name here" (arg1 "Integer", arg2 "Integer", arg3 "Date"):\n true`;
+ const result = parseArgumentsFromLogicString(logicString2);
+
+ expect(result).toEqual([
+ { argumentName: "arg1", dataType: "Integer" },
+ { argumentName: "arg2", dataType: "Integer" },
+ { argumentName: "arg3", dataType: "Date" },
+ ]);
+ });
+ test("Can parse out arguments with commas embedded in dataType", () => {
+ const logicString3 = `define fluent function "Numerator Observation"(Encounter "Encounter, Performed" ):
+ duration in hours of Encounter.relevantPeriod`;
+ const result = parseArgumentsFromLogicString(logicString3);
+ expect(result).toEqual([
+ { argumentName: "Encounter", dataType: "Encounter, Performed" },
+ ]);
+ });
+});
diff --git a/src/CqlBuilderPanel/functionsSection/EditFunctionDialog.tsx b/src/CqlBuilderPanel/functionsSection/EditFunctionDialog.tsx
new file mode 100644
index 00000000..60185f53
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/EditFunctionDialog.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { MadieDialog } from "@madie/madie-design-system/dist/react";
+import FunctionBuilder from "./functionBuilder/FunctionBuilder";
+
+interface PropTypes {
+ open: boolean;
+ onClose: () => void;
+ cqlBuilderLookupsTypes?: any;
+ funct?: any;
+ setEditFunctionDialogOpen: Function;
+ handleApplyFunction: Function;
+ handleFunctionEdit: Function;
+}
+
+export const parseArgumentsFromLogicString = (logicString) => {
+ // `s` flag for multiline content
+ const argumentListRegex = /\(([^)]*)\)/s;
+ const match = logicString.match(argumentListRegex);
+
+ // Nobody in parenthesis
+ if (!match) {
+ return [];
+ }
+
+ const argumentsString = match[1].trim();
+ // no args
+ if (!argumentsString) {
+ return [];
+ }
+
+ // Regex to match argument and data type pairs
+ const argumentRegex = /([\w]+)\s+"([^"]+)"/g;
+ const results = [];
+
+ let argumentMatch;
+ while ((argumentMatch = argumentRegex.exec(argumentsString)) !== null) {
+ const [, argumentName, dataType] = argumentMatch;
+ results.push({ argumentName, dataType });
+ }
+ return results;
+};
+
+const EditFunctionDialog = ({
+ funct,
+ handleFunctionEdit,
+ onClose,
+ open,
+ setEditFunctionDialogOpen,
+ cqlBuilderLookupsTypes,
+ handleApplyFunction,
+}: PropTypes) => {
+ // a property is passed called argument names that does not seem to work for anything with commas.
+ // the following is regex to grab arguments and dataTypes using a matcher and add them to the function to edit.
+
+ const updatedFunction = {
+ ...funct,
+ fluentFunction: funct?.isFluent === true ? true : false,
+ functionsArguments: parseArgumentsFromLogicString(
+ funct?.logic ? funct?.logic : ""
+ ),
+ expressionEditorValue: funct?.logic,
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default EditFunctionDialog;
diff --git a/src/CqlBuilderPanel/functionsSection/FunctionsSection.test.tsx b/src/CqlBuilderPanel/functionsSection/FunctionsSection.test.tsx
index ed7173df..50c4cf5d 100644
--- a/src/CqlBuilderPanel/functionsSection/FunctionsSection.test.tsx
+++ b/src/CqlBuilderPanel/functionsSection/FunctionsSection.test.tsx
@@ -5,6 +5,8 @@ import userEvent from "@testing-library/user-event";
import { mockMeasureStoreCql } from "../__mocks__/MockMeasureStoreCql";
import { cqlBuilderLookup } from "../__mocks__/MockCqlBuilderLookupsTypes";
+const resetCql = jest.fn();
+
const props = {
canEdit: true,
loading: false,
@@ -12,6 +14,7 @@ const props = {
cql: mockMeasureStoreCql,
isCQLUnchanged: false,
cqlBuilderLookupsTypes: cqlBuilderLookup,
+ resetCql,
};
describe("FunctionsSection", () => {
@@ -62,4 +65,116 @@ describe("FunctionsSection", () => {
expect(funct).toBeInTheDocument();
expect(savedfunctions).toBeInTheDocument();
});
+
+ it("Should open a confirmation dialog on click", async () => {
+ render();
+ const funct = await screen.findByTestId("function-tab");
+ const savedfunctions = await screen.findByText("Saved Functions (2)");
+ expect(funct).toBeInTheDocument();
+ expect(savedfunctions).toBeInTheDocument();
+ await waitFor(() => {
+ expect(funct).toHaveAttribute("aria-selected", "true");
+ });
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "false");
+ });
+ userEvent.click(savedfunctions);
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "true");
+ });
+ const editButon0 = screen.getByTestId("edit-button-0");
+ userEvent.click(editButon0);
+ expect(screen.getByTestId("discard-dialog")).toBeInTheDocument();
+ expect(screen.getByText("Discard Changes?")).toBeInTheDocument();
+ const cancelBtn = screen.getByTestId("discard-dialog-cancel-button");
+ const discardBtn = screen.getByTestId("discard-dialog-continue-button");
+ expect(cancelBtn).toBeInTheDocument();
+ expect(discardBtn).toBeInTheDocument();
+
+ userEvent.click(discardBtn);
+ await waitFor(() => {
+ expect(screen.getByText("Edit")).toBeInTheDocument();
+ });
+ });
+
+ it("Should open edit dialog on click", async () => {
+ render();
+ const funct = await screen.findByTestId("function-tab");
+ const savedfunctions = await screen.findByText("Saved Functions (2)");
+ expect(funct).toBeInTheDocument();
+ expect(savedfunctions).toBeInTheDocument();
+ await waitFor(() => {
+ expect(funct).toHaveAttribute("aria-selected", "true");
+ });
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "false");
+ });
+ userEvent.click(savedfunctions);
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "true");
+ });
+ const editButon0 = screen.getByTestId("edit-button-0");
+ userEvent.click(editButon0);
+ await waitFor(() => {
+ expect(screen.getByText("Edit")).toBeInTheDocument();
+ });
+ });
+
+ it("Should close discard dialog on click", async () => {
+ render();
+ const funct = await screen.findByTestId("function-tab");
+ const savedfunctions = await screen.findByText("Saved Functions (2)");
+ expect(funct).toBeInTheDocument();
+ expect(savedfunctions).toBeInTheDocument();
+ await waitFor(() => {
+ expect(funct).toHaveAttribute("aria-selected", "true");
+ });
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "false");
+ });
+ userEvent.click(savedfunctions);
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "true");
+ });
+ const editButon0 = screen.getByTestId("edit-button-0");
+ userEvent.click(editButon0);
+ expect(screen.getByTestId("discard-dialog")).toBeInTheDocument();
+ expect(screen.getByText("Discard Changes?")).toBeInTheDocument();
+ const cancelBtn = screen.getByTestId("discard-dialog-cancel-button");
+ expect(cancelBtn).toBeInTheDocument();
+ userEvent.click(cancelBtn);
+
+ await waitFor(() => {
+ expect(screen.queryByTestId("discard-dialog")).not.toBeInTheDocument();
+ });
+ });
+
+ it("Should close edit dialog on click", async () => {
+ render();
+ const funct = await screen.findByTestId("function-tab");
+ const savedfunctions = await screen.findByText("Saved Functions (2)");
+ expect(funct).toBeInTheDocument();
+ expect(savedfunctions).toBeInTheDocument();
+ await waitFor(() => {
+ expect(funct).toHaveAttribute("aria-selected", "true");
+ });
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "false");
+ });
+ userEvent.click(savedfunctions);
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "true");
+ });
+ const editButon0 = screen.getByTestId("edit-button-0");
+ userEvent.click(editButon0);
+ await waitFor(() => {
+ expect(screen.getByText("Edit")).toBeInTheDocument();
+ });
+ const closeButton = screen.getByRole("button", { name: "Close" });
+ expect(closeButton).toBeInTheDocument();
+ userEvent.click(closeButton);
+ await waitFor(() => {
+ expect(screen.queryByTestId("discard-dialog")).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx
index 995a428b..2aef2648 100644
--- a/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx
+++ b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx
@@ -3,10 +3,7 @@ import "./Functions.scss";
import FunctionSectionNavTabs from "./FunctionSectionNavTabs";
import Functions from "./functions/Functions";
import FunctionBuilder from "./functionBuilder/FunctionBuilder";
-import {
- CqlBuilderLookup,
- FunctionLookup,
-} from "../..//model/CqlBuilderLookup";
+import { CqlBuilderLookup, FunctionLookup } from "../../model/CqlBuilderLookup";
import * as _ from "lodash";
import { CqlAntlr } from "@madie/cql-antlr-parser/dist/src";
@@ -18,7 +15,9 @@ export interface FunctionProps {
cql: string;
isCQLUnchanged: boolean;
functions?: FunctionLookup[];
+ resetCql?: Function;
}
+
const getArgumentNames = (logic: string) => {
const args = logic.substring(logic.indexOf("(") + 1, logic.indexOf(")"));
return args.split(",");
@@ -27,10 +26,11 @@ const getArgumentNames = (logic: string) => {
export default function FunctionsSection({
canEdit,
handleApplyFunction,
- loading,
cql,
isCQLUnchanged,
cqlBuilderLookupsTypes,
+ resetCql,
+ loading,
}: FunctionProps) {
const [activeTab, setActiveTab] = useState("function");
@@ -70,7 +70,6 @@ export default function FunctionsSection({
}) || []
);
functionLookups = _.sortBy(functionLookups, (o) => o.name?.toLowerCase());
-
return (
<>
)}
diff --git a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx
index b9e2a2eb..7c7ef62a 100644
--- a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx
+++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx
@@ -251,8 +251,20 @@ describe("CQL Function Builder Tests", () => {
const functionArgumentTable = screen.getByTestId("function-argument-tbl");
expect(functionArgumentTable).toBeInTheDocument();
+ await waitFor(() => {
+ expect(
+ screen.getByTestId("function-builder-success")
+ ).toBeInTheDocument();
+ });
const tableRow = functionArgumentTable.querySelector("tbody").children[0];
expect(tableRow.children[1].textContent).toEqual("Test");
+ const closeButton = screen.getByTestId(
+ "function-builder-toast-close-button"
+ );
+ userEvent.click(closeButton);
+ await waitFor(() => {
+ expect(closeButton).not.toBeInTheDocument();
+ });
});
it("Should delete argument from the table", async () => {
diff --git a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx
index 7c4e285e..0649337e 100644
--- a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx
+++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx
@@ -6,6 +6,7 @@ import {
Button,
TextArea,
TextField,
+ Toast,
} from "@madie/madie-design-system/dist/react";
import "../Functions.scss";
import { FunctionSectionSchemaValidator } from "../../../validations/FunctionSectionSchemaValidator";
@@ -17,12 +18,14 @@ import ArgumentSection from "../argumentSection/ArgumentSection";
import ExpressionEditor from "../../definitionsSection/expressionSection/ExpressionEditor";
import { getNewExpressionsAndLines } from "../../common/utils";
import { CqlBuilderLookup } from "../../../model/CqlBuilderLookup";
+import UseToast from "../../../common/UseToast";
export interface Funct {
- functionName?: string;
+ name?: string;
fluentFunction?: boolean;
functionsArguments: any;
comment?: string;
+ expressionEditorValue?: string;
}
export interface FunctionProps {
@@ -33,6 +36,7 @@ export interface FunctionProps {
funct?: Funct;
onClose?: Function;
operation?: string;
+ setEditFunctionDialogOpen?: Function;
}
export default function FunctionBuilder({
@@ -52,13 +56,14 @@ export default function FunctionBuilder({
const [confirmationDialog, setConfirmationDialog] = useState(false);
const [cursorPosition, setCursorPosition] = useState(null);
const [autoInsert, setAutoInsert] = useState(false);
+
const formik = useFormik({
initialValues: {
- functionName: funct?.functionName || "",
+ functionName: funct?.name || "",
comment: funct?.comment || "",
fluentFunction: funct?.fluentFunction || true,
functionsArguments: funct?.functionsArguments || [],
- expressionEditorValue: "",
+ expressionEditorValue: funct?.expressionEditorValue || "",
type: "",
name: "",
},
@@ -76,7 +81,16 @@ export default function FunctionBuilder({
});
// going to pass dirty down to know when we need to reset sub form
const { resetForm, dirty } = formik;
-
+ // toast utilities
+ const {
+ toastOpen,
+ setToastOpen,
+ toastMessage,
+ setToastMessage,
+ toastType,
+ setToastType,
+ onToastClose,
+ } = UseToast();
// update formik, and expressionEditor, cursor, lines
const updateExpressionAndLines = (
newEditorExpressionValue,
@@ -99,6 +113,11 @@ export default function FunctionBuilder({
const addArgumentToFunctionsArguments = (fn) => {
const newArgs = [...formik.values.functionsArguments, fn];
formik.setFieldValue("functionsArguments", newArgs);
+ setToastMessage(
+ `Argument ${fn.argumentName} has been successfully added to the function.`
+ );
+ setToastType("success");
+ setToastOpen(true);
};
const deleteArgumentFromFunctionArguments = (fn) => {
@@ -223,17 +242,21 @@ export default function FunctionBuilder({
@@ -247,6 +270,22 @@ export default function FunctionBuilder({
}}
/>
+
);
}
diff --git a/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx b/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx
index fa1fc66f..e98f4c39 100644
--- a/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx
+++ b/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx
@@ -15,7 +15,11 @@ import ToolTippedIcon from "../../../toolTippedIcon/ToolTippedIcon";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import BorderColorOutlinedIcon from "@mui/icons-material/BorderColorOutlined";
import Skeleton from "@mui/material/Skeleton";
-import { Pagination } from "@madie/madie-design-system/dist/react";
+import {
+ Pagination,
+ MadieDiscardDialog,
+} from "@madie/madie-design-system/dist/react";
+import EditFunctionDialog from "../EditFunctionDialog";
import Tooltip from "@mui/material/Tooltip";
const TH = tw.th`p-3 text-left text-sm font-bold capitalize`;
@@ -39,18 +43,20 @@ const Functions = ({
loading,
functions,
isCQLUnchanged,
+ resetCql,
+ cqlBuilderLookupsTypes,
+ handleApplyFunction,
}: FunctionProps) => {
+ // pagination utilities
const [totalPages, setTotalPages] = useState(0);
const [totalItems, setTotalItems] = useState(0);
const [visibleItems, setVisibleItems] = useState(0);
const [visibleFunctions, setVisibleFunctions] = useState(
[]
);
-
const [offset, setOffset] = useState(0);
const [currentLimit, setCurrentLimit] = useState(5);
const [currentPage, setCurrentPage] = useState(1);
-
const canGoPrev = currentPage > 1;
const canGoNext = (() => {
return currentPage < totalPages;
@@ -62,11 +68,20 @@ const Functions = ({
setCurrentLimit(e.target.value);
setCurrentPage(1);
};
-
useEffect(() => {
managePagination();
}, [functions, currentPage, currentLimit]);
+ // edit dialog utilities
+ const [discardDialog, setDiscardDialog] = useState({
+ open: false,
+ operation: null,
+ });
+ const [editFunctionDialogOpen, setEditFunctionDialogOpen] =
+ useState();
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [selectedFunction, setSelectedFunction] = useState();
+ const handleFunctionEdit = () => {};
// table data
const data = visibleFunctions;
@@ -145,7 +160,14 @@ const Functions = ({
"data-testid": `edit-button-${row.cell.row.id}`,
"aria-label": `edit-button-${row.cell.row.id}`,
size: "small",
- onClick: (e) => {},
+ onClick: (e) => {
+ setSelectedFunction(table.getRow(row.cell.row.id).original);
+ if (!isCQLUnchanged) {
+ setDiscardDialog({ open: true, operation: "edit" });
+ } else {
+ setEditFunctionDialogOpen(true);
+ }
+ },
}}
>
@@ -262,6 +284,35 @@ const Functions = ({
hidePrevButton={!canGoPrev}
/>
+ {
+ resetCql();
+ if (discardDialog?.operation === "edit") {
+ setDiscardDialog({
+ open: false,
+ operation: "edit",
+ });
+ setEditFunctionDialogOpen(true);
+ }
+ }}
+ onClose={() => {
+ setDiscardDialog({
+ open: false,
+ operation: null,
+ });
+ }}
+ />
+
+ setEditFunctionDialogOpen(false)}
+ cqlBuilderLookupsTypes={cqlBuilderLookupsTypes}
+ handleApplyFunction={handleApplyFunction}
+ handleFunctionEdit={handleFunctionEdit}
+ />
>
);
};
diff --git a/src/common/UseToast.tsx b/src/common/UseToast.tsx
new file mode 100644
index 00000000..f9915c2d
--- /dev/null
+++ b/src/common/UseToast.tsx
@@ -0,0 +1,24 @@
+import { useState } from "react";
+
+function UseToast() {
+ const [toastOpen, setToastOpen] = useState(false);
+ const [toastMessage, setToastMessage] = useState("");
+ const [toastType, setToastType] = useState("danger");
+
+ const onToastClose = () => {
+ setToastOpen(false);
+ setToastMessage("");
+ setToastType("danger");
+ };
+
+ return {
+ toastOpen,
+ setToastOpen,
+ toastMessage,
+ setToastMessage,
+ toastType,
+ setToastType,
+ onToastClose,
+ };
+}
+export default UseToast;