diff --git a/builder-api/src/main/java/org/acme/controller/EligibilityCheckResource.java b/builder-api/src/main/java/org/acme/controller/EligibilityCheckResource.java index 13d51a14..1e247453 100644 --- a/builder-api/src/main/java/org/acme/controller/EligibilityCheckResource.java +++ b/builder-api/src/main/java/org/acme/controller/EligibilityCheckResource.java @@ -10,10 +10,12 @@ import org.acme.auth.AuthUtils; import org.acme.constants.CheckStatus; import org.acme.model.domain.EligibilityCheck; -import org.acme.model.dto.SaveDmnRequest; +import org.acme.model.dto.CheckDmnRequest; import org.acme.persistence.EligibilityCheckRepository; import org.acme.persistence.StorageService; +import org.acme.service.DmnService; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -27,6 +29,9 @@ public class EligibilityCheckResource { @Inject StorageService storageService; + @Inject + DmnService dmnService; + @GET @Path("/checks") public Response getPublicChecks(@Context SecurityIdentity identity) { @@ -105,7 +110,7 @@ public Response updatePublicCheck(@Context SecurityIdentity identity, @POST @Consumes(MediaType.APPLICATION_JSON) @Path("/save-check-dmn") - public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnRequest saveDmnRequest){ + public Response updateCheckDmn(@Context SecurityIdentity identity, CheckDmnRequest saveDmnRequest){ String checkId = saveDmnRequest.id; String dmnModel = saveDmnRequest.dmnModel; if (checkId == null || checkId.isBlank()){ @@ -121,17 +126,10 @@ public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnReques } EligibilityCheck check = checkOpt.get(); - - //AUTHORIZATION -// if (!check.getOwnerId().equals(userId)){ -// return Response.status(Response.Status.UNAUTHORIZED).build(); -// } - - if (dmnModel == null){ - return Response.status(Response.Status.BAD_REQUEST) - .entity("Error: Missing required data: DMN Model") - .build(); + if (!check.getOwnerId().equals(userId)){ + return Response.status(Response.Status.UNAUTHORIZED).build(); } + try { String filePath = storageService.getCheckDmnModelPath(checkId); storageService.writeStringToStorage(filePath, dmnModel, "application/xml"); @@ -147,6 +145,51 @@ public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnReques } } + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("/validate-check-dmn") + public Response validateCheckDmn(@Context SecurityIdentity identity, CheckDmnRequest validateDmnRequest){ + String checkId = validateDmnRequest.id; + String dmnModel = validateDmnRequest.dmnModel; + if (checkId == null || checkId.isBlank()){ + return Response.status(Response.Status.BAD_REQUEST) + .entity("Error: Missing required data: checkId") + .build(); + } + + String userId = AuthUtils.getUserId(identity); + Optional checkOpt = eligibilityCheckRepository.getWorkingCustomCheck(userId, checkId); + if (checkOpt.isEmpty()){ + return Response.status(Response.Status.NOT_FOUND).build(); + } + + EligibilityCheck check = checkOpt.get(); + if (!check.getOwnerId().equals(userId)){ + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + if (dmnModel == null || dmnModel.isBlank()){ + return Response.ok(Map.of("errors", List.of("DMN Definition cannot be empty"))).build(); + } + + try { + HashMap dmnDependenciesMap = new HashMap(); + List validationErrors = dmnService.validateDmnXml(dmnModel, dmnDependenciesMap, check.getName(), check.getName()); + if (!validationErrors.isEmpty()) { + validationErrors = validationErrors.stream() + .map(error -> error.replaceAll("\\(.*?\\)", "")) + .collect(java.util.stream.Collectors.toList()); + + return Response.ok(Map.of("errors", validationErrors)).build(); + } + + return Response.ok(Map.of("errors", List.of())).build(); + } catch (Exception e){ + Log.info(("Failed to save DMN model for check " + checkId)); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + // By default, returns the most recent versions of all published checks owned by the calling user // If the query parameter 'working' is set to true, // then all the working check objects owned by the user are returned diff --git a/builder-api/src/main/java/org/acme/model/dto/SaveDmnRequest.java b/builder-api/src/main/java/org/acme/model/dto/CheckDmnRequest.java similarity index 72% rename from builder-api/src/main/java/org/acme/model/dto/SaveDmnRequest.java rename to builder-api/src/main/java/org/acme/model/dto/CheckDmnRequest.java index a17b1f4b..ab0962ad 100644 --- a/builder-api/src/main/java/org/acme/model/dto/SaveDmnRequest.java +++ b/builder-api/src/main/java/org/acme/model/dto/CheckDmnRequest.java @@ -1,6 +1,6 @@ package org.acme.model.dto; -public class SaveDmnRequest { +public class CheckDmnRequest { public String id; public String dmnModel; } diff --git a/builder-api/src/main/java/org/acme/service/DmnService.java b/builder-api/src/main/java/org/acme/service/DmnService.java index a47e354c..95fec9e4 100644 --- a/builder-api/src/main/java/org/acme/service/DmnService.java +++ b/builder-api/src/main/java/org/acme/service/DmnService.java @@ -1,9 +1,16 @@ package org.acme.service; import org.acme.enums.OptionalBoolean; +import java.util.List; import java.util.Map; public interface DmnService { + public List validateDmnXml( + String dmnXml, + Map dependenciesMap, + String modelId, + String requiredBooleanDecisionName + ) throws Exception; public OptionalBoolean evaluateDmn( String dmnFilePath, String dmnModelName, diff --git a/builder-api/src/main/java/org/acme/service/KieDmnService.java b/builder-api/src/main/java/org/acme/service/KieDmnService.java index e82cf44e..8e0c3822 100644 --- a/builder-api/src/main/java/org/acme/service/KieDmnService.java +++ b/builder-api/src/main/java/org/acme/service/KieDmnService.java @@ -11,11 +11,23 @@ import org.kie.api.runtime.KieContainer; import org.kie.api.runtime.KieSession; import org.kie.dmn.api.core.*; +import org.kie.dmn.api.core.ast.DecisionNode; + import java.io.*; import java.util.*; import org.drools.compiler.kie.builder.impl.InternalKieModule; +class DmnCompilationResult { + public byte[] dmnBytes; + public List errors; + + public DmnCompilationResult(byte[] dmnBytes, List errors) { + this.dmnBytes = dmnBytes; + this.errors = errors; + } +} + @ApplicationScoped public class KieDmnService implements DmnService { @Inject @@ -31,7 +43,46 @@ private KieSession initializeKieSession(byte[] moduleBytes) throws IOException { return kieContainer.newKieSession(); } - private byte[] compileDmnModel(String dmnXml, Map dependenciesMap, String modelId) throws IOException { + // Validates that the DMN XML can compile and contains the required decision. + // Returns a list of error messages if any issues are found. + public List validateDmnXml ( + String dmnXml, Map dependenciesMap, String modelId, String requiredBooleanDecisionName + ) throws Exception { + DmnCompilationResult compilationResult = compileDmnModel(dmnXml, dependenciesMap, modelId); + if (!compilationResult.errors.isEmpty()) { + return compilationResult.errors; + } + + KieSession kieSession = initializeKieSession(compilationResult.dmnBytes); + DMNRuntime dmnRuntime = kieSession.getKieRuntime(DMNRuntime.class); + + List dmnModels = dmnRuntime.getModels(); + if (dmnModels.size() != 1) { + return List.of("Expected exactly one DMN model, found: " + dmnModels.size()); + } + + DMNModel dmnModel = dmnModels.get(0); + DecisionNode requiredBooleanDecision = dmnModel.getDecisions().stream() + .filter(d -> d.getName().equals(requiredBooleanDecisionName)) + .findFirst() + .orElse(null); + if (requiredBooleanDecision == null) { + List decisionNames = dmnModel.getDecisions().stream() + .map(DecisionNode::getName) + .toList(); + return List.of( + "Required Decision '" + requiredBooleanDecisionName + "' not found in DMN definition. " + + "Decisions found: " + decisionNames + ); + } + + if (requiredBooleanDecision.getResultType().getName() != "boolean") { + return List.of("The Result DataType of Decision '" + requiredBooleanDecisionName + "' must be of type 'boolean'."); + } + return new ArrayList(); + } + + private DmnCompilationResult compileDmnModel(String dmnXml, Map dependenciesMap, String modelId) { Log.info("Compiling and saving DMN model: " + modelId); KieServices kieServices = KieServices.Factory.get(); @@ -69,18 +120,17 @@ private byte[] compileDmnModel(String dmnXml, Map dependenciesMa Results results = kieBuilder.getResults(); if (results.hasMessages(Message.Level.ERROR)) { - Log.error("DMN Compilation errors for model " + modelId + ":"); - for (Message message : results.getMessages(Message.Level.ERROR)) { - Log.error(message.getText()); - } - throw new IllegalStateException("DMN Model compilation failed for model: " + modelId); + return new DmnCompilationResult( + null, + results.getMessages(Message.Level.ERROR).stream().map(Message::getText).toList() + ); } InternalKieModule kieModule = (InternalKieModule) kieBuilder.getKieModule(); byte[] kieModuleBytes = kieModule.getBytes(); Log.info("Serialized kieModule for model " + modelId); - return kieModuleBytes; + return new DmnCompilationResult(kieModuleBytes, new ArrayList()); } public OptionalBoolean evaluateDmn( @@ -95,12 +145,19 @@ public OptionalBoolean evaluateDmn( if (dmnXmlOpt.isEmpty()) { throw new RuntimeException("DMN file not found: " + dmnFilePath); } - String dmnXml = dmnXmlOpt.get(); + HashMap dmnDependenciesMap = new HashMap(); - byte[] serializedModel = compileDmnModel(dmnXml, dmnDependenciesMap, dmnModelName); + DmnCompilationResult compilationResult = compileDmnModel(dmnXml, dmnDependenciesMap, dmnModelName); + if (!compilationResult.errors.isEmpty()) { + Log.error("DMN Compilation errors for model " + dmnModelName + ":"); + for (String error : compilationResult.errors) { + Log.error(error); + } + throw new IllegalStateException("DMN Model compilation failed for model: " + dmnModelName); + } - KieSession kieSession = initializeKieSession(serializedModel); + KieSession kieSession = initializeKieSession(compilationResult.dmnBytes); DMNRuntime dmnRuntime = kieSession.getKieRuntime(DMNRuntime.class); List dmnModels = dmnRuntime.getModels(); diff --git a/builder-frontend/src/api/check.ts b/builder-frontend/src/api/check.ts index efa22177..34cd9835 100644 --- a/builder-frontend/src/api/check.ts +++ b/builder-frontend/src/api/check.ts @@ -140,6 +140,30 @@ export const saveCheckDmn = async (checkId: string, dmnModel: string) => { } }; +export const validateCheckDmn = async (checkId: string, dmnModel: string): Promise => { + const url = apiUrl + "/validate-check-dmn"; + try { + const response = await authFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ id: checkId, dmnModel: dmnModel }), + }); + + if (!response.ok) { + throw new Error(`Validation failed with status: ${response.status}`); + } + + const data = await response.json(); + return data.errors; + } catch (error) { + console.error("Error validation DMN for check:", error); + throw error; // rethrow so you can handle it in your component if needed + } +}; + export const fetchUserDefinedChecks = async ( working: boolean ): Promise => { diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/EligibilityCheckDetail.tsx b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/EligibilityCheckDetail.tsx index 4f5028e3..b1db3b00 100644 --- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/EligibilityCheckDetail.tsx +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/EligibilityCheckDetail.tsx @@ -1,30 +1,49 @@ -import { Accessor, createSignal, For, Match, Show, Switch } from "solid-js"; +import { createSignal, Match, Show, Switch } from "solid-js"; import { useParams } from "@solidjs/router"; +import { clsx } from "clsx"; +import toast from "solid-toast"; + import Header from "../../../Header"; import Loading from "../../../Loading"; -import ParameterModal from "./modals/ParameterModal"; import KogitoDmnEditorView from "./KogitoDmnEditorView"; +import EligibilityCheckTest from "./checkTesting/EligibilityCheckTest"; +import PublishCheck from "./PublishCheck"; import eligibilityCheckDetailResource from "./eligibilityCheckDetailResource"; -import type { EligibilityCheck, ParameterDefinition } from "@/types"; -import ConfirmationModal from "@/components/shared/ConfirmationModal"; -import EligibilityCheckTest from "./checkTesting/EligibilityCheckTest"; -import PublishCheck from "./PublishCheck"; +import ErrorDisplayModal from "@/components/shared/ErrorModal"; +import ParametersConfiguration from "./ParametersConfiguration"; -type CheckDetailScreenMode = "Parameter Configuration" | "DMN Definition" | "Testing" | "Publish"; +type CheckDetailScreenMode = "Parameter Configuration" | "DMN Definition" | "Testing" | "Publish"; const EligibilityCheckDetail = () => { const { checkId } = useParams(); - const [tmpDmnModel, setTmpDmnModel] = createSignal(""); + const [currentDmnModel, setCurrentDmnModel] = createSignal(""); const [screenMode, setScreenMode] = createSignal("Parameter Configuration"); + const [validationErrors, setValidationErrors] = createSignal([]); + const [showingErrorModal, setShowingErrorModal] = createSignal(false); + const { eligibilityCheck, actions, actionInProgress, initialLoadStatus } = eligibilityCheckDetailResource(() => checkId); + const hasDmnModelChanged = (): boolean => { + return eligibilityCheck().dmnModel !== currentDmnModel(); + }; + + const validateDmnModel = async (dmnString: string) => { + const errors: string[] = await actions.validateDmnModel(dmnString); + setValidationErrors(errors); + if (errors.length > 0) { + setShowingErrorModal(true); + } else { + toast.success("No validation errors found in DMN model."); + } + } + return (
@@ -49,7 +68,7 @@ const EligibilityCheckDetail = () => { - { <>
actions.saveDmnModel(tmpDmnModel())} + class="btn-default btn-blue" + onClick={() => validateDmnModel(currentDmnModel())} > - Save DMN + Validate Current DMN +
+
actions.saveDmnModel(currentDmnModel())} + > + Save Changes
eligibilityCheck().dmnModel} - setTmpDmnModel={setTmpDmnModel} + dmnModelToLoad={() => eligibilityCheck().dmnModel} + onDmnModelChange={setCurrentDmnModel} />
@@ -86,125 +111,13 @@ const EligibilityCheckDetail = () => {
-
- ); -}; - -const ParametersScreen = ({ - eligibilityCheck, - addParameter, - editParameter, - removeParameter, -}: { - eligibilityCheck: Accessor; - addParameter: (parameter: ParameterDefinition) => Promise; - editParameter: ( - parameterIndex: number, - parameter: ParameterDefinition - ) => Promise; - removeParameter: (parameterIndex: number) => Promise; -}) => { - const [addingParameter, setAddingParameter] = createSignal(false); - const [parameterIndexToEdit, setParameterIndexToEdit] = createSignal< - null | number - >(null); - const [parameterIndexToRemove, setParameterIndexToRemove] = createSignal< - null | number - >(null); - - const handleProjectMenuClicked = (e, parameterIndex: number) => { - e.stopPropagation(); - setParameterIndexToRemove(parameterIndex); - }; - - return ( -
-
- {eligibilityCheck().name} -
-

{eligibilityCheck().description}

-
-

Parameters

-
{ - setAddingParameter(true); - }} - > - Create New Parameter -
- 0} - fallback={

No parameters defined.

} - > -
- - {(param, parameterIndex) => ( -
{ - console.log("here"); - setParameterIndexToEdit(parameterIndex()); - }} - > -
- {param.key} -
-
- Type: {param.type} -
-
- Label: {param.label} -
-
- Required:{" "} - {param.required.toString()} -
-
- handleProjectMenuClicked(e, parameterIndex()) - } - > - X -
-
- )} -
-
-
-
- {addingParameter() && ( - setAddingParameter(false)} - modalAction={addParameter} + + setShowingErrorModal(false)} /> - )} - {parameterIndexToEdit() !== null && ( - setParameterIndexToEdit(null)} - modalAction={async (parameter) => { - editParameter(parameterIndexToEdit(), parameter); - }} - initialData={{ - key: eligibilityCheck().parameters[parameterIndexToEdit()].key, - type: eligibilityCheck().parameters[parameterIndexToEdit()].type, - label: eligibilityCheck().parameters[parameterIndexToEdit()].label, - required: - eligibilityCheck().parameters[parameterIndexToEdit()].required, - }} - /> - )} - {parameterIndexToRemove() !== null && ( - removeParameter(parameterIndexToRemove())} - closeModal={() => setParameterIndexToRemove(null)} - /> - )} +
); }; diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/KogitoDmnEditorView.tsx b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/KogitoDmnEditorView.tsx index 4675a0cd..61b7ff51 100644 --- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/KogitoDmnEditorView.tsx +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/KogitoDmnEditorView.tsx @@ -1,4 +1,4 @@ -import { onCleanup, onMount, Accessor, Setter, createEffect } from "solid-js"; +import { onCleanup, onMount, Accessor, Setter } from "solid-js"; import * as DmnEditor from "@kogito-tooling/kie-editors-standalone/dist/dmn"; @@ -15,12 +15,13 @@ const trimXml = (xml: string): string => { return xml; }; -export default function KogitoDmnEditorView({ - dmnModel, setTmpDmnModel, +const KogitoDmnEditorView = ({ + dmnModelToLoad, + onDmnModelChange, }: { - dmnModel: Accessor; - setTmpDmnModel: Setter; -}) { + dmnModelToLoad: Accessor; + onDmnModelChange: (dmnModelXml: string) => void; +}) => { let editorElement: null | Element = null; let editorObject: null | any = null; let saveTimeoutId: null | number = null; @@ -35,7 +36,7 @@ export default function KogitoDmnEditorView({ }); const initializeEditor = async () => { - const modelXml: string = dmnModel(); + const modelXml: string = dmnModelToLoad(); const initialDmnPromise: string = (modelXml) ? trimXml(modelXml): ""; editorObject = DmnEditor.open({ @@ -44,10 +45,12 @@ export default function KogitoDmnEditorView({ resources: new Map(), readOnly: false, }); + onDmnModelChange(modelXml); + editorObject.subscribeToContentChanges( async (_: boolean) => { const xml = await editorObject.getContent(); - setTmpDmnModel(xml); + onDmnModelChange(xml); } ); }; @@ -60,4 +63,6 @@ export default function KogitoDmnEditorView({ /> ); -} \ No newline at end of file +} + +export default KogitoDmnEditorView; diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/ParametersConfiguration.tsx b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/ParametersConfiguration.tsx new file mode 100644 index 00000000..0a4a6111 --- /dev/null +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/ParametersConfiguration.tsx @@ -0,0 +1,124 @@ +import { Accessor, createSignal, For, Show } from "solid-js"; + +import ParameterModal from "./modals/ParameterModal"; + +import ConfirmationModal from "@/components/shared/ConfirmationModal"; +import type { EligibilityCheck, ParameterDefinition } from "@/types"; + + +const ParametersConfiguration = ({ + eligibilityCheck, + addParameter, + editParameter, + removeParameter, +}: { + eligibilityCheck: Accessor; + addParameter: (parameter: ParameterDefinition) => Promise; + editParameter: ( + parameterIndex: number, + parameter: ParameterDefinition + ) => Promise; + removeParameter: (parameterIndex: number) => Promise; +}) => { + const [addingParameter, setAddingParameter] = createSignal(false); + const [parameterIndexToEdit, setParameterIndexToEdit] = createSignal(null); + const [parameterIndexToRemove, setParameterIndexToRemove] = createSignal(null); + + const handleProjectMenuClicked = (e, parameterIndex: number) => { + e.stopPropagation(); + setParameterIndexToRemove(parameterIndex); + }; + + return ( +
+
+ {eligibilityCheck().name} +
+

{eligibilityCheck().description}

+
+

Parameters

+
{ + setAddingParameter(true); + }} + > + Create New Parameter +
+ 0} + fallback={

No parameters defined.

} + > +
+ + {(param, parameterIndex) => ( +
{ + console.log("here"); + setParameterIndexToEdit(parameterIndex()); + }} + > +
+ {param.key} +
+
+ Type: {param.type} +
+
+ Label: {param.label} +
+
+ Required:{" "} + {param.required.toString()} +
+
+ handleProjectMenuClicked(e, parameterIndex()) + } + > + X +
+
+ )} +
+
+
+
+ {addingParameter() && ( + setAddingParameter(false)} + modalAction={addParameter} + /> + )} + {parameterIndexToEdit() !== null && ( + setParameterIndexToEdit(null)} + modalAction={async (parameter) => { + editParameter(parameterIndexToEdit(), parameter); + }} + initialData={{ + key: eligibilityCheck().parameters[parameterIndexToEdit()].key, + type: eligibilityCheck().parameters[parameterIndexToEdit()].type, + label: eligibilityCheck().parameters[parameterIndexToEdit()].label, + required: + eligibilityCheck().parameters[parameterIndexToEdit()].required, + }} + /> + )} + {parameterIndexToRemove() !== null && ( + removeParameter(parameterIndexToRemove())} + closeModal={() => setParameterIndexToRemove(null)} + /> + )} +
+ ); +}; + +export default ParametersConfiguration; diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/PublishCheck.tsx b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/PublishCheck.tsx index 7a3a2e3a..357f5b5c 100644 --- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/PublishCheck.tsx +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/PublishCheck.tsx @@ -43,7 +43,7 @@ const PublishCheck = ({ {(checkVersion) => (
- Version {checkVersion.version} + {checkVersion.name} - v{checkVersion.version}
diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/eligibilityCheckDetailResource.ts b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/eligibilityCheckDetailResource.ts index d1a4f52b..62f80579 100644 --- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/eligibilityCheckDetailResource.ts +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/eligibilityCheckDetailResource.ts @@ -13,6 +13,7 @@ import { updateCheck, evaluateWorkingCheck, publishCheck as publishCheckApi, + validateCheckDmn, } from "@/api/check"; import type { @@ -32,6 +33,7 @@ export interface EligibilityCheckDetailResource { ) => Promise; removeParameter: (parameterIndex: number) => Promise; saveDmnModel: (dmnString: string) => Promise; + validateDmnModel: (dmnString: string) => Promise; testEligibility: (checkConfg: CheckConfig, inputData: Record) => Promise; publishCheck: (checkId: string) => Promise; }; @@ -126,6 +128,7 @@ const eligibilityCheckDetailResource = ( setActionInProgress(true); try { await saveCheckDmn(eligibilityCheck.id, dmnString); + toast.success("DMN model saved successfully."); await refetch(); } catch (e) { console.error("Failed to save DMN model", e); @@ -133,6 +136,19 @@ const eligibilityCheckDetailResource = ( setActionInProgress(false); }; + const validateDmnModel = async (dmnString: string) => { + setActionInProgress(true); + try { + const errors: string[] = await validateCheckDmn(eligibilityCheck.id, dmnString); + console.log(errors); + setActionInProgress(false); + return errors; + } catch (e) { + console.error("Failed to validate DMN model", e); + } + return []; + }; + const testEligibility = async (checkConfg: CheckConfig, inputData: Record): Promise => { setActionInProgress(true); try { @@ -164,6 +180,7 @@ const eligibilityCheckDetailResource = ( updateParameter, removeParameter, saveDmnModel, + validateDmnModel, testEligibility, publishCheck, }, diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx index 5f2ce6f5..707bd576 100644 --- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckDetail/modals/ParameterModal.tsx @@ -1,7 +1,6 @@ import { createStore } from "solid-js/store" -import type { EligibilityCheck, ParameterDefinition } from "@/types"; -import { Accessor } from "solid-js"; +import type { ParameterDefinition } from "@/types"; type ParamValues = { diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.ts b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.ts index 80a19bd0..58aef002 100644 --- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.ts +++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.ts @@ -2,11 +2,7 @@ import { createResource, createEffect, Accessor, createSignal } from "solid-js"; import { createStore } from "solid-js/store"; import type { EligibilityCheck } from "@/types"; -import { - addCheck, - fetchPublicChecks, - fetchUserDefinedChecks, -} from "@/api/check"; +import { addCheck, fetchUserDefinedChecks } from "@/api/check"; export interface EligibilityCheckResource { checks: () => EligibilityCheck[]; diff --git a/builder-frontend/src/components/project/manageBenefits/configureBenefit/ConfigureBenefit.tsx b/builder-frontend/src/components/project/manageBenefits/configureBenefit/ConfigureBenefit.tsx index 2b2ef0e6..bc5e942d 100644 --- a/builder-frontend/src/components/project/manageBenefits/configureBenefit/ConfigureBenefit.tsx +++ b/builder-frontend/src/components/project/manageBenefits/configureBenefit/ConfigureBenefit.tsx @@ -96,20 +96,18 @@ const ConfigureBenefit = ({ {(checkConfig, checkIndex) => { return ( - - checkConfig} - onRemove={() => - onRemoveEligibilityCheck(checkIndex()) + checkConfig.checkId} + checkConfig={() => checkConfig} + onRemove={() => + onRemoveEligibilityCheck(checkIndex()) + } + updateCheckConfigParams={ + (newCheckData: ParameterValues) => { + actions.updateCheckConfigParams(checkIndex(), newCheckData); } - updateCheckConfigParams={ - (newCheckData: ParameterValues) => { - actions.updateCheckConfigParams(checkIndex(), newCheckData); - } - } - /> - + } + /> ); }} diff --git a/builder-frontend/src/components/project/manageBenefits/configureBenefit/EligibilityCheckListView.tsx b/builder-frontend/src/components/project/manageBenefits/configureBenefit/EligibilityCheckListView.tsx index eacdf978..248dfe43 100644 --- a/builder-frontend/src/components/project/manageBenefits/configureBenefit/EligibilityCheckListView.tsx +++ b/builder-frontend/src/components/project/manageBenefits/configureBenefit/EligibilityCheckListView.tsx @@ -141,7 +141,7 @@ const EligibilityCheckRow = ({ {titleCase(check.name)} {check.description} - {check.version} + v{check.version} ); }; diff --git a/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx b/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx index 6df3ca9e..f39f5c54 100644 --- a/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx +++ b/builder-frontend/src/components/project/manageBenefits/configureBenefit/SelectedEligibilityCheck.tsx @@ -1,15 +1,16 @@ -import { Accessor, createSignal, For } from "solid-js"; +import { Accessor, createResource, createSignal, For, Show } from "solid-js"; import ConfigureCheckModal from "./modals/ConfigureCheckModal"; +import { fetchCustomCheck } from "@/api/check"; import { titleCase } from "@/utils/title_case"; import type { CheckConfig, - EligibilityCheck, ParameterDefinition, ParameterValues, } from "@/types"; +import Loading from "@/components/Loading"; interface ParameterWithConfiguredValue { parameter: ParameterDefinition; @@ -17,99 +18,101 @@ interface ParameterWithConfiguredValue { } const SelectedEligibilityCheck = ( - { check, checkConfig, onRemove, updateCheckConfigParams }: + { checkId, checkConfig, onRemove, updateCheckConfigParams }: { - check: EligibilityCheck; + checkId: Accessor; checkConfig: Accessor; onRemove: () => void; updateCheckConfigParams: (newCheckData: ParameterValues) => void } ) => { + const [check] = createResource(() => checkId(), fetchCustomCheck); const [configuringCheckModalOpen, setConfiguringCheckModalOpen] = createSignal(false); - const checkParameters: Accessor = () => check.parameters.map((param) => { + const checkParameters: Accessor = () => check().parameters.map((param) => { return { parameter: param, value: checkConfig().parameters[param.key]! }; }); const unfilledRequiredParameters = () => { return []; }; - // const unfilledRequiredParameters = () => {return check.parameters.filter( - // (param) => param.required && param.value === undefined - // )}; return ( <> -
{ - setConfiguringCheckModalOpen(true); - }} - class=" - mb-4 p-4 cursor-pointer select-none relative - border-2 border-gray-200 rounded-lg hover:bg-gray-200" - > + + + + +
{ e.stopPropagation(); onRemove(); }} + onClick={() => { setConfiguringCheckModalOpen(true); }} + class=" + mb-4 p-4 cursor-pointer select-none relative + border-2 border-gray-200 rounded-lg hover:bg-gray-200" > - X -
-
{titleCase(check.name)}
-
{check.description}
- - {check.inputs.length > 0 && ( -
-
Inputs
- - {(input) => ( -
-
{titleCase(input.key)}:
-
"{input.prompt}"
-
- )} -
+
{ e.stopPropagation(); onRemove(); }} + > + X
- )} - {checkParameters().length > 0 && ( -
-
Parameters
- - {({ parameter, value }: ParameterWithConfiguredValue) => { - const getLabel = () => { - return value !== undefined ? ( - value.toString() - ) : ( - Not configured - ); - }; - return ( +
{titleCase(check().name)} - v{check().version}
+
{check().description}
+ + {check().inputs.length > 0 && ( +
+
Inputs
+ + {(input) => (
-
{titleCase(parameter.key)}:
-
{getLabel()}
+
{titleCase(input.key)}:
+
"{input.prompt}"
- ); - }} -
-
- )} - {unfilledRequiredParameters().length > 0 && ( -
- Warning: This check has required parameter(s) that are not - configured. Click here to edit. -
+ )} +
+
+ )} + {checkParameters().length > 0 && ( +
+
Parameters
+ + {({ parameter, value }: ParameterWithConfiguredValue) => { + const getLabel = () => { + return value !== undefined ? ( + value.toString() + ) : ( + Not configured + ); + }; + return ( +
+
{titleCase(parameter.key)}:
+
{getLabel()}
+
+ ); + }} +
+
+ )} + {unfilledRequiredParameters().length > 0 && ( +
+ Warning: This check has required parameter(s) that are not + configured. Click here to edit. +
+ )} +
+ + {configuringCheckModalOpen() && ( + { + setConfiguringCheckModalOpen(false); + }} + /> )} -
- - {configuringCheckModalOpen() && ( - { - setConfiguringCheckModalOpen(false); - }} - /> - )} + ); }; diff --git a/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx b/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx index ea6a5742..b18ae669 100644 --- a/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx +++ b/builder-frontend/src/components/project/manageBenefits/configureBenefit/modals/ConfigureCheckModal.tsx @@ -18,7 +18,7 @@ const ConfigureCheckModal = ( { checkConfig, check, updateCheckConfigParams, closeModal }: { checkConfig: Accessor; - check: EligibilityCheck; + check: Accessor; updateCheckConfigParams: (newCheckData: ParameterValues) => void; closeModal: () => void } @@ -41,14 +41,14 @@ const ConfigureCheckModal = ( Configure Check: {titleCase(checkConfig().checkName)}
- {check.parameters.length === 0 && ( + {check().parameters.length === 0 && (
This check has no configurable parameters.
)} - {check.parameters.length > 0 && ( + {check().parameters.length > 0 && (
Parameters
- + {(parameter) => { return ( void, errors: string[] } +) => { + return ( +
+
+
{title}
+
+
+ + {(error, errorIndex) => ( + <> +
+ {error} +
+ + )} +
+
+
+ +
+
{ closeModal(); }} + > + Close +
+
+
+
+ ); +} +export default ErrorDisplayModal; diff --git a/builder-frontend/src/index.css b/builder-frontend/src/index.css index 5dfcf466..3baf52d0 100644 --- a/builder-frontend/src/index.css +++ b/builder-frontend/src/index.css @@ -17,6 +17,9 @@ .btn-default.btn-red { @apply bg-red-800 hover:bg-red-900 text-white; } + .btn-default.btn-yellow { + @apply bg-yellow-700 hover:bg-yellow-800 text-white; + } .btn-default.btn-blue { @apply bg-sky-600 hover:bg-sky-700 text-white }