chore: 版本 0.1.0 更新 toTree 支持嵌套分组与同逻辑合并
Node.js Build / build (push) Successful in 10s

This commit is contained in:
2026-08-07 10:28:46 +08:00
parent e5077aeefa
commit be6bfae4ac
2 changed files with 129 additions and 8 deletions
+62 -8
View File
@@ -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;
}
+67
View File
@@ -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([]);
});
});