This commit is contained in:
+62
-8
@@ -1,15 +1,69 @@
|
||||
/**
|
||||
* Normalize an item: unwrap explicit nested group shapes.
|
||||
*
|
||||
* Supported input shapes:
|
||||
* - plain leaf: { field, query, op, value, ... }
|
||||
* - inline children: { logic, children: [...] }
|
||||
* - group key: { group: [...], logic? }
|
||||
*
|
||||
* Nested children are recursively built into their own tree so inner
|
||||
* groups honor Q6 (nested and/or groups).
|
||||
*
|
||||
* @param {*} item
|
||||
* @returns {object}
|
||||
*/
|
||||
function normalize(item) {
|
||||
if (Array.isArray(item?.children)) {
|
||||
return { ...item, children: toTree(item.children) };
|
||||
}
|
||||
if (Array.isArray(item?.group)) {
|
||||
const { group, ...rest } = item;
|
||||
return { ...rest, children: toTree(group) };
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a nested condition tree from a flat filter list.
|
||||
*
|
||||
* Semantics (aligned with component output, Q7/Q54):
|
||||
* - the first item carries no `logic`; each following item's `logic`
|
||||
* expresses how it joins the previous accumulated node
|
||||
* - adjacent items sharing the same `logic` are merged into one
|
||||
* `{ logic, children: [...] }` node (Q6: nested groups)
|
||||
* - mixed logic is left-associative: A AND B OR C -> (A AND B) OR C
|
||||
*
|
||||
* @param {Array<object>} flat
|
||||
* @param {{ mergeSameLogic?: boolean }} [options]
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function toTree(flat) {
|
||||
if (!flat.length) return [];
|
||||
if (flat.length === 1) return [flat[0]];
|
||||
export function toTree(flat, options = {}) {
|
||||
const { mergeSameLogic = true } = options;
|
||||
if (!Array.isArray(flat) || flat.length === 0) return [];
|
||||
|
||||
let acc = flat[0];
|
||||
for (let i = 1; i < flat.length; i++) {
|
||||
const logic = flat[i].logic || 'AND';
|
||||
acc = { logic, children: [acc, flat[i]] };
|
||||
const items = flat.map(normalize);
|
||||
if (items.length === 1) return items;
|
||||
|
||||
const tree = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (tree.length === 0) {
|
||||
tree.push(item);
|
||||
continue;
|
||||
}
|
||||
return [acc];
|
||||
|
||||
const logic = (item.logic || 'AND').toUpperCase();
|
||||
const prev = tree[tree.length - 1];
|
||||
|
||||
if (mergeSameLogic && prev.logic === logic && Array.isArray(prev.children)) {
|
||||
prev.children.push(item);
|
||||
} else if (mergeSameLogic && prev.logic === logic) {
|
||||
tree[tree.length - 1] = { logic, children: [prev, item] };
|
||||
} else {
|
||||
const joined = { logic, children: [tree.pop(), item] };
|
||||
tree.push(joined);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,73 @@ describe('toTree', () => {
|
||||
logic: 'OR',
|
||||
children: expect.any(Array),
|
||||
});
|
||||
expect(tree[0].children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('merges adjacent same-logic leaves into one node', () => {
|
||||
const flat = [
|
||||
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
||||
{ field: 'b', query: 'EQ', op: '=', value: '2', logic: 'AND' },
|
||||
{ field: 'c', query: 'EQ', op: '=', value: '3', logic: 'AND' },
|
||||
];
|
||||
const tree = toTree(flat);
|
||||
expect(tree).toHaveLength(1);
|
||||
expect(tree[0].logic).toBe('AND');
|
||||
expect(tree[0].children).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('left-associates mixed logic: A AND B OR C -> (A AND B) OR C', () => {
|
||||
const flat = [
|
||||
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
||||
{ field: 'b', query: 'EQ', op: '=', value: '2', logic: 'AND' },
|
||||
{ field: 'c', query: 'EQ', op: '=', value: '3', logic: 'OR' },
|
||||
];
|
||||
const tree = toTree(flat);
|
||||
expect(tree).toHaveLength(1);
|
||||
expect(tree[0].logic).toBe('OR');
|
||||
expect(tree[0].children).toHaveLength(2);
|
||||
expect(tree[0].children[0].logic).toBe('AND');
|
||||
expect(tree[0].children[0].children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('supports explicit nested group via children key', () => {
|
||||
const flat = [
|
||||
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
||||
{
|
||||
logic: 'OR',
|
||||
children: [
|
||||
{ field: 'b', query: 'EQ', op: '=', value: '2' },
|
||||
{ field: 'c', query: 'EQ', op: '=', value: '3', logic: 'AND' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const tree = toTree(flat);
|
||||
// a OR (b AND c)
|
||||
expect(tree[0].logic).toBe('OR');
|
||||
expect(tree[0].children).toHaveLength(2);
|
||||
const inner = tree[0].children[1].children;
|
||||
expect(inner).toHaveLength(1);
|
||||
expect(inner[0].logic).toBe('AND');
|
||||
expect(inner[0].children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('supports group key shorthand', () => {
|
||||
const flat = [
|
||||
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
||||
{
|
||||
logic: 'OR',
|
||||
group: [{ field: 'b', query: 'EQ', op: '=', value: '2' }],
|
||||
},
|
||||
];
|
||||
const tree = toTree(flat);
|
||||
expect(tree[0].logic).toBe('OR');
|
||||
expect(tree[0].children).toHaveLength(2);
|
||||
expect(tree[0].children[1].children).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns empty array for empty input', () => {
|
||||
expect(toTree([])).toEqual([]);
|
||||
expect(toTree(null)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user