Skip to content

Commit 54a01e5

Browse files
Merge pull request #293 from CodeForPhilly/form-paths-typing
feat: Updated FormPath dropdown to preserve types in the FormEditor.
2 parents 2a5f7d8 + eaef0e7 commit 54a01e5

File tree

10 files changed

+188
-45
lines changed

10 files changed

+188
-45
lines changed

builder-api/src/main/java/org/acme/controller/ScreenerResource.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import java.util.ArrayList;
2727
import java.util.Collections;
28+
import java.util.Comparator;
2829
import java.util.List;
2930
import java.util.Map;
3031
import java.util.Optional;
@@ -284,8 +285,13 @@ public Response getScreenerFormPaths(@Context SecurityIdentity identity,
284285

285286
try {
286287
List<Benefit> benefits = screenerRepository.getBenefitsInScreener(screener);
287-
List<String> paths = new ArrayList<>(inputSchemaService.extractAllInputPaths(benefits));
288-
Collections.sort(paths);
288+
List<FormPath> paths = new ArrayList<>(inputSchemaService.extractAllInputPaths(benefits));
289+
Collections.sort(paths, new Comparator<FormPath>() {
290+
public int compare(FormPath fp1, FormPath fp2) {
291+
// compare two instance of `Score` and return `int` as result.
292+
return fp1.getPath().compareTo(fp2.getPath());
293+
}
294+
});
289295
return Response.ok().entity(new FormPathsResponse(paths)).build();
290296
} catch (Exception e) {
291297
Log.error(e);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.acme.model.domain;
2+
3+
public class FormPath {
4+
private String path;
5+
private String type;
6+
7+
public FormPath() {
8+
}
9+
10+
public FormPath(String path, String type) {
11+
this.path = path;
12+
this.type = type;
13+
}
14+
15+
public String getPath() {
16+
return path;
17+
}
18+
19+
public void setPath(String path) {
20+
this.path = path;
21+
}
22+
23+
public String getType() {
24+
return type;
25+
}
26+
27+
public void setType(String type) {
28+
this.type = type;
29+
}
30+
31+
@Override
32+
public boolean equals(Object o) {
33+
if (this == o) return true;
34+
if (o == null || getClass() != o.getClass()) return false; // Check for null and type equality
35+
FormPath formPath = (FormPath) o; // Cast the object
36+
// Compare relevant fields using Objects.equals() for objects and == for primitives
37+
return path.equals(formPath.path) && type.equals(formPath.type);
38+
}
39+
}

builder-api/src/main/java/org/acme/model/dto/FormPathsResponse.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,27 @@
22

33
import java.util.List;
44

5+
import org.acme.model.domain.FormPath;
6+
57
/**
68
* Response DTO for the form paths endpoint.
79
* Contains the list of unique input paths required by all checks in a screener.
810
*/
911
public class FormPathsResponse {
10-
private List<String> paths;
12+
private List<FormPath> paths;
1113

1214
public FormPathsResponse() {
1315
}
1416

15-
public FormPathsResponse(List<String> paths) {
17+
public FormPathsResponse(List<FormPath> paths) {
1618
this.paths = paths;
1719
}
1820

19-
public List<String> getPaths() {
21+
public List<FormPath> getPaths() {
2022
return paths;
2123
}
2224

23-
public void setPaths(List<String> paths) {
25+
public void setPaths(List<FormPath> paths) {
2426
this.paths = paths;
2527
}
2628
}

builder-api/src/main/java/org/acme/service/InputSchemaService.java

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import org.acme.model.domain.Benefit;
99
import org.acme.model.domain.CheckConfig;
10+
import org.acme.model.domain.FormPath;
1011

1112
import java.util.*;
1213

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

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

3435
for (CheckConfig check : checks) {
3536
JsonNode transformedSchema = transformInputDefinitionSchema(check);
36-
List<String> paths = extractJsonSchemaPaths(transformedSchema);
37+
List<FormPath> paths = extractJsonSchemaPaths(transformedSchema);
3738
pathSet.addAll(paths);
3839
}
3940
}
@@ -261,19 +262,19 @@ public JsonNode transformEnrollmentsSchema(JsonNode schema, List<String> personI
261262
* @param jsonSchema The JSON Schema to parse
262263
* @return List of dot-separated paths (e.g., ["people.applicant.dateOfBirth", "income"])
263264
*/
264-
public List<String> extractJsonSchemaPaths(JsonNode jsonSchema) {
265+
public List<FormPath> extractJsonSchemaPaths(JsonNode jsonSchema) {
265266
if (jsonSchema == null || !jsonSchema.has("properties")) {
266267
return new ArrayList<>();
267268
}
268269

269270
return traverseSchema(jsonSchema, "");
270271
}
271272

272-
private List<String> traverseSchema(JsonNode schema, String parentPath) {
273-
List<String> paths = new ArrayList<>();
273+
private List<FormPath> traverseSchema(JsonNode schema, String parentPath) {
274+
List<FormPath> formPaths = new ArrayList<>();
274275

275276
if (schema == null || !schema.has("properties")) {
276-
return paths;
277+
return formPaths;
277278
}
278279

279280
JsonNode propertiesJsonNode = schema.get("properties");
@@ -295,26 +296,54 @@ private List<String> traverseSchema(JsonNode schema, String parentPath) {
295296
}
296297

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

299301
// If this property has nested properties, recurse into it
300302
if (propValue.has("properties")) {
301-
paths.addAll(traverseSchema(propValue, currentPath));
302-
} else if ("array".equals(getType(propValue)) && propValue.has("items")) {
303+
formPaths.addAll(traverseSchema(propValue, currentPath));
304+
} else if ("array".equals(currentType) && propValue.has("items")) {
303305
// Handle arrays - recurse into items schema with the current path
304306
JsonNode itemsSchema = propValue.get("items");
305307
if (itemsSchema.has("properties")) {
306-
paths.addAll(traverseSchema(itemsSchema, currentPath));
308+
formPaths.addAll(traverseSchema(itemsSchema, currentPath));
307309
} else {
308310
// Array of primitives - add the path
309-
paths.add(currentPath);
311+
String itemType = getType(itemsSchema);
312+
formPaths.add(new FormPath(currentPath, "array:" + (itemType != null ? itemType : "any")));
310313
}
311314
} else {
312315
// Leaf property - add the path
313-
paths.add(currentPath);
316+
formPaths.add(new FormPath(currentPath, currentType));
314317
}
315318
}
316319

317-
return paths;
320+
return formPaths;
321+
}
322+
323+
/**
324+
* Determines the effective type of a JSON Schema property, considering format hints.
325+
* For example, a string with format "date" returns "date" instead of "string".
326+
*/
327+
private String getEffectiveType(JsonNode schema) {
328+
if (schema == null) {
329+
return "any";
330+
}
331+
332+
String type = getType(schema);
333+
if (type == null) {
334+
return "any";
335+
}
336+
337+
// Check for format hints that provide more specific type info
338+
if (type.equals("string") && schema.has("format")) {
339+
String format = schema.get("format").asText();
340+
// Common date/time formats
341+
if ("date".equals(format) || "date-time".equals(format) || "time".equals(format)) {
342+
return format;
343+
}
344+
}
345+
346+
return type;
318347
}
319348

320349
private String getType(JsonNode schema) {

builder-api/src/test/java/org/acme/service/InputSchemaServiceTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.fasterxml.jackson.databind.JsonNode;
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import org.acme.model.domain.CheckConfig;
6+
import org.acme.model.domain.FormPath;
67
import org.junit.jupiter.api.BeforeEach;
78
import org.junit.jupiter.api.Test;
89

@@ -548,10 +549,10 @@ void extractJsonSchemaPaths_withTransformedSchema_extractsCorrectPaths() throws
548549
""";
549550
JsonNode schema = objectMapper.readTree(schemaJson);
550551

551-
List<String> paths = service.extractJsonSchemaPaths(schema);
552+
List<FormPath> paths = service.extractJsonSchemaPaths(schema);
552553

553-
assertTrue(paths.contains("people.applicant.dateOfBirth"));
554-
assertTrue(paths.contains("people.applicant.enrollments"));
554+
assertTrue(paths.contains(new FormPath("people.applicant.dateOfBirth", "string")));
555+
assertTrue(paths.contains(new FormPath("people.applicant.enrollments", "array")));
555556
assertEquals(2, paths.size());
556557
}
557558

@@ -581,9 +582,9 @@ void extractJsonSchemaPaths_withEnrollmentOnlySchema_extractsCorrectPath() throw
581582
""";
582583
JsonNode schema = objectMapper.readTree(schemaJson);
583584

584-
List<String> paths = service.extractJsonSchemaPaths(schema);
585+
List<FormPath> paths = service.extractJsonSchemaPaths(schema);
585586

586-
assertTrue(paths.contains("people.applicant.enrollments"));
587+
assertTrue(paths.contains(new FormPath("people.applicant.enrollments", "array")));
587588
assertEquals(1, paths.size());
588589
}
589590

builder-frontend/src/api/screener.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { env } from "@/config/environment";
22

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

5-
import type { BenefitDetail, ScreenerResult } from "@/types";
5+
import type { BenefitDetail, FormPath, ScreenerResult } from "@/types";
66

77
const apiUrl = env.apiUrl;
88

@@ -159,7 +159,7 @@ export const removeCustomBenefit = async (
159159
};
160160

161161
export interface FormPathsResponse {
162-
paths: string[];
162+
paths: FormPath[];
163163
}
164164

165165
export const fetchFormPaths = async (screenerId: string): Promise<FormPathsResponse> => {

builder-frontend/src/components/project/FormEditorView.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
onCleanup, onMount,
33
createEffect, createSignal, createResource,
44
For, Match, Show, Switch,
5+
Accessor,
56
} from "solid-js";
67
import toast from "solid-toast";
78
import { useParams } from "@solidjs/router";
@@ -19,14 +20,15 @@ import Loading from "../Loading";
1920

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

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

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

104-
const paths = formPaths() || [];
105-
const validPathSet = new Set(paths);
106+
const currentFormPaths: FormPath[] = formPaths() || [];
107+
const validPathSet = new Set(currentFormPaths.map((formPath: FormPath) => formPath.path));
106108

107109
const pathOptionsService = formEditor.get("pathOptionsService") as PathOptionsService;
108110
pathOptionsService.setOptions(
109-
paths.map((path) => ({ value: path, label: path }))
111+
currentFormPaths.map(
112+
(formPath: FormPath) => ({ value: formPath.path, label: formPath.path, type: formPath.type })
113+
)
110114
);
111115

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

196-
const FormValidationDrawer = ({ formSchema, expectedInputPaths }) => {
200+
const FormValidationDrawer = (
201+
{ formSchema, expectedInputPaths }:
202+
{formSchema: any, expectedInputPaths: Accessor<FormPath[]>}
203+
) => {
197204
const formOutputs = () =>
198205
formSchema() ? extractFormPaths(formSchema()) : [];
199206

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

206213
const satisfiedInputs = () =>
207-
expectedInputs().filter((p) => formOutputSet().has(p));
214+
expectedInputs().filter((formPath) => formOutputSet().has(formPath.path));
208215

209216
const missingInputs = () =>
210-
expectedInputs().filter((p) => !formOutputSet().has(p));
217+
expectedInputs().filter((formPath) => !formOutputSet().has(formPath.path));
211218

212219
return (
213220
<Drawer side="right">
@@ -281,9 +288,9 @@ const FormValidationDrawer = ({ formSchema, expectedInputPaths }) => {
281288
</p>
282289
}
283290
>
284-
{(path) => (
291+
{(formPath) => (
285292
<div class="py-2 px-3 mb-2 bg-red-50 rounded border border-red-300 font-mono text-sm text-red-800">
286-
{path}
293+
{formPath.path} ({formPath.type})
287294
</div>
288295
)}
289296
</For>
@@ -302,9 +309,9 @@ const FormValidationDrawer = ({ formSchema, expectedInputPaths }) => {
302309
</p>
303310
}
304311
>
305-
{(path) => (
312+
{(formPath) => (
306313
<div class="py-2 px-3 mb-2 bg-green-50 rounded border border-green-300 font-mono text-sm text-green-800">
307-
{path}
314+
{formPath.path} ({formPath.type})
308315
</div>
309316
)}
310317
</For>

builder-frontend/src/components/project/formJsExtensions/customKeyDropdown/customKeyDropdownProvider.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import SelectEntry from './bpmn-io-dependencies';
22

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

75+
// Get the component type to filter compatible options
76+
const componentType = field.type || '';
77+
78+
console.log(componentType);
79+
7680
// Get options from the injected service, passing current key to exclude from disabling
77-
const options = pathOptionsService?.getOptions(currentKey) || [];
81+
const options = pathOptionsService?.getOptions(currentKey, componentType) || [];
7882

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

0 commit comments

Comments
 (0)