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 {Array<object>} flat
|
||||||
|
* @param {{ mergeSameLogic?: boolean }} [options]
|
||||||
* @returns {Array<object>}
|
* @returns {Array<object>}
|
||||||
*/
|
*/
|
||||||
export function toTree(flat) {
|
export function toTree(flat, options = {}) {
|
||||||
if (!flat.length) return [];
|
const { mergeSameLogic = true } = options;
|
||||||
if (flat.length === 1) return [flat[0]];
|
if (!Array.isArray(flat) || flat.length === 0) return [];
|
||||||
|
|
||||||
let acc = flat[0];
|
const items = flat.map(normalize);
|
||||||
for (let i = 1; i < flat.length; i++) {
|
if (items.length === 1) return items;
|
||||||
const logic = flat[i].logic || 'AND';
|
|
||||||
acc = { logic, children: [acc, flat[i]] };
|
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',
|
logic: 'OR',
|
||||||
children: expect.any(Array),
|
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