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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@earthranger/react-native-jsonforms-formatter",
"version": "2.0.0-beta.33",
"version": "2.0.0-beta.35",
"description": "Converts JTD into JSON Schema ",
"main": "./dist/bundle.js",
"types": "./dist/index.d.ts",
Expand Down
45 changes: 41 additions & 4 deletions src/v2/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,14 +340,45 @@ export const buildConditionSchema = (
};
};

/**
* Groups conditions by field, preserving the order fields first appear in.
*/
const groupConditionsByField = (conditions: V2Condition[]): V2Condition[][] => {
const order: string[] = [];
const groups: Record<string, V2Condition[]> = {};

conditions.forEach((condition) => {
if (!groups[condition.field]) {
groups[condition.field] = [];
order.push(condition.field);
}
groups[condition.field].push(condition);
});

return order.map((field) => groups[field]);
};
Comment thread
doneill marked this conversation as resolved.

const buildFieldGroupSchema = (group: V2Condition[]): Record<string, unknown> => {
if (group.length === 1) {
return buildConditionSchema(group[0]);
}

return {
anyOf: group.map((condition) => buildConditionSchema(condition)),
};
};

/**
* Builds a JSONForms schema-based condition from one or more V2 conditions.
*
* Single condition:
* - IS_EMPTY → root scope "#" (must check for field absence at object level)
* - All others → field scope "#/properties/fieldName"
*
* Multiple conditions: root scope "#" with allOf combining all condition schemas.
* Multiple conditions: grouped by field, then combined at root scope "#" —
* conditions on the same field are OR'd together (anyOf) as alternative
* values, and the resulting per-field groups are AND'd together (allOf) as
* independent requirements.
*/
export const buildSchemaBasedCondition = (
conditions: V2Condition[],
Expand All @@ -372,13 +403,19 @@ export const buildSchemaBasedCondition = (
};
}

// Multiple conditions: combine with allOf at root scope
const allOfSchemas = conditions.map((condition) => buildConditionSchema(condition));
const groups = groupConditionsByField(conditions);

if (groups.length === 1) {
return {
scope: '#',
schema: buildFieldGroupSchema(groups[0]),
};
}

return {
scope: '#',
schema: {
allOf: allOfSchemas,
allOf: groups.map((group) => buildFieldGroupSchema(group)),
},
};
};
Expand Down
74 changes: 74 additions & 0 deletions test/v2.conditions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,64 @@ describe('buildSchemaBasedCondition', () => {
});
});
});

describe('field grouping', () => {
it('same-field conditions are OR\'d (anyOf) with no schema flag needed', () => {
const result = buildSchemaBasedCondition([
makeCondition('yes_or_no', 'IS_EXACTLY', 'yes'),
makeCondition('yes_or_no', 'IS_EXACTLY', 'no'),
]) as any;
expect(result.schema.anyOf).toHaveLength(2);
expect(result.schema.allOf).toBeUndefined();
});

it('each same-field condition is still field-wrapped under anyOf', () => {
const result = buildSchemaBasedCondition([
makeCondition('yes_or_no', 'IS_EXACTLY', 'yes'),
makeCondition('yes_or_no', 'IS_EXACTLY', 'no'),
]) as any;
expect(result.schema.anyOf[0].properties.yes_or_no).toBeDefined();
expect(result.schema.anyOf[0].required).toContain('yes_or_no');
});

it('three same-field conditions all OR together', () => {
const result = buildSchemaBasedCondition([
makeCondition('status', 'IS_EXACTLY', 'active'),
makeCondition('status', 'IS_EXACTLY', 'pending'),
makeCondition('status', 'IS_EXACTLY', 'closed'),
]) as any;
expect(result.schema.anyOf).toHaveLength(3);
});

it('different-field conditions are AND\'d (allOf), unchanged from before', () => {
const result = buildSchemaBasedCondition([
makeCondition('injured_animal', 'CONTAINS', 'elephant'),
makeCondition('requires_attention', 'IS_NOT_EMPTY'),
]) as any;
expect(result.schema.allOf).toHaveLength(2);
expect(result.schema.anyOf).toBeUndefined();
});

it('mixes grouping: same-field group OR\'d, then AND\'d with other fields', () => {
const result = buildSchemaBasedCondition([
makeCondition('yes_or_no', 'IS_EXACTLY', 'yes'),
makeCondition('yes_or_no', 'IS_EXACTLY', 'no'),
makeCondition('requires_attention', 'IS_NOT_EMPTY'),
]) as any;
expect(result.schema.allOf).toHaveLength(2);
// First group: the two yes_or_no conditions, OR'd
expect(result.schema.allOf[0].anyOf).toHaveLength(2);
// Second group: the single requires_attention condition, not wrapped in anyOf
expect(result.schema.allOf[1].properties.requires_attention).toBeDefined();
});

it('field grouping does not apply to a single condition', () => {
const result = buildSchemaBasedCondition([
makeCondition('injured_animal', 'CONTAINS', 'elephant'),
]);
expect(result.scope).toBe('#/properties/injured_animal');
});
});
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -607,6 +665,22 @@ describe('createSectionRule', () => {
]);
expect(rule.condition.scope).toBe('#');
});

it('uses anyOf for multiple conditions on the same field, with no flag needed', () => {
const rule = createSectionRule([
{ field: 'yes_or_no', id: 'c1', operator: 'IS_EXACTLY', value: 'yes' },
{ field: 'yes_or_no', id: 'c2', operator: 'IS_EXACTLY', value: 'no' },
]) as any;
expect(rule.condition.schema.anyOf).toHaveLength(2);
});

it('uses allOf for multiple conditions on different fields', () => {
const rule = createSectionRule([
{ field: 'injured_animal', id: 'c1', operator: 'CONTAINS', value: 'elephant' },
{ field: 'requires_attention', id: 'c2', operator: 'IS_NOT_EMPTY' },
]) as any;
expect(rule.condition.schema.allOf).toHaveLength(2);
});
});

// ---------------------------------------------------------------------------
Expand Down
Loading