Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -284,8 +285,13 @@ public Response getScreenerFormPaths(@Context SecurityIdentity identity,

try {
List<Benefit> benefits = screenerRepository.getBenefitsInScreener(screener);
List<String> paths = new ArrayList<>(inputSchemaService.extractAllInputPaths(benefits));
Collections.sort(paths);
List<FormPath> paths = new ArrayList<>(inputSchemaService.extractAllInputPaths(benefits));
Collections.sort(paths, new Comparator<FormPath>() {
public int compare(FormPath fp1, FormPath fp2) {
// compare two instance of `Score` and return `int` as result.
return fp1.getPath().compareTo(fp2.getPath());
}
});
return Response.ok().entity(new FormPathsResponse(paths)).build();
} catch (Exception e) {
Log.error(e);
Expand Down
39 changes: 39 additions & 0 deletions builder-api/src/main/java/org/acme/model/domain/FormPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.acme.model.domain;

public class FormPath {
private String path;
private String type;

public FormPath() {
}

public FormPath(String path, String type) {
this.path = path;
this.type = type;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; // Check for null and type equality
FormPath formPath = (FormPath) o; // Cast the object
// Compare relevant fields using Objects.equals() for objects and == for primitives
return path.equals(formPath.path) && type.equals(formPath.type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@

import java.util.List;

import org.acme.model.domain.FormPath;

/**
* Response DTO for the form paths endpoint.
* Contains the list of unique input paths required by all checks in a screener.
*/
public class FormPathsResponse {
private List<String> paths;
private List<FormPath> paths;

public FormPathsResponse() {
}

public FormPathsResponse(List<String> paths) {
public FormPathsResponse(List<FormPath> paths) {
this.paths = paths;
}

public List<String> getPaths() {
public List<FormPath> getPaths() {
return paths;
}

public void setPaths(List<String> paths) {
public void setPaths(List<FormPath> paths) {
this.paths = paths;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import org.acme.model.domain.Benefit;
import org.acme.model.domain.CheckConfig;
import org.acme.model.domain.FormPath;

import java.util.*;

Expand All @@ -24,16 +25,16 @@ public class InputSchemaService {
* @param benefits List of benefits containing checks with inputDefinitions
* @return Set of unique dot-separated paths (e.g., "people.applicant.dateOfBirth")
*/
public Set<String> extractAllInputPaths(List<Benefit> benefits) {
Set<String> pathSet = new HashSet<>();
public Set<FormPath> extractAllInputPaths(List<Benefit> benefits) {
Set<FormPath> pathSet = new HashSet<>();

for (Benefit benefit : benefits) {
List<CheckConfig> checks = benefit.getChecks();
if (checks == null) continue;

for (CheckConfig check : checks) {
JsonNode transformedSchema = transformInputDefinitionSchema(check);
List<String> paths = extractJsonSchemaPaths(transformedSchema);
List<FormPath> paths = extractJsonSchemaPaths(transformedSchema);
pathSet.addAll(paths);
}
}
Expand Down Expand Up @@ -212,19 +213,19 @@ public JsonNode transformEnrollmentsSchema(JsonNode schema, String personId) {
* @param jsonSchema The JSON Schema to parse
* @return List of dot-separated paths (e.g., ["people.applicant.dateOfBirth", "income"])
*/
public List<String> extractJsonSchemaPaths(JsonNode jsonSchema) {
public List<FormPath> extractJsonSchemaPaths(JsonNode jsonSchema) {
if (jsonSchema == null || !jsonSchema.has("properties")) {
return new ArrayList<>();
}

return traverseSchema(jsonSchema, "");
}

private List<String> traverseSchema(JsonNode schema, String parentPath) {
List<String> paths = new ArrayList<>();
private List<FormPath> traverseSchema(JsonNode schema, String parentPath) {
List<FormPath> formPaths = new ArrayList<>();

if (schema == null || !schema.has("properties")) {
return paths;
return formPaths;
}

JsonNode propertiesJsonNode = schema.get("properties");
Expand All @@ -246,26 +247,54 @@ private List<String> traverseSchema(JsonNode schema, String parentPath) {
}

String currentPath = parentPath.isEmpty() ? propKey : parentPath + "." + propKey;
String currentType = getEffectiveType(propValue);

// If this property has nested properties, recurse into it
if (propValue.has("properties")) {
paths.addAll(traverseSchema(propValue, currentPath));
} else if ("array".equals(getType(propValue)) && propValue.has("items")) {
formPaths.addAll(traverseSchema(propValue, currentPath));
} else if ("array".equals(currentType) && propValue.has("items")) {
// Handle arrays - recurse into items schema with the current path
JsonNode itemsSchema = propValue.get("items");
if (itemsSchema.has("properties")) {
paths.addAll(traverseSchema(itemsSchema, currentPath));
formPaths.addAll(traverseSchema(itemsSchema, currentPath));
} else {
// Array of primitives - add the path
paths.add(currentPath);
String itemType = getType(itemsSchema);
formPaths.add(new FormPath(currentPath, "array:" + (itemType != null ? itemType : "any")));
}
} else {
// Leaf property - add the path
paths.add(currentPath);
formPaths.add(new FormPath(currentPath, currentType));
}
}

return paths;
return formPaths;
}

/**
* Determines the effective type of a JSON Schema property, considering format hints.
* For example, a string with format "date" returns "date" instead of "string".
*/
private String getEffectiveType(JsonNode schema) {
if (schema == null) {
return "any";
}

String type = getType(schema);
if (type == null) {
return "any";
}

// Check for format hints that provide more specific type info
if (type.equals("string") && schema.has("format")) {
String format = schema.get("format").asText();
// Common date/time formats
if ("date".equals(format) || "date-time".equals(format) || "time".equals(format)) {
return format;
}
}

return type;
}

private String getType(JsonNode schema) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.acme.model.domain.CheckConfig;
import org.acme.model.domain.FormPath;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -303,10 +304,10 @@ void extractJsonSchemaPaths_withTransformedSchema_extractsCorrectPaths() throws
""";
JsonNode schema = objectMapper.readTree(schemaJson);

List<String> paths = service.extractJsonSchemaPaths(schema);
List<FormPath> paths = service.extractJsonSchemaPaths(schema);

assertTrue(paths.contains("people.applicant.dateOfBirth"));
assertTrue(paths.contains("people.applicant.enrollments"));
assertTrue(paths.contains(new FormPath("people.applicant.dateOfBirth", "string")));
assertTrue(paths.contains(new FormPath("people.applicant.enrollments", "array")));
assertEquals(2, paths.size());
}

Expand Down Expand Up @@ -336,9 +337,9 @@ void extractJsonSchemaPaths_withEnrollmentOnlySchema_extractsCorrectPath() throw
""";
JsonNode schema = objectMapper.readTree(schemaJson);

List<String> paths = service.extractJsonSchemaPaths(schema);
List<FormPath> paths = service.extractJsonSchemaPaths(schema);

assertTrue(paths.contains("people.applicant.enrollments"));
assertTrue(paths.contains(new FormPath("people.applicant.enrollments", "array")));
assertEquals(1, paths.size());
}
}
4 changes: 2 additions & 2 deletions builder-frontend/src/api/screener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { env } from "@/config/environment";

import { authDelete, authGet, authPatch, authPost } from "@/api/auth";

import type { BenefitDetail, ScreenerResult } from "@/types";
import type { BenefitDetail, FormPath, ScreenerResult } from "@/types";

const apiUrl = env.apiUrl;

Expand Down Expand Up @@ -159,7 +159,7 @@ export const removeCustomBenefit = async (
};

export interface FormPathsResponse {
paths: string[];
paths: FormPath[];
}

export const fetchFormPaths = async (screenerId: string): Promise<FormPathsResponse> => {
Expand Down
29 changes: 18 additions & 11 deletions builder-frontend/src/components/project/FormEditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
onCleanup, onMount,
createEffect, createSignal, createResource,
For, Match, Show, Switch,
Accessor,
} from "solid-js";
import toast from "solid-toast";
import { useParams } from "@solidjs/router";
Expand All @@ -19,14 +20,15 @@ import Loading from "../Loading";

import "@bpmn-io/form-js/dist/assets/form-js.css";
import "@bpmn-io/form-js-editor/dist/assets/form-js-editor.css";
import { FormPath } from "@/types";

function FormEditorView({ formSchema, setFormSchema }) {
const [isUnsaved, setIsUnsaved] = createSignal(false);
const [isSaving, setIsSaving] = createSignal(false);
const params = useParams();

// Fetch form paths from backend (replaces local transformation logic)
const [formPaths] = createResource(
const [formPaths] = createResource<FormPath[]>(
() => params.projectId,
async (screenerId: string) => {
if (!screenerId) return [];
Expand Down Expand Up @@ -101,12 +103,14 @@ function FormEditorView({ formSchema, setFormSchema }) {
createEffect(() => {
if (!formEditor || formPaths.loading) return;

const paths = formPaths() || [];
const validPathSet = new Set(paths);
const currentFormPaths: FormPath[] = formPaths() || [];
const validPathSet = new Set(currentFormPaths.map((formPath: FormPath) => formPath.path));

const pathOptionsService = formEditor.get("pathOptionsService") as PathOptionsService;
pathOptionsService.setOptions(
paths.map((path) => ({ value: path, label: path }))
currentFormPaths.map(
(formPath: FormPath) => ({ value: formPath.path, label: formPath.path, type: formPath.type })
)
);

// Clean up any form fields with keys that are no longer valid options
Expand Down Expand Up @@ -193,7 +197,10 @@ function FormEditorView({ formSchema, setFormSchema }) {
);
}

const FormValidationDrawer = ({ formSchema, expectedInputPaths }) => {
const FormValidationDrawer = (
{ formSchema, expectedInputPaths }:
{formSchema: any, expectedInputPaths: Accessor<FormPath[]>}
) => {
const formOutputs = () =>
formSchema() ? extractFormPaths(formSchema()) : [];

Expand All @@ -204,10 +211,10 @@ const FormValidationDrawer = ({ formSchema, expectedInputPaths }) => {
const formOutputSet = () => new Set(formOutputs());

const satisfiedInputs = () =>
expectedInputs().filter((p) => formOutputSet().has(p));
expectedInputs().filter((formPath) => formOutputSet().has(formPath.path));

const missingInputs = () =>
expectedInputs().filter((p) => !formOutputSet().has(p));
expectedInputs().filter((formPath) => !formOutputSet().has(formPath.path));

return (
<Drawer side="right">
Expand Down Expand Up @@ -281,9 +288,9 @@ const FormValidationDrawer = ({ formSchema, expectedInputPaths }) => {
</p>
}
>
{(path) => (
{(formPath) => (
<div class="py-2 px-3 mb-2 bg-red-50 rounded border border-red-300 font-mono text-sm text-red-800">
{path}
{formPath.path} ({formPath.type})
</div>
)}
</For>
Expand All @@ -302,9 +309,9 @@ const FormValidationDrawer = ({ formSchema, expectedInputPaths }) => {
</p>
}
>
{(path) => (
{(formPath) => (
<div class="py-2 px-3 mb-2 bg-green-50 rounded border border-green-300 font-mono text-sm text-green-800">
{path}
{formPath.path} ({formPath.type})
</div>
)}
</For>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import SelectEntry from './bpmn-io-dependencies';


/**
* Custom properties provider that replaces the path entry
*/
Expand Down Expand Up @@ -73,8 +72,13 @@ function CustomKeyDropdown(props: any) {
// Get current field's key so it won't be disabled in the dropdown
const currentKey = field.key || '';

// Get the component type to filter compatible options
const componentType = field.type || '';

console.log(componentType);

// Get options from the injected service, passing current key to exclude from disabling
const options = pathOptionsService?.getOptions(currentKey) || [];
const options = pathOptionsService?.getOptions(currentKey, componentType) || [];

// Add empty option
return [{ value: field.id, label: '(none)' }, ...options];
Expand Down
Loading
Loading