@svgrid/enterprise 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ExpressionEditorHarness.svelte +47 -0
- package/dist/ExpressionEditorHarness.svelte.d.ts +15 -0
- package/dist/SvAdvancedFilter.svelte +208 -0
- package/dist/SvAdvancedFilter.svelte.d.ts +37 -0
- package/dist/SvExpressionEditor.svelte +251 -123
- package/dist/SvGridEditPanel.svelte +1157 -1157
- package/dist/advanced-filter/expr-columns-from-grid.d.ts +30 -0
- package/dist/advanced-filter/expr-columns-from-grid.js +44 -0
- package/dist/advanced-filter-enable.d.ts +5 -0
- package/dist/advanced-filter-enable.js +35 -0
- package/dist/cdn/svgrid-enterprise.svelte-external.js +8274 -7706
- package/dist/designer/assets/GridMenus-Bjr19Tz_.js +7 -0
- package/dist/designer/assets/{SvListBox-BVzVfCG3.js → SvListBox-BB8rt4aP.js} +1 -1
- package/dist/designer/assets/{index-CKdwDseq.js → index-xjfKckuH.js} +43 -43
- package/dist/designer/assets/{js-scroller.svelte-tHanKJx0.js → js-scroller.svelte-CGkUUXwV.js} +2 -2
- package/dist/designer/assets/src-BT3LjWJL.css +1 -0
- package/dist/designer/assets/src-DDhAK0WP.js +2893 -0
- package/dist/designer/index.html +5 -5
- package/dist/expressions/builder-tree.d.ts +62 -0
- package/dist/expressions/builder-tree.js +133 -0
- package/dist/expressions/compile.d.ts +14 -0
- package/dist/expressions/compile.js +286 -0
- package/dist/expressions/expression-columns.d.ts +1 -11
- package/dist/expressions/expression-columns.js +40 -11
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/install.js +2 -0
- package/dist/node/studio.js +61 -13
- package/dist/sources/rest.js +25 -0
- package/dist/sources/supabase.js +49 -3
- package/dist/studio/emit-project.js +2125 -2125
- package/dist/studio/ui-components.generated.js +33 -0
- package/dist/sveltekit/in-memory.js +96 -3
- package/dist/sveltekit/query-plan.d.ts +32 -1
- package/dist/sveltekit/query-plan.js +88 -1
- package/dist/sveltekit/sql.d.ts +20 -0
- package/dist/sveltekit/sql.js +26 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/designer/assets/GridMenus-CW-rnHS9.js +0 -7
- package/dist/designer/assets/src-DCKhP5Xy.css +0 -1
- package/dist/designer/assets/src-DLsSdh3J.js +0 -2893
package/dist/designer/index.html
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>SvGrid Studio - Designer</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-xjfKckuH.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-Dd_uD5pT.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="./assets/disclose-version-CIm4S9xS.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="./assets/export-format-DwjuTfrQ.js">
|
|
11
|
-
<link rel="modulepreload" crossorigin href="./assets/js-scroller.svelte-
|
|
11
|
+
<link rel="modulepreload" crossorigin href="./assets/js-scroller.svelte-CGkUUXwV.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="./assets/dismissable-2EZKoens.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="./assets/popover-CUkLsvRX.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="./assets/SvDateTimePicker-DOBYyLeN.js">
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/easing-zaypKrr0.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/chart-DHEgt4lN.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="./assets/SvGridChart-BEDNTdc5.js">
|
|
19
|
-
<link rel="modulepreload" crossorigin href="./assets/src-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="./assets/SvListBox-
|
|
19
|
+
<link rel="modulepreload" crossorigin href="./assets/src-DDhAK0WP.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="./assets/SvListBox-BB8rt4aP.js">
|
|
21
21
|
<link rel="stylesheet" crossorigin href="./assets/dismissable-BF-9TJSd.css">
|
|
22
22
|
<link rel="stylesheet" crossorigin href="./assets/SvDateTimePicker-Cygij-dr.css">
|
|
23
23
|
<link rel="stylesheet" crossorigin href="./assets/SvGridDropdown-Crwb2P3Q.css">
|
|
24
24
|
<link rel="stylesheet" crossorigin href="./assets/SvGridChart-Vu4Nvjiv.css">
|
|
25
|
-
<link rel="stylesheet" crossorigin href="./assets/src-
|
|
25
|
+
<link rel="stylesheet" crossorigin href="./assets/src-BT3LjWJL.css">
|
|
26
26
|
<link rel="stylesheet" crossorigin href="./assets/SvListBox-BMSfFJJT.css">
|
|
27
27
|
<link rel="stylesheet" crossorigin href="./assets/index-BfzoL904.css">
|
|
28
28
|
</head>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builder-tree - the shape the structured filter builder edits, and its
|
|
3
|
+
* conversion to and from `PredicateExpr`.
|
|
4
|
+
*
|
|
5
|
+
* The builder used to be flat: one combinator over a list of comparisons.
|
|
6
|
+
* That covers "region is EMEA AND arr > 50000" but not the query people
|
|
7
|
+
* actually reach for next - "EMEA accounts, OR any account over 300k that is
|
|
8
|
+
* also high risk" - which needs a group nested inside the top-level list. Any
|
|
9
|
+
* such expression fell back to text mode, and the Builder tab was disabled.
|
|
10
|
+
*
|
|
11
|
+
* A tree makes those expressible. Two boundaries are deliberate:
|
|
12
|
+
*
|
|
13
|
+
* - `scalarCmp` (column maths, aggregates) has no place in a
|
|
14
|
+
* column/operator/value row, so it still forces text mode. That is a
|
|
15
|
+
* property of the expression, not a missing feature.
|
|
16
|
+
* - `not` is representable, but only as a group. Negating a single condition
|
|
17
|
+
* is better said with the negative operator the grid already has
|
|
18
|
+
* (`notEquals`, `notContains`), so a `not` around one `cmp` is left to text
|
|
19
|
+
* mode rather than growing a second way to say the same thing.
|
|
20
|
+
*
|
|
21
|
+
* Kept as a pure module so the conversion is unit-testable without mounting a
|
|
22
|
+
* component, and so the round-trip property can be asserted directly.
|
|
23
|
+
*/
|
|
24
|
+
import type { ExcelFilterOperator } from '@svgrid/grid';
|
|
25
|
+
import { type ExprColumn } from './expression-columns';
|
|
26
|
+
import type { PredicateExpr } from './expression-types';
|
|
27
|
+
/** One column/operator/value row. */
|
|
28
|
+
export type BuilderCondition = {
|
|
29
|
+
kind: 'cond';
|
|
30
|
+
column: string;
|
|
31
|
+
op: ExcelFilterOperator;
|
|
32
|
+
/** Single value, and the low end of a range operator. */
|
|
33
|
+
value: string;
|
|
34
|
+
/** High end of a range operator (`between`). */
|
|
35
|
+
valueTo: string;
|
|
36
|
+
/** Values for a set operator (`in`, `notIn`). */
|
|
37
|
+
values: string[];
|
|
38
|
+
};
|
|
39
|
+
/** A combinator over children, which may themselves be groups. */
|
|
40
|
+
export type BuilderGroup = {
|
|
41
|
+
kind: 'group';
|
|
42
|
+
combinator: 'and' | 'or';
|
|
43
|
+
/** Wrap the whole group in `not`. */
|
|
44
|
+
negated: boolean;
|
|
45
|
+
children: BuilderNode[];
|
|
46
|
+
};
|
|
47
|
+
export type BuilderNode = BuilderCondition | BuilderGroup;
|
|
48
|
+
/** How deep the UI allows nesting. */
|
|
49
|
+
export declare const MAX_BUILDER_DEPTH = 4;
|
|
50
|
+
/** A blank condition on the first column, with that column's first operator. */
|
|
51
|
+
export declare function freshCondition(columns: ReadonlyArray<ExprColumn>): BuilderCondition;
|
|
52
|
+
/** A blank group holding one blank condition. */
|
|
53
|
+
export declare function freshGroup(columns: ReadonlyArray<ExprColumn>, combinator?: 'and' | 'or'): BuilderGroup;
|
|
54
|
+
/**
|
|
55
|
+
* The whole expression as a builder tree, or null if it must stay text.
|
|
56
|
+
*
|
|
57
|
+
* The result is always a group, so the UI has a single root to render even
|
|
58
|
+
* when the expression is one bare comparison.
|
|
59
|
+
*/
|
|
60
|
+
export declare function toBuilderTree(expr: PredicateExpr, columns: ReadonlyArray<ExprColumn>): BuilderGroup | null;
|
|
61
|
+
/** The builder tree back to an expression. An empty tree means "no filter". */
|
|
62
|
+
export declare function fromBuilderTree(root: BuilderGroup): PredicateExpr;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { isRangeOperator, isSetOperator, isValueless, operatorsForType, } from './expression-columns';
|
|
2
|
+
/** How deep the UI allows nesting. */
|
|
3
|
+
export const MAX_BUILDER_DEPTH = 4;
|
|
4
|
+
/** A blank condition on the first column, with that column's first operator. */
|
|
5
|
+
export function freshCondition(columns) {
|
|
6
|
+
const first = columns[0];
|
|
7
|
+
const ops = operatorsForType(first?.type);
|
|
8
|
+
return {
|
|
9
|
+
kind: 'cond',
|
|
10
|
+
column: first?.id ?? '',
|
|
11
|
+
op: ops[0]?.value ?? 'equals',
|
|
12
|
+
value: '',
|
|
13
|
+
valueTo: '',
|
|
14
|
+
values: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** A blank group holding one blank condition. */
|
|
18
|
+
export function freshGroup(columns, combinator = 'and') {
|
|
19
|
+
return {
|
|
20
|
+
kind: 'group',
|
|
21
|
+
combinator,
|
|
22
|
+
negated: false,
|
|
23
|
+
children: [freshCondition(columns)],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function conditionFromCmp(p) {
|
|
27
|
+
return {
|
|
28
|
+
kind: 'cond',
|
|
29
|
+
column: p.column,
|
|
30
|
+
op: p.op,
|
|
31
|
+
value: p.value != null && !Array.isArray(p.value) ? String(p.value) : '',
|
|
32
|
+
valueTo: p.valueTo != null ? String(p.valueTo) : '',
|
|
33
|
+
values: Array.isArray(p.value) ? p.value.map(String) : [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One node of the expression to one node of the builder, or null when the
|
|
38
|
+
* expression cannot be shown as a condition row or a group.
|
|
39
|
+
*
|
|
40
|
+
* `depth` guards against an expression nested deeper than the UI can render.
|
|
41
|
+
* Rather than draw a tree the user cannot fully see, such an expression stays
|
|
42
|
+
* in text mode where it is at least intact and editable.
|
|
43
|
+
*/
|
|
44
|
+
function nodeFrom(expr, depth) {
|
|
45
|
+
if (depth > MAX_BUILDER_DEPTH)
|
|
46
|
+
return null;
|
|
47
|
+
switch (expr.kind) {
|
|
48
|
+
case 'cmp':
|
|
49
|
+
return conditionFromCmp(expr);
|
|
50
|
+
case 'and':
|
|
51
|
+
case 'or': {
|
|
52
|
+
const children = [];
|
|
53
|
+
for (const part of expr.parts) {
|
|
54
|
+
const child = nodeFrom(part, depth + 1);
|
|
55
|
+
if (!child)
|
|
56
|
+
return null;
|
|
57
|
+
children.push(child);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
kind: 'group',
|
|
61
|
+
combinator: expr.kind,
|
|
62
|
+
negated: false,
|
|
63
|
+
children,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case 'not': {
|
|
67
|
+
// Only a negated GROUP round-trips. `not` around one comparison is said
|
|
68
|
+
// better by the operator itself, so it stays in text mode.
|
|
69
|
+
const inner = expr.expr;
|
|
70
|
+
if (inner.kind !== 'and' && inner.kind !== 'or')
|
|
71
|
+
return null;
|
|
72
|
+
const group = nodeFrom(inner, depth);
|
|
73
|
+
if (!group || group.kind !== 'group')
|
|
74
|
+
return null;
|
|
75
|
+
return { ...group, negated: true };
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
// const / scalarCmp: no condition-row representation.
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The whole expression as a builder tree, or null if it must stay text.
|
|
84
|
+
*
|
|
85
|
+
* The result is always a group, so the UI has a single root to render even
|
|
86
|
+
* when the expression is one bare comparison.
|
|
87
|
+
*/
|
|
88
|
+
export function toBuilderTree(expr, columns) {
|
|
89
|
+
// `const true` is how "no filter yet" is spelled. Show it as an empty
|
|
90
|
+
// builder rather than an unrepresentable expression, so a fresh panel opens
|
|
91
|
+
// on a ready-to-fill row instead of dropping into text mode showing `true`.
|
|
92
|
+
if (expr.kind === 'const') {
|
|
93
|
+
return expr.value ? freshGroup(columns) : null;
|
|
94
|
+
}
|
|
95
|
+
const node = nodeFrom(expr, 0);
|
|
96
|
+
if (!node)
|
|
97
|
+
return null;
|
|
98
|
+
if (node.kind === 'cond') {
|
|
99
|
+
return { kind: 'group', combinator: 'and', negated: false, children: [node] };
|
|
100
|
+
}
|
|
101
|
+
return node;
|
|
102
|
+
}
|
|
103
|
+
function cmpFromCondition(c) {
|
|
104
|
+
const base = { kind: 'cmp', column: c.column, op: c.op };
|
|
105
|
+
if (isValueless(c.op))
|
|
106
|
+
return base;
|
|
107
|
+
if (isSetOperator(c.op))
|
|
108
|
+
return { ...base, value: c.values };
|
|
109
|
+
if (isRangeOperator(c.op))
|
|
110
|
+
return { ...base, value: c.value, valueTo: c.valueTo };
|
|
111
|
+
return { ...base, value: c.value };
|
|
112
|
+
}
|
|
113
|
+
function nodeTo(node) {
|
|
114
|
+
if (node.kind === 'cond')
|
|
115
|
+
return cmpFromCondition(node);
|
|
116
|
+
const parts = [];
|
|
117
|
+
for (const child of node.children) {
|
|
118
|
+
const part = nodeTo(child);
|
|
119
|
+
// An empty group contributes nothing rather than collapsing the whole
|
|
120
|
+
// expression - a half-built group in the UI must not silently change what
|
|
121
|
+
// the finished filter means.
|
|
122
|
+
if (part)
|
|
123
|
+
parts.push(part);
|
|
124
|
+
}
|
|
125
|
+
if (parts.length === 0)
|
|
126
|
+
return null;
|
|
127
|
+
const inner = parts.length === 1 ? parts[0] : { kind: node.combinator, parts };
|
|
128
|
+
return node.negated ? { kind: 'not', expr: inner } : inner;
|
|
129
|
+
}
|
|
130
|
+
/** The builder tree back to an expression. An empty tree means "no filter". */
|
|
131
|
+
export function fromBuilderTree(root) {
|
|
132
|
+
return nodeTo(root) ?? { kind: 'const', value: true };
|
|
133
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PredicateExpr, ScalarExpr } from './expression-types';
|
|
2
|
+
type Scalar = number | string | boolean | null;
|
|
3
|
+
export type CompileOptions<TRow> = {
|
|
4
|
+
getValue: (row: TRow, columnId: string) => unknown;
|
|
5
|
+
locale?: string | ReadonlyArray<string>;
|
|
6
|
+
/** Rows the aggregates fold over. */
|
|
7
|
+
rows: ReadonlyArray<TRow>;
|
|
8
|
+
functions?: Record<string, (args: Scalar[]) => Scalar>;
|
|
9
|
+
};
|
|
10
|
+
/** Compile a predicate. Returns null when the expression is unusable. */
|
|
11
|
+
export declare function compilePredicate<TRow>(expr: PredicateExpr, opts: CompileOptions<TRow>): ((row: TRow) => boolean) | null;
|
|
12
|
+
/** Compile a scalar expression on its own (aggregates folded over `opts.rows`). */
|
|
13
|
+
export declare function compileScalar<TRow>(expr: ScalarExpr, opts: CompileOptions<TRow>): (row: TRow) => Scalar;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a predicate expression into a row predicate.
|
|
3
|
+
*
|
|
4
|
+
* `evaluate.ts` walks the AST for every row. That is fine for alert rules,
|
|
5
|
+
* which run over a handful of changed rows, and wrong for a grid filter, which
|
|
6
|
+
* runs over the whole dataset on every keystroke. This module does the walk
|
|
7
|
+
* once and returns a closure tree.
|
|
8
|
+
*
|
|
9
|
+
* Two properties are load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Identical semantics.** Leaf comparisons go through the grid's own
|
|
12
|
+
* `compileExcelFilter`, the same code path the column-menu filter uses, so
|
|
13
|
+
* an advanced filter and a column filter agree by construction.
|
|
14
|
+
* `compile.test.ts` asserts the compiled result matches `evaluatePredicate`
|
|
15
|
+
* across a corpus, so the compiler's correctness is a consequence of the
|
|
16
|
+
* evaluator's rather than a parallel claim.
|
|
17
|
+
*
|
|
18
|
+
* 2. **Aggregates fold once.** `evaluateScalar`'s `agg` case rescans every row
|
|
19
|
+
* for every row, so `SUM(amount) > 1000` is O(N^2). Here the distinct
|
|
20
|
+
* aggregates are computed in a single pass up front and compiled to
|
|
21
|
+
* constants.
|
|
22
|
+
*/
|
|
23
|
+
import { compileExcelFilter } from '@svgrid/grid/filtering';
|
|
24
|
+
const num = (v) => typeof v === 'number' ? v : v == null || v === '' ? NaN : Number(v);
|
|
25
|
+
const BUILTINS = {
|
|
26
|
+
ABS: (a) => Math.abs(num(a[0])),
|
|
27
|
+
ROUND: (a) => Math.round(num(a[0])),
|
|
28
|
+
FLOOR: (a) => Math.floor(num(a[0])),
|
|
29
|
+
CEIL: (a) => Math.ceil(num(a[0])),
|
|
30
|
+
MIN: (a) => Math.min(...a.map(num)),
|
|
31
|
+
MAX: (a) => Math.max(...a.map(num)),
|
|
32
|
+
IF: (a) => (a[0] ? (a[1] ?? null) : (a[2] ?? null)),
|
|
33
|
+
COALESCE: (a) => a.find((v) => v != null) ?? null,
|
|
34
|
+
CONCAT: (a) => a.map((v) => (v == null ? '' : String(v))).join(''),
|
|
35
|
+
LOWER: (a) => String(a[0] ?? '').toLowerCase(),
|
|
36
|
+
UPPER: (a) => String(a[0] ?? '').toUpperCase(),
|
|
37
|
+
LEN: (a) => String(a[0] ?? '').length,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Fold every `agg` node in the tree to a constant, in ONE pass over the rows.
|
|
41
|
+
* Returns a key -> value map; the key is `fn:column`.
|
|
42
|
+
*/
|
|
43
|
+
function foldAggregates(expr, opts) {
|
|
44
|
+
const wanted = new Set();
|
|
45
|
+
const columns = new Set();
|
|
46
|
+
const scanScalar = (s) => {
|
|
47
|
+
switch (s.kind) {
|
|
48
|
+
case 'agg':
|
|
49
|
+
wanted.add(`${s.fn}:${s.column}`);
|
|
50
|
+
if (s.fn !== 'count')
|
|
51
|
+
columns.add(s.column);
|
|
52
|
+
break;
|
|
53
|
+
case 'neg':
|
|
54
|
+
scanScalar(s.expr);
|
|
55
|
+
break;
|
|
56
|
+
case 'bin':
|
|
57
|
+
scanScalar(s.left);
|
|
58
|
+
scanScalar(s.right);
|
|
59
|
+
break;
|
|
60
|
+
case 'func':
|
|
61
|
+
s.args.forEach(scanScalar);
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const scan = (p) => {
|
|
68
|
+
switch (p.kind) {
|
|
69
|
+
case 'and':
|
|
70
|
+
case 'or':
|
|
71
|
+
p.parts.forEach(scan);
|
|
72
|
+
break;
|
|
73
|
+
case 'not':
|
|
74
|
+
scan(p.expr);
|
|
75
|
+
break;
|
|
76
|
+
case 'scalarCmp':
|
|
77
|
+
scanScalar(p.left);
|
|
78
|
+
scanScalar(p.right);
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
scan(expr);
|
|
85
|
+
const out = new Map();
|
|
86
|
+
if (wanted.size === 0)
|
|
87
|
+
return out; // overwhelmingly the common case
|
|
88
|
+
// One pass, accumulating per column.
|
|
89
|
+
const acc = new Map();
|
|
90
|
+
for (const c of columns)
|
|
91
|
+
acc.set(c, { sum: 0, n: 0, min: Infinity, max: -Infinity });
|
|
92
|
+
for (const row of opts.rows) {
|
|
93
|
+
for (const c of columns) {
|
|
94
|
+
const n = num(opts.getValue(row, c));
|
|
95
|
+
if (!Number.isFinite(n))
|
|
96
|
+
continue;
|
|
97
|
+
const a = acc.get(c);
|
|
98
|
+
a.sum += n;
|
|
99
|
+
a.n += 1;
|
|
100
|
+
if (n < a.min)
|
|
101
|
+
a.min = n;
|
|
102
|
+
if (n > a.max)
|
|
103
|
+
a.max = n;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const key of wanted) {
|
|
107
|
+
const [fn, column] = key.split(/:(.*)/s);
|
|
108
|
+
if (fn === 'count') {
|
|
109
|
+
out.set(key, opts.rows.length);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const a = acc.get(column);
|
|
113
|
+
// Matches evaluate.ts: an empty numeric set is 0 for sum, null otherwise.
|
|
114
|
+
if (!a || a.n === 0) {
|
|
115
|
+
out.set(key, fn === 'sum' ? 0 : null);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
switch (fn) {
|
|
119
|
+
case 'sum':
|
|
120
|
+
out.set(key, a.sum);
|
|
121
|
+
break;
|
|
122
|
+
case 'avg':
|
|
123
|
+
out.set(key, a.sum / a.n);
|
|
124
|
+
break;
|
|
125
|
+
case 'min':
|
|
126
|
+
out.set(key, a.min);
|
|
127
|
+
break;
|
|
128
|
+
case 'max':
|
|
129
|
+
out.set(key, a.max);
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
out.set(key, null);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
function compileScalarInternal(expr, opts, aggs) {
|
|
138
|
+
switch (expr.kind) {
|
|
139
|
+
case 'lit': {
|
|
140
|
+
const v = expr.value;
|
|
141
|
+
return () => v;
|
|
142
|
+
}
|
|
143
|
+
case 'col': {
|
|
144
|
+
const id = expr.id;
|
|
145
|
+
return (row) => {
|
|
146
|
+
const raw = opts.getValue(row, id);
|
|
147
|
+
return (raw ?? null);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
case 'neg': {
|
|
151
|
+
const inner = compileScalarInternal(expr.expr, opts, aggs);
|
|
152
|
+
return (row) => -num(inner(row));
|
|
153
|
+
}
|
|
154
|
+
case 'bin': {
|
|
155
|
+
const l = compileScalarInternal(expr.left, opts, aggs);
|
|
156
|
+
const r = compileScalarInternal(expr.right, opts, aggs);
|
|
157
|
+
const op = expr.op;
|
|
158
|
+
return (row) => {
|
|
159
|
+
const a = num(l(row));
|
|
160
|
+
const b = num(r(row));
|
|
161
|
+
switch (op) {
|
|
162
|
+
case '+':
|
|
163
|
+
return a + b;
|
|
164
|
+
case '-':
|
|
165
|
+
return a - b;
|
|
166
|
+
case '*':
|
|
167
|
+
return a * b;
|
|
168
|
+
case '/':
|
|
169
|
+
return b === 0 ? NaN : a / b;
|
|
170
|
+
case '%':
|
|
171
|
+
return b === 0 ? NaN : a % b;
|
|
172
|
+
default:
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
case 'agg': {
|
|
178
|
+
// Already folded to a constant.
|
|
179
|
+
const v = aggs.get(`${expr.fn}:${expr.column}`) ?? null;
|
|
180
|
+
return () => v;
|
|
181
|
+
}
|
|
182
|
+
case 'func': {
|
|
183
|
+
const name = expr.name.toUpperCase();
|
|
184
|
+
const args = expr.args.map((a) => compileScalarInternal(a, opts, aggs));
|
|
185
|
+
const fn = opts.functions?.[name] ?? BUILTINS[name];
|
|
186
|
+
if (!fn)
|
|
187
|
+
return () => null;
|
|
188
|
+
return (row) => fn(args.map((a) => a(row)));
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
return () => null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function compareScalars(a, op, b) {
|
|
195
|
+
// Numeric when both sides look numeric, else string - mirrors evaluate.ts.
|
|
196
|
+
const na = num(a);
|
|
197
|
+
const nb = num(b);
|
|
198
|
+
const numeric = Number.isFinite(na) && Number.isFinite(nb);
|
|
199
|
+
const x = numeric ? na : String(a ?? '');
|
|
200
|
+
const y = numeric ? nb : String(b ?? '');
|
|
201
|
+
switch (op) {
|
|
202
|
+
case '=':
|
|
203
|
+
return x === y;
|
|
204
|
+
case '!=':
|
|
205
|
+
return x !== y;
|
|
206
|
+
case '>':
|
|
207
|
+
return x > y;
|
|
208
|
+
case '<':
|
|
209
|
+
return x < y;
|
|
210
|
+
case '>=':
|
|
211
|
+
return x >= y;
|
|
212
|
+
case '<=':
|
|
213
|
+
return x <= y;
|
|
214
|
+
default:
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function compileInternal(expr, opts, aggs) {
|
|
219
|
+
switch (expr.kind) {
|
|
220
|
+
case 'const': {
|
|
221
|
+
const v = expr.value === true;
|
|
222
|
+
return () => v;
|
|
223
|
+
}
|
|
224
|
+
case 'and': {
|
|
225
|
+
const parts = expr.parts.map((p) => compileInternal(p, opts, aggs));
|
|
226
|
+
if (parts.length === 0)
|
|
227
|
+
return () => true;
|
|
228
|
+
return (row) => {
|
|
229
|
+
for (const p of parts)
|
|
230
|
+
if (!p(row))
|
|
231
|
+
return false;
|
|
232
|
+
return true;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
case 'or': {
|
|
236
|
+
const parts = expr.parts.map((p) => compileInternal(p, opts, aggs));
|
|
237
|
+
if (parts.length === 0)
|
|
238
|
+
return () => false;
|
|
239
|
+
return (row) => {
|
|
240
|
+
for (const p of parts)
|
|
241
|
+
if (p(row))
|
|
242
|
+
return true;
|
|
243
|
+
return false;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
case 'not': {
|
|
247
|
+
const inner = compileInternal(expr.expr, opts, aggs);
|
|
248
|
+
return (row) => !inner(row);
|
|
249
|
+
}
|
|
250
|
+
case 'cmp': {
|
|
251
|
+
// The needle-side work happens once, here.
|
|
252
|
+
const test = compileExcelFilter({
|
|
253
|
+
id: expr.column,
|
|
254
|
+
operator: expr.op,
|
|
255
|
+
value: expr.value,
|
|
256
|
+
valueTo: expr.valueTo,
|
|
257
|
+
}, { locale: opts.locale });
|
|
258
|
+
const column = expr.column;
|
|
259
|
+
return (row) => test(opts.getValue(row, column));
|
|
260
|
+
}
|
|
261
|
+
case 'scalarCmp': {
|
|
262
|
+
const l = compileScalarInternal(expr.left, opts, aggs);
|
|
263
|
+
const r = compileScalarInternal(expr.right, opts, aggs);
|
|
264
|
+
const op = expr.op;
|
|
265
|
+
return (row) => compareScalars(l(row), op, r(row));
|
|
266
|
+
}
|
|
267
|
+
default:
|
|
268
|
+
// Unknown node kind (hand-edited JSON): match nothing rather than throw.
|
|
269
|
+
return () => false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Compile a predicate. Returns null when the expression is unusable. */
|
|
273
|
+
export function compilePredicate(expr, opts) {
|
|
274
|
+
try {
|
|
275
|
+
const aggs = foldAggregates(expr, opts);
|
|
276
|
+
return compileInternal(expr, opts, aggs);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Compile a scalar expression on its own (aggregates folded over `opts.rows`). */
|
|
283
|
+
export function compileScalar(expr, opts) {
|
|
284
|
+
const aggs = foldAggregates({ kind: 'scalarCmp', left: expr, op: '=', right: expr }, opts);
|
|
285
|
+
return compileScalarInternal(expr, opts, aggs);
|
|
286
|
+
}
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Column metadata for the expression layer - the small, self-contained operator
|
|
3
|
-
* catalogue the editor draws from, plus name <-> id resolution for the
|
|
4
|
-
* `[Bracketed Name]` reference syntax.
|
|
5
|
-
*
|
|
6
|
-
* The operator set mirrors the grid's own filter row (text / number / date),
|
|
7
|
-
* so an expression's operators line up with what a user already knows from the
|
|
8
|
-
* column menu. We keep a local copy rather than importing the grid's internal
|
|
9
|
-
* `filter-operators` module (which is not part of the public surface).
|
|
10
|
-
*/
|
|
11
1
|
import type { ExcelFilterOperator } from '@svgrid/grid';
|
|
12
2
|
/** Coarse value type used to decide which operators a column offers. */
|
|
13
3
|
export type ExprColumnType = 'text' | 'number' | 'date' | 'datetime' | 'boolean';
|
|
@@ -37,7 +27,7 @@ export declare const OPERATORS: ReadonlyArray<OperatorMeta>;
|
|
|
37
27
|
export declare function operatorMeta(op: ExcelFilterOperator): OperatorMeta;
|
|
38
28
|
/** Operators valid for a column type (defaults to the text set). */
|
|
39
29
|
export declare function operatorsForType(type: ExprColumnType | undefined): OperatorMeta[];
|
|
40
|
-
/** Whether an operator needs no value input. */
|
|
30
|
+
/** Whether an operator needs no value input. Delegates to the grid catalogue. */
|
|
41
31
|
export declare function isValueless(op: ExcelFilterOperator): boolean;
|
|
42
32
|
/** Whether an operator takes a multi-value token list. */
|
|
43
33
|
export declare function isSetOperator(op: ExcelFilterOperator): boolean;
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column metadata for the expression layer - the small, self-contained operator
|
|
3
|
+
* catalogue the editor draws from, plus name <-> id resolution for the
|
|
4
|
+
* `[Bracketed Name]` reference syntax.
|
|
5
|
+
*
|
|
6
|
+
* The operator set mirrors the grid's own filter row (text / number / date),
|
|
7
|
+
* so an expression's operators line up with what a user already knows from the
|
|
8
|
+
* column menu.
|
|
9
|
+
*
|
|
10
|
+
* Identity and input-shape semantics come from the grid's shared catalogue, so
|
|
11
|
+
* the two surfaces cannot disagree about which operators exist or which need a
|
|
12
|
+
* chip input / no input / a second value. Labels and ordering stay local on
|
|
13
|
+
* purpose: the editor leads with `Equals` where the filter menu leads with
|
|
14
|
+
* `Contains`, and each is right for its context.
|
|
15
|
+
*/
|
|
16
|
+
// Imported from the `/filtering` subpath, not the package root: the root would
|
|
17
|
+
// drag SvGrid.svelte into a module that is otherwise pure logic.
|
|
18
|
+
import { isRangeOperator as gridIsRangeOperator, isSetOperator as gridIsSetOperator, isValuelessOperator as gridIsValuelessOperator, } from '@svgrid/grid/filtering';
|
|
1
19
|
const TEXT = ['text'];
|
|
2
20
|
const NUM = ['number'];
|
|
3
21
|
const DATES = ['date', 'datetime'];
|
|
4
22
|
const ALL = ['text', 'number', 'date', 'datetime', 'boolean'];
|
|
5
|
-
/**
|
|
6
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Label + type applicability are local (presentation); the `valueless` / `set` /
|
|
25
|
+
* `range` flags are stamped on from the grid's shared catalogue below, so the
|
|
26
|
+
* editor can never render the wrong input control for an operator.
|
|
27
|
+
*/
|
|
28
|
+
const OPERATOR_PRESENTATION = [
|
|
7
29
|
{ value: 'equals', label: 'Equals', types: ALL },
|
|
8
30
|
{ value: 'notEquals', label: 'Not equals', types: ALL },
|
|
9
31
|
{ value: 'contains', label: 'Contains', types: TEXT },
|
|
@@ -13,12 +35,19 @@ export const OPERATORS = [
|
|
|
13
35
|
{ value: 'regex', label: 'Matches regex', types: TEXT },
|
|
14
36
|
{ value: 'greaterThan', label: 'Greater than', types: [...NUM, ...DATES] },
|
|
15
37
|
{ value: 'lessThan', label: 'Less than', types: [...NUM, ...DATES] },
|
|
16
|
-
{ value: 'between', label: 'Between', types: [...NUM, ...DATES]
|
|
17
|
-
{ value: 'in', label: 'In', types: ['text', 'number']
|
|
18
|
-
{ value: 'notIn', label: 'Not in', types: ['text', 'number']
|
|
19
|
-
{ value: 'isBlank', label: 'Is blank', types: ALL
|
|
20
|
-
{ value: 'isNotBlank', label: 'Is not blank', types: ALL
|
|
38
|
+
{ value: 'between', label: 'Between', types: [...NUM, ...DATES] },
|
|
39
|
+
{ value: 'in', label: 'In', types: ['text', 'number'] },
|
|
40
|
+
{ value: 'notIn', label: 'Not in', types: ['text', 'number'] },
|
|
41
|
+
{ value: 'isBlank', label: 'Is blank', types: ALL },
|
|
42
|
+
{ value: 'isNotBlank', label: 'Is not blank', types: ALL },
|
|
21
43
|
];
|
|
44
|
+
/** The full operator catalogue, ordered as the editor should present it. */
|
|
45
|
+
export const OPERATORS = OPERATOR_PRESENTATION.map((op) => ({
|
|
46
|
+
...op,
|
|
47
|
+
...(gridIsValuelessOperator(op.value) ? { valueless: true } : {}),
|
|
48
|
+
...(gridIsSetOperator(op.value) ? { set: true } : {}),
|
|
49
|
+
...(gridIsRangeOperator(op.value) ? { range: true } : {}),
|
|
50
|
+
}));
|
|
22
51
|
const BY_VALUE = new Map(OPERATORS.map((op) => [op.value, op]));
|
|
23
52
|
/** Metadata for one operator (falls back to a plain `equals`). */
|
|
24
53
|
export function operatorMeta(op) {
|
|
@@ -29,17 +58,17 @@ export function operatorsForType(type) {
|
|
|
29
58
|
const t = type ?? 'text';
|
|
30
59
|
return OPERATORS.filter((op) => op.types.includes(t));
|
|
31
60
|
}
|
|
32
|
-
/** Whether an operator needs no value input. */
|
|
61
|
+
/** Whether an operator needs no value input. Delegates to the grid catalogue. */
|
|
33
62
|
export function isValueless(op) {
|
|
34
|
-
return
|
|
63
|
+
return gridIsValuelessOperator(op);
|
|
35
64
|
}
|
|
36
65
|
/** Whether an operator takes a multi-value token list. */
|
|
37
66
|
export function isSetOperator(op) {
|
|
38
|
-
return
|
|
67
|
+
return gridIsSetOperator(op);
|
|
39
68
|
}
|
|
40
69
|
/** Whether an operator needs a second (`valueTo`) value. */
|
|
41
70
|
export function isRangeOperator(op) {
|
|
42
|
-
return
|
|
71
|
+
return gridIsRangeOperator(op);
|
|
43
72
|
}
|
|
44
73
|
/** Look a column up by its canonical id. */
|
|
45
74
|
export function columnById(columns, id) {
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export { installEnterprise, type EnterpriseGridApi, type EnterpriseAIApi, type E
|
|
|
10
10
|
export { enableSchedulerView, SvGridScheduler } from './scheduler';
|
|
11
11
|
export { enableBoardView, SvGridBoard } from './board';
|
|
12
12
|
export { enablePivot } from './pivot-enable';
|
|
13
|
+
export { enableAdvancedFilter } from './advanced-filter-enable';
|
|
14
|
+
export { default as SvAdvancedFilter } from './SvAdvancedFilter.svelte';
|
|
15
|
+
export { exprColumnsFromGrid, type ExprColumnsOptions, } from './advanced-filter/expr-columns-from-grid';
|
|
16
|
+
export { compilePredicate, compileScalar, type CompileOptions, } from './expressions/compile';
|
|
13
17
|
export type { SchedulerProConfig } from './scheduler-config';
|
|
14
18
|
export { buildDependencyGraph, topoOrder, hasCycle, requiredStart, cascade, violations, type DependencyType, type SchedulerDependency, type EventTimes, type DependencyGraph, type CascadeOptions, } from './scheduler-dependencies';
|
|
15
19
|
export { expandAssignments, resourceLoad, overallocations, assignedMinutes, type SchedulerAssignment, type Allocation, type Overallocation, } from './scheduler-assignments';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,10 @@ export { enableBoardView, SvGridBoard } from './board';
|
|
|
20
20
|
// In-grid pivot mode (Pro). `enablePivot()` registers the pivot engine so
|
|
21
21
|
// `<SvGrid pivot={...}>` renders a pivot table in place of the flat table.
|
|
22
22
|
export { enablePivot } from './pivot-enable';
|
|
23
|
+
export { enableAdvancedFilter } from './advanced-filter-enable';
|
|
24
|
+
export { default as SvAdvancedFilter } from './SvAdvancedFilter.svelte';
|
|
25
|
+
export { exprColumnsFromGrid, } from './advanced-filter/expr-columns-from-grid';
|
|
26
|
+
export { compilePredicate, compileScalar, } from './expressions/compile';
|
|
23
27
|
export { buildDependencyGraph, topoOrder, hasCycle, requiredStart, cascade, violations, } from './scheduler-dependencies';
|
|
24
28
|
export { expandAssignments, resourceLoad, overallocations, assignedMinutes, } from './scheduler-assignments';
|
|
25
29
|
export { columnSummaries, sumReducer, countReducer, busyMinutesReducer, } from './scheduler-summary';
|
package/dist/install.js
CHANGED
|
@@ -9,6 +9,7 @@ import { aiFilter, aiSmartFill, aiSummarize, aiClassify, aiExport, aiFindAnomali
|
|
|
9
9
|
import { createPivotModel, } from './pivot';
|
|
10
10
|
import { enableSchedulerView } from './scheduler';
|
|
11
11
|
import { enableBoardView } from './board';
|
|
12
|
+
import { enableAdvancedFilter } from './advanced-filter-enable';
|
|
12
13
|
import { enablePivot } from './pivot-enable';
|
|
13
14
|
/**
|
|
14
15
|
* Augment a SvGridApi instance with Pro methods (`exportData`, `print`,
|
|
@@ -45,6 +46,7 @@ export function installEnterprise(api) {
|
|
|
45
46
|
// enterprise formats (xlsx / pdf / ...); free grids without it get the plan only.
|
|
46
47
|
registerExportProvider(exportGrid);
|
|
47
48
|
enablePivot();
|
|
49
|
+
enableAdvancedFilter();
|
|
48
50
|
pro.pivot = {
|
|
49
51
|
build: (config) => createPivotModel(pro.getData(), config),
|
|
50
52
|
buildFrom: (data, config) =>
|