diff --git a/src/AceEditor/madie-ace-editor.tsx b/src/AceEditor/madie-ace-editor.tsx
index 9ffa8a8e..8cc86403 100644
--- a/src/AceEditor/madie-ace-editor.tsx
+++ b/src/AceEditor/madie-ace-editor.tsx
@@ -25,6 +25,7 @@ import {
} from "../api/useTerminologyServiceApi";
import { Definition } from "../CqlBuilderPanel/definitionsSection/definitionBuilder/DefinitionBuilder";
import { SelectedLibrary } from "../CqlBuilderPanel/Includes/CqlLibraryDetailsDialog";
+import { Funct } from "../CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder";
export interface EditorPropsType {
value: string;
@@ -45,6 +46,7 @@ export interface EditorPropsType {
editedLib: SelectedLibrary
) => void;
handleDeleteLibrary?: (lib: SelectedLibrary) => void;
+ handleApplyFunction?: (funct: Funct) => void;
parseDebounceTime?: number;
inboundAnnotations?: Ace.Annotation[];
inboundErrorMarkers?: Ace.MarkerLike[];
diff --git a/src/CqlBuilderPanel/CqlBuilderPanel.test.tsx b/src/CqlBuilderPanel/CqlBuilderPanel.test.tsx
index 1f4c4079..aabff0a8 100644
--- a/src/CqlBuilderPanel/CqlBuilderPanel.test.tsx
+++ b/src/CqlBuilderPanel/CqlBuilderPanel.test.tsx
@@ -98,6 +98,7 @@ jest.mock("@madie/madie-util", () => ({
useFeatureFlags: jest.fn().mockReturnValue({
CQLBuilderIncludes: true,
CQLBuilderDefinitions: true,
+ CQLBuilderFunctions: true,
QDMValueSetSearch: true,
qdmCodeSearch: true,
}),
@@ -121,6 +122,9 @@ const props = {
handleDeleteLibrary: jest.fn(),
handleEditLibrary: jest.fn(),
handleDefinitionEdit: jest.fn(),
+ handleApplyFunction: jest.fn(),
+ handleParameterEdit: jest.fn(),
+ handleApplyParameter: jest.fn(),
resetCql: jest.fn(),
getCqlDefinitionReturnTypes: jest.fn(),
makeExpanded: jest.fn(),
@@ -867,4 +871,40 @@ describe("CqlBuilderPanel", () => {
expect(aceEditor.value).toContain("Some more Text");
});
});
+
+ it("Functions tab exists and it's enabled", async () => {
+ useFeatureFlags.mockImplementationOnce(() => ({
+ CQLBuilderIncludes: true,
+ QDMValueSetSearch: true,
+ CQLBuilderDefinitions: true,
+ qdmCodeSearch: true,
+ CQLBuilderParameters: true,
+ CQLBuilderFunctions: true,
+ }));
+ const newProps = { ...props, canEdit: false };
+ mockedAxios.put.mockResolvedValue({
+ data: mockCqlBuilderLookUpData,
+ });
+ render();
+ const parameterTab = await screen.queryByText("Functions");
+ expect(parameterTab).toBeInTheDocument();
+ expect(parameterTab).toBeEnabled();
+ });
+
+ it("Functions tab does not exist", async () => {
+ useFeatureFlags.mockImplementationOnce(() => ({
+ CQLBuilderIncludes: true,
+ QDMValueSetSearch: true,
+ CQLBuilderDefinitions: true,
+ qdmCodeSearch: true,
+ CQLBuilderParameters: true,
+ CQLBuilderFunctions: false,
+ }));
+ mockedAxios.put.mockResolvedValue({
+ data: mockCqlBuilderLookUpData,
+ });
+ render();
+ const parameterTab = await screen.queryByText("Functions");
+ expect(parameterTab).not.toBeInTheDocument();
+ });
});
diff --git a/src/CqlBuilderPanel/CqlBuilderPanel.tsx b/src/CqlBuilderPanel/CqlBuilderPanel.tsx
index ac98e4d1..99ce7634 100644
--- a/src/CqlBuilderPanel/CqlBuilderPanel.tsx
+++ b/src/CqlBuilderPanel/CqlBuilderPanel.tsx
@@ -3,6 +3,7 @@ import CqlBuilderSectionPanelNavTabs from "./CqlBuilderSectionPanelNavTabs";
import ValueSetsSection from "./ValueSets/ValueSets";
import CodesSection from "./codesSection/CodesSection";
import DefinitionsSection from "./definitionsSection/DefinitionsSection";
+import FunctionsSection from "./functionsSection/FunctionsSection";
import { useFeatureFlags } from "@madie/madie-util";
import IncludesTabSection from "./Includes/Includes";
import Parameters from "./Parameters/Parameters";
@@ -34,13 +35,18 @@ export default function CqlBuilderPanel({
handleApplyDefinition,
handleDefinitionEdit,
handleDefinitionDelete,
+ handleApplyFunction,
resetCql,
getCqlDefinitionReturnTypes,
makeExpanded,
}) {
const featureFlags = useFeatureFlags();
- const { CQLBuilderDefinitions, CQLBuilderIncludes, CQLBuilderParameters } =
- featureFlags;
+ const {
+ CQLBuilderDefinitions,
+ CQLBuilderIncludes,
+ CQLBuilderParameters,
+ CQLBuilderFunctions,
+ } = featureFlags;
// we have multiple flags and need to select a starting value based off of what's available and canEdit.
const getStartingPage = (() => {
// if cqlBuilderIncludes -> includes
@@ -143,6 +149,7 @@ export default function CqlBuilderPanel({
isQDM={measureModel?.includes("QDM")}
CQLBuilderParameters={CQLBuilderParameters}
CQLBuilderIncludes={CQLBuilderIncludes}
+ CQLBuilderFunctions={CQLBuilderFunctions}
/>
)}
+
+ {activeTab === "functions" && (
+
+ )}
diff --git a/src/CqlBuilderPanel/CqlBuilderSectionPanelNavTabs.tsx b/src/CqlBuilderPanel/CqlBuilderSectionPanelNavTabs.tsx
index 73e71c83..7d290a01 100644
--- a/src/CqlBuilderPanel/CqlBuilderSectionPanelNavTabs.tsx
+++ b/src/CqlBuilderPanel/CqlBuilderSectionPanelNavTabs.tsx
@@ -6,6 +6,7 @@ export interface NavTabProps {
CQLBuilderIncludes: boolean;
CQLBuilderParameters: boolean;
CQLBuilderDefinitions: boolean;
+ CQLBuilderFunctions: boolean;
isQDM: boolean;
}
@@ -16,6 +17,7 @@ export default function CqlBuilderSectionPanelNavTabs(props: NavTabProps) {
CQLBuilderDefinitions,
CQLBuilderIncludes,
CQLBuilderParameters,
+ CQLBuilderFunctions,
isQDM,
} = props;
@@ -78,6 +80,16 @@ export default function CqlBuilderSectionPanelNavTabs(props: NavTabProps) {
data-testid="definitions-tab"
/>
)}
+ {CQLBuilderFunctions && (
+
+ )}
);
}
diff --git a/src/CqlBuilderPanel/common/ConfirmationDialog.tsx b/src/CqlBuilderPanel/common/ConfirmationDialog.tsx
new file mode 100644
index 00000000..e459c177
--- /dev/null
+++ b/src/CqlBuilderPanel/common/ConfirmationDialog.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { MadieDialog } from "@madie/madie-design-system/dist/react";
+import ErrorIcon from "@mui/icons-material/Error";
+
+const ConfirmationDialog = ({ open, onClose, onSubmit }) => {
+ return (
+
+
+
+ You are about to clear this function, including all arguments.
+
+
+
+ This action cannot be undone!
+
+
+
+ );
+};
+
+export default ConfirmationDialog;
diff --git a/src/CqlBuilderPanel/functionsSection/FunctionSectionNavTabs.tsx b/src/CqlBuilderPanel/functionsSection/FunctionSectionNavTabs.tsx
new file mode 100644
index 00000000..b68c3582
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/FunctionSectionNavTabs.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import { Tabs, Tab } from "@madie/madie-design-system/dist/react";
+
+export interface NavTabProps {
+ activeTab: string;
+ setActiveTab: (value: string) => void;
+ functionCount: number;
+ loading: boolean;
+}
+
+export default function FunctionSectionNavTabs(props: NavTabProps) {
+ const { activeTab, setActiveTab, functionCount, loading } = props;
+
+ return (
+
+ {
+ setActiveTab(v);
+ }}
+ type="B"
+ visibleScrollbar
+ variant="scrollable"
+ scrollButtons={false}
+ >
+
+
+
+
+ );
+}
diff --git a/src/CqlBuilderPanel/functionsSection/Functions.scss b/src/CqlBuilderPanel/functionsSection/Functions.scss
new file mode 100644
index 00000000..1959f358
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/Functions.scss
@@ -0,0 +1,86 @@
+#function-form {
+ font-family: Rubik, sans-serif;
+ grid-column: span 5 / span 14;
+ display: flex;
+ flex-direction: column;
+ padding-top: 32px;
+ textArea {
+ box-sizing: border-box;
+ border-radius: 3px;
+ border: solid 1px #8c8c8c;
+ background-color: #fff;
+ opacity: 1;
+ height: 100%;
+ font-size: 14px;
+ font-weight: 400;
+ resize: both;
+ color: #333;
+ &::placeholder {
+ color: #717171;
+ opacity: 1;
+ }
+ }
+ > .content {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ position: relative;
+ .subTitle {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: baseline;
+ padding-bottom: 8px;
+ margin-bottom: 33px;
+ border-bottom: solid 1px;
+ position: relative;
+ h2 {
+ font-size: 24px;
+ border: none;
+ margin: 0;
+ padding: 0;
+ }
+ }
+ h3 {
+ font-weight: 400;
+ font-size: 32px;
+ line-height: 48px;
+ display: flex;
+ flex-direction: row;
+ padding-bottom: 8px;
+ border-bottom: solid 1px #8c8c8c;
+ }
+ label:not(.Mui-disabled) {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 17px;
+ color: #333333;
+ text-transform: none;
+ margin-bottom: 7px;
+ }
+ }
+ .form-actions {
+ display: flex;
+ flex-direction: row;
+ min-width: 100%;
+ justify-content: end;
+ > .cancel-button {
+ margin-top: 1rem;
+ margin-right: 32px;
+ }
+ > button[type="submit"] {
+ margin-top: 0;
+ }
+ }
+ .left-box {
+ width: 40%;
+ float: left;
+ }
+ .right-box {
+ width: 55%;
+ font-family: Rubik, sans-serif;
+ margin-left: 30px;
+ height: 80%;
+ float: right;
+ }
+}
diff --git a/src/CqlBuilderPanel/functionsSection/FunctionsSection.test.tsx b/src/CqlBuilderPanel/functionsSection/FunctionsSection.test.tsx
new file mode 100644
index 00000000..914fa42e
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/FunctionsSection.test.tsx
@@ -0,0 +1,44 @@
+import * as React from "react";
+import { render, screen, waitFor, act } from "@testing-library/react";
+import FunctionsSection from "./FunctionsSection";
+import userEvent from "@testing-library/user-event";
+
+const props = {
+ canEdit: true,
+ loading: false,
+ handleApplyFunction: jest.fn(),
+};
+
+describe("FunctionsSection", () => {
+ it("Should display function section", async () => {
+ render();
+ const funct = await screen.findByTestId("function-tab");
+ const savedfunctions = await screen.findByText("Saved Functions (0)");
+ expect(funct).toBeInTheDocument();
+ expect(savedfunctions).toBeInTheDocument();
+ await waitFor(() => {
+ expect(funct).toHaveAttribute("aria-selected", "true");
+ });
+ await waitFor(() => {
+ expect(savedfunctions).toHaveAttribute("aria-selected", "false");
+ });
+ });
+
+ it("Should display saved function section", async () => {
+ render();
+ const funct = await screen.findByTestId("function-tab");
+ const savedfunctions = await screen.findByText("Saved Functions (0)");
+ 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");
+ });
+ });
+});
diff --git a/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx
new file mode 100644
index 00000000..d1f78d41
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx
@@ -0,0 +1,39 @@
+import React, { useState } from "react";
+import "./Functions.scss";
+import FunctionSectionNavTabs from "./FunctionSectionNavTabs";
+import Functions from "./functions/Functions";
+import FunctionBuilder from "./functionBuilder/FunctionBuilder";
+
+interface FunctionProps {
+ canEdit: boolean;
+ handleApplyFunction: Function;
+ loading: boolean;
+}
+
+export default function FunctionsSection({
+ canEdit,
+ handleApplyFunction,
+ loading,
+}: FunctionProps) {
+ const [activeTab, setActiveTab] = useState("function");
+
+ return (
+ <>
+
+
+ {activeTab === "function" && (
+
+ )}
+ {activeTab === "saved-functions" && }
+
+ >
+ );
+}
diff --git a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx
new file mode 100644
index 00000000..6dc614ab
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.test.tsx
@@ -0,0 +1,93 @@
+import * as React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { describe, it } from "@jest/globals";
+import "@testing-library/jest-dom";
+import FunctionBuilder from "./FunctionBuilder";
+
+describe("CQL Function Builder Tests", () => {
+ it("Should display name and comment fields", async () => {
+ render();
+ const functionNameTextBox = await screen.findByRole("textbox", {
+ name: "Function Name",
+ });
+ expect(functionNameTextBox).toBeInTheDocument();
+
+ const functionCommentTextBox = await screen.findByRole("textbox", {
+ name: "Comment",
+ });
+ expect(functionCommentTextBox).toBeInTheDocument();
+
+ expect(
+ screen.getByTestId("terminology-section-Expression Editor-sub-heading")
+ ).toBeInTheDocument();
+ expect(screen.queryByTestId("type-selector-input")).not.toBeInTheDocument();
+ });
+
+ it("Should disable Apply button with canEdit being false", async () => {
+ render();
+
+ const applyBtn = screen.getByTestId("function-apply-btn");
+ expect(applyBtn).toBeInTheDocument();
+ expect(applyBtn).toBeDisabled();
+
+ const clearBtn = screen.getByTestId("clear-function-btn");
+ expect(clearBtn).toBeInTheDocument();
+ expect(clearBtn).toBeDisabled();
+ });
+
+ it("Should generate pop up when clear button is clicked, Cancel clear", 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: "Test" },
+ });
+ expect(functionNameInput.value).toBe("Test");
+
+ const functionCommentTextBox = await screen.findByRole("textbox", {
+ name: "Comment",
+ });
+ expect(functionCommentTextBox).toBeInTheDocument();
+
+ const clearBtn = screen.getByTestId("clear-function-btn");
+ expect(clearBtn).toBeEnabled();
+ fireEvent.click(clearBtn);
+
+ const confirmationDialog = screen.getByText("Are you sure?");
+ const cancelButton = screen.getByTestId("confirmation-cancel-button");
+ expect(cancelButton).toBeEnabled();
+ fireEvent.click(cancelButton);
+ expect(functionNameInput.value).toBe("Test");
+ });
+
+ it("Should generate pop up when clear button is clicked, confirm clear", 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: "Test" },
+ });
+ expect(functionNameInput.value).toBe("Test");
+
+ const functionCommentTextBox = await screen.findByRole("textbox", {
+ name: "Comment",
+ });
+ expect(functionCommentTextBox).toBeInTheDocument();
+
+ const clearBtn = screen.getByTestId("clear-function-btn");
+ expect(clearBtn).toBeEnabled();
+ fireEvent.click(clearBtn);
+
+ const confirmationDialog = screen.getByText("Are you sure?");
+ const clearButton = screen.getByTestId("confirmation-clear-button");
+ expect(clearButton).toBeEnabled();
+ fireEvent.click(clearButton);
+ expect(functionNameInput.value).toBe("");
+ });
+});
diff --git a/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx
new file mode 100644
index 00000000..aaa509d8
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/functionBuilder/FunctionBuilder.tsx
@@ -0,0 +1,152 @@
+import React, { useState, useRef } from "react";
+import "twin.macro";
+import "styled-components/macro";
+import { useFormik } from "formik";
+import {
+ Button,
+ TextArea,
+ TextField,
+} from "@madie/madie-design-system/dist/react";
+import "../Functions.scss";
+import ExpandingSection from "../../../common/ExpandingSection";
+import { Checkbox, FormControlLabel } from "@mui/material";
+import { Box } from "@mui/system";
+import ConfirmationDialog from "../../common/ConfirmationDialog";
+
+export interface Funct {
+ functionName?: string;
+ fluentFunction?: boolean;
+ comment?: string;
+}
+
+export interface FunctionProps {
+ canEdit: boolean;
+ handleApplyFunction: Function;
+ handleFunctionEdit?: Function;
+ funct?: Funct;
+ onClose?: Function;
+}
+
+export default function FunctionBuilder({
+ canEdit,
+ handleApplyFunction,
+ handleFunctionEdit,
+ onClose,
+ funct,
+}: FunctionProps) {
+ const [argumentsEditorOpen, setArgumentsEditorOpen] =
+ useState(false);
+ const [expressionEditorOpen, setExpressionEditorOpen] =
+ useState(false);
+ const textAreaRef = useRef(null);
+ const [confirmationDialog, setConfirmationDialog] = useState(false);
+
+ const formik = useFormik({
+ initialValues: {
+ functionName: funct?.functionName || "",
+ comment: funct?.comment || "",
+ fluentFunction: funct?.fluentFunction || false,
+ },
+ enableReinitialize: true,
+ onSubmit: (values) => {},
+ });
+ const { resetForm } = formik;
+
+ return (
+
+ );
+}
diff --git a/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx b/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx
new file mode 100644
index 00000000..ef4c3587
--- /dev/null
+++ b/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+import _ from "lodash";
+import tw from "twin.macro";
+import "styled-components/macro";
+
+const TH = tw.th`p-3 text-left text-sm font-bold capitalize`;
+const TD = tw.td`p-3 text-left text-sm break-all`;
+
+const Functions = () => {
+ return <>{/* MAT-7794 */}>;
+};
+
+export default Functions;
diff --git a/src/cqlEditorWithTerminology/CqlEditorWithTerminology.tsx b/src/cqlEditorWithTerminology/CqlEditorWithTerminology.tsx
index c7309263..3d4404ec 100644
--- a/src/cqlEditorWithTerminology/CqlEditorWithTerminology.tsx
+++ b/src/cqlEditorWithTerminology/CqlEditorWithTerminology.tsx
@@ -23,6 +23,7 @@ const CqlEditorWithTerminology = ({
handleEditLibrary,
handleDeleteLibrary,
handleDefinitionEdit,
+ handleApplyFunction,
height,
parseDebounceTime = 1500,
inboundAnnotations,
@@ -120,6 +121,7 @@ const CqlEditorWithTerminology = ({
handleApplyLibrary={handleApplyLibrary}
handleEditLibrary={handleEditLibrary}
handleDeleteLibrary={handleDeleteLibrary}
+ handleApplyFunction={handleApplyFunction}
resetCql={resetCql}
getCqlDefinitionReturnTypes={getCqlDefinitionReturnTypes}
/>
diff --git a/src/madie-madie-util.d.ts b/src/madie-madie-util.d.ts
index 110e10e6..eb58ef5c 100644
--- a/src/madie-madie-util.d.ts
+++ b/src/madie-madie-util.d.ts
@@ -5,6 +5,7 @@ declare module "@madie/madie-util" {
CQLBuilderIncludes: boolean;
CQLBuilderDefinitions: boolean;
CQLBuilderParameters: boolean;
+ CQLBuilderFunctions: boolean;
}
export const useOktaTokens: (storageKey?: string) => {