diff --git a/src/CqlBuilderPanel/CqlBuilderPanel.tsx b/src/CqlBuilderPanel/CqlBuilderPanel.tsx index 227dfca0..45d7391c 100644 --- a/src/CqlBuilderPanel/CqlBuilderPanel.tsx +++ b/src/CqlBuilderPanel/CqlBuilderPanel.tsx @@ -4,6 +4,7 @@ import ValueSetsSection from "./ValueSets/ValueSets"; import CodesSection from "./codesSection/CodesSection"; import DefinitionsSection from "./definitionsSection/DefinitionsSection"; import FunctionsSection from "./functionsSection/FunctionsSection"; +// import FunctionsSection from "" import { useFeatureFlags } from "@madie/madie-util"; import IncludesTabSection from "./Includes/Includes"; import Parameters from "./Parameters/Parameters"; @@ -51,7 +52,7 @@ export default function CqlBuilderPanel({ return "includes"; })(); - const [activeTab, setActiveTab] = useState(getStartingPage); + const [activeTab, setActiveTab] = useState("functions"); const [cqlBuilderLookupsTypes, setCqlBuilderLookupsTypes] = useState(); const [errors, setErrors] = useState(null); @@ -247,6 +248,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..e2e81369 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,35 @@ 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(); + }); + }); }); diff --git a/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx b/src/CqlBuilderPanel/functionsSection/FunctionsSection.tsx index 995a428b..cfb09de7 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,10 @@ export interface FunctionProps { cql: string; isCQLUnchanged: boolean; functions?: FunctionLookup[]; + resetCql?: Function; + cqlBuilderLookupTypes?: any; } + const getArgumentNames = (logic: string) => { const args = logic.substring(logic.indexOf("(") + 1, logic.indexOf(")")); return args.split(","); @@ -27,10 +27,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 +71,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..5e7d2d1f 100644 --- a/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx +++ b/src/CqlBuilderPanel/functionsSection/functions/Functions.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; import _ from "lodash"; import tw from "twin.macro"; import "styled-components/macro"; -import { FunctionLookup } from "../../../model/CqlBuilderLookup"; +import { FunctionLookup, Lookup } from "../../../model/CqlBuilderLookup"; import { FunctionProps } from "../FunctionsSection"; import { ColumnDef, @@ -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,41 @@ const Functions = ({ hidePrevButton={!canGoPrev} /> + { + resetCql(); + if (discardDialog?.operation === "edit") { + setDiscardDialog({ + open: false, + operation: "edit", + }); + setEditFunctionDialogOpen(true); + } else if (discardDialog?.operation === "delete") { + setDiscardDialog({ + open: false, + operation: "delete", + }); + setDeleteDialogOpen(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;