@olenbetong/appframe-core 2.15.2 → 2.16.0
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/esm/af.common.js +147 -86
- package/dist/esm/af.common.legacy.js +143 -78
- package/dist/esm/af.common.legacy.min.js +3 -3
- package/dist/esm/af.common.legacy.min.js.map +1 -1
- package/dist/esm/af.common.min.js +3 -3
- package/dist/esm/af.common.min.js.map +1 -1
- package/dist/iife/af.common.js +151 -85
- package/dist/iife/af.common.legacy.js +147 -77
- package/dist/iife/af.common.legacy.min.js +3 -3
- package/dist/iife/af.common.legacy.min.js.map +1 -1
- package/dist/iife/af.common.min.js +3 -3
- package/dist/iife/af.common.min.js.map +1 -1
- package/es/filter/columnValues.test.d.ts +1 -0
- package/es/filter/columnValues.test.js +119 -0
- package/es/filter/describeFilter.js +11 -9
- package/es/filter/index.d.ts +3 -3
- package/es/filter/index.js +2 -2
- package/es/filter/operators.d.ts +14 -0
- package/es/filter/operators.js +63 -3
- package/es/filter/tree.d.ts +10 -1
- package/es/filter/tree.js +43 -5
- package/es/filter/types.d.ts +27 -6
- package/package.json +6 -4
- package/src/filter/columnValues.test.ts +164 -0
- package/src/filter/describeFilter.ts +16 -10
- package/src/filter/index.ts +6 -0
- package/src/filter/operators.ts +74 -3
- package/src/filter/tree.ts +64 -7
- package/src/filter/types.ts +43 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { describeFilter } from "./describeFilter.js";
|
|
3
|
+
import { getColumnsForField, getDynamicValuesForField, getOperatorArity, getOperatorsForField, supportsColumnValue, } from "./operators.js";
|
|
4
|
+
import { changeOperator, isCompleteExpression, needsFilterString } from "./tree.js";
|
|
5
|
+
const fields = [
|
|
6
|
+
{ name: "Date", caption: "Dato", type: "date" },
|
|
7
|
+
{ name: "DeliveryDate", caption: "Leveringsdato", type: "datetime" },
|
|
8
|
+
{ name: "Amount", caption: "Beløp", type: "number" },
|
|
9
|
+
{ name: "Quantity", type: "number" },
|
|
10
|
+
{ name: "Hidden", type: "number", excludeFromFilter: true },
|
|
11
|
+
{ name: "Name", type: "string" },
|
|
12
|
+
{ name: "Active", type: "boolean" },
|
|
13
|
+
{ name: "Archived", type: "boolean" },
|
|
14
|
+
];
|
|
15
|
+
function expression(partial) {
|
|
16
|
+
return {
|
|
17
|
+
type: "expression",
|
|
18
|
+
column: "DeliveryDate",
|
|
19
|
+
operator: "greaterthan",
|
|
20
|
+
value: "",
|
|
21
|
+
valueType: "datetime",
|
|
22
|
+
...partial,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function group(...items) {
|
|
26
|
+
return { type: "group", mode: "and", items };
|
|
27
|
+
}
|
|
28
|
+
describe("getColumnsForField", () => {
|
|
29
|
+
test("offers the other fields of the same type, dates and datetimes together", () => {
|
|
30
|
+
expect(getColumnsForField(fields[1], fields).map((field) => field.name)).toEqual(["Date"]);
|
|
31
|
+
expect(getColumnsForField(fields[2], fields).map((field) => field.name)).toEqual(["Quantity"]);
|
|
32
|
+
});
|
|
33
|
+
test("offers nothing for booleans", () => {
|
|
34
|
+
expect(getColumnsForField(fields[6], fields)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("supportsColumnValue", () => {
|
|
38
|
+
test("allows comparisons and ranges, not text checks or lists", () => {
|
|
39
|
+
expect(supportsColumnValue("greaterthan")).toBe(true);
|
|
40
|
+
expect(supportsColumnValue("between")).toBe(true);
|
|
41
|
+
expect(supportsColumnValue("contains")).toBe(false);
|
|
42
|
+
expect(supportsColumnValue("inlist")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("getDynamicValuesForField", () => {
|
|
46
|
+
test("includes the dates only the Windows editor had", () => {
|
|
47
|
+
expect(getDynamicValuesForField({ type: "date" })).toEqual(expect.arrayContaining(["Now", "DayAfterTomorrow", "TwoWeeksAway"]));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("needsFilterString", () => {
|
|
51
|
+
test("is false for values the framework reads", () => {
|
|
52
|
+
expect(needsFilterString(group(expression({ value: "Today", valueType: "special" })))).toBe(false);
|
|
53
|
+
expect(needsFilterString(group(expression({ operator: "between", value: ["2026-01-01", "2026-02-01"] })))).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
test("is true for a column value", () => {
|
|
56
|
+
expect(needsFilterString(group(expression({ value: "Date", valueType: "column" })))).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
test("is true for a date only the Windows editor had", () => {
|
|
59
|
+
expect(needsFilterString(group(expression({ value: "TwoWeeksAway", valueType: "special" })))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
test("is true for a range with an end that is not a plain value", () => {
|
|
62
|
+
let range = expression({
|
|
63
|
+
operator: "between",
|
|
64
|
+
value: [{ valueType: "special", value: "Tomorrow" }, "2026-02-01"],
|
|
65
|
+
});
|
|
66
|
+
expect(needsFilterString({ type: "group", mode: "or", items: [group(range)] })).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("isCompleteExpression", () => {
|
|
70
|
+
test("needs a value at both ends of a range", () => {
|
|
71
|
+
let complete = expression({ operator: "between", value: [{ valueType: "column", value: "Date" }, "2026-02-01"] });
|
|
72
|
+
let missing = expression({ operator: "between", value: [{ valueType: "column", value: "" }, "2026-02-01"] });
|
|
73
|
+
expect(isCompleteExpression(complete)).toBe(true);
|
|
74
|
+
expect(isCompleteExpression(missing)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("changeOperator", () => {
|
|
78
|
+
test("keeps a column across comparisons", () => {
|
|
79
|
+
let changed = changeOperator(expression({ value: "Date", valueType: "column" }), "lessthan", fields[1]);
|
|
80
|
+
expect(changed).toMatchObject({ value: "Date", valueType: "column" });
|
|
81
|
+
});
|
|
82
|
+
test("drops a column for an operator that cannot use one", () => {
|
|
83
|
+
let changed = changeOperator(expression({ column: "Name", value: "Other", valueType: "column" }), "contains", fields[5]);
|
|
84
|
+
expect(changed).toMatchObject({ value: "", valueType: "string" });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("describeFilter", () => {
|
|
88
|
+
test("names the compared column by its caption", () => {
|
|
89
|
+
expect(describeFilter(group(expression({ value: "Date", valueType: "column" })), { fields })).toBe("[Leveringsdato] After [Dato]");
|
|
90
|
+
});
|
|
91
|
+
test("describes each end of a range", () => {
|
|
92
|
+
let range = expression({
|
|
93
|
+
operator: "between",
|
|
94
|
+
value: [
|
|
95
|
+
{ valueType: "special", value: "Tomorrow" },
|
|
96
|
+
{ valueType: "special", value: "TwoWeeksAway" },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
expect(describeFilter(group(range), { fields })).toBe("[Leveringsdato] Between Tomorrow - Two weeks away");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("period operators", () => {
|
|
103
|
+
test("are offered for dates and take no value", () => {
|
|
104
|
+
let operators = getOperatorsForField({ type: "datetime" });
|
|
105
|
+
expect(operators).toContain("islastweek");
|
|
106
|
+
expect(operators).toContain("ispriorthisyear");
|
|
107
|
+
expect(getOperatorsForField({ type: "number" })).not.toContain("islastweek");
|
|
108
|
+
expect(getOperatorArity("islastweek")).toBe("none");
|
|
109
|
+
});
|
|
110
|
+
test("need a filter string", () => {
|
|
111
|
+
let period = expression({ operator: "islastweek", value: null, valueType: "null" });
|
|
112
|
+
expect(isCompleteExpression(period)).toBe(true);
|
|
113
|
+
expect(needsFilterString(group(period))).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
test("are described without a value", () => {
|
|
116
|
+
let period = expression({ operator: "islastweek", value: null, valueType: "null" });
|
|
117
|
+
expect(describeFilter(group(period), { fields })).toBe("[Leveringsdato] Is last week");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDynamicValueLabel, getOperatorArity, getOperatorLabel, normalizeOperator } from "./operators.js";
|
|
2
|
-
import { isExpression } from "./tree.js";
|
|
2
|
+
import { isExpression, isRangeEnd } from "./tree.js";
|
|
3
3
|
/**
|
|
4
4
|
* Builds a human-readable description of a filter, without contacting the
|
|
5
5
|
* server. Use this for read-only summaries; the authoritative filter string is
|
|
@@ -34,20 +34,22 @@ function describeExpression(expression, fields) {
|
|
|
34
34
|
let arity = getOperatorArity(operator);
|
|
35
35
|
if (arity === "none")
|
|
36
36
|
return `[${caption}] ${label}`;
|
|
37
|
-
return `[${caption}] ${label} ${formatValue(expression, arity)}`;
|
|
37
|
+
return `[${caption}] ${label} ${formatValue(expression, arity, fields)}`;
|
|
38
38
|
}
|
|
39
|
-
function formatValue(expression, arity) {
|
|
39
|
+
function formatValue(expression, arity, fields) {
|
|
40
40
|
let { value, valueType } = expression;
|
|
41
|
-
if (valueType === "special" && typeof value === "string") {
|
|
42
|
-
return getDynamicValueLabel(value);
|
|
43
|
-
}
|
|
44
41
|
if (Array.isArray(value)) {
|
|
45
|
-
let formatted = value.map((item) => formatScalar(item, valueType));
|
|
42
|
+
let formatted = value.map((item) => isRangeEnd(item) ? formatScalar(item.value, item.valueType, fields) : formatScalar(item, valueType, fields));
|
|
46
43
|
return arity === "pair" ? formatted.join(" - ") : `(${formatted.join(", ")})`;
|
|
47
44
|
}
|
|
48
|
-
return formatScalar(value, valueType);
|
|
45
|
+
return formatScalar(value, valueType, fields);
|
|
49
46
|
}
|
|
50
|
-
function formatScalar(value, valueType) {
|
|
47
|
+
function formatScalar(value, valueType, fields) {
|
|
48
|
+
if (valueType === "special" && typeof value === "string")
|
|
49
|
+
return getDynamicValueLabel(value);
|
|
50
|
+
if (valueType === "column" && typeof value === "string") {
|
|
51
|
+
return `[${fields.get(value.toLowerCase())?.caption ?? value}]`;
|
|
52
|
+
}
|
|
51
53
|
if (value === null || value === undefined)
|
|
52
54
|
return "";
|
|
53
55
|
if (value instanceof Date)
|
package/es/filter/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { type ConvertRequest, convertFilter, type FilterConversionResult } from "./convertFilter.js";
|
|
2
2
|
export { describeFilter, type DescribeFilterOptions } from "./describeFilter.js";
|
|
3
3
|
export { toFieldMetadata, toFieldMetadataList, toFilterFieldType, type ViewFieldRecord } from "./fields.js";
|
|
4
|
-
export { getDefaultOperator, getDynamicValueLabel, getDynamicValuesForField, getOperatorArity, getOperatorLabel, getOperatorsForField, getValueType, normalizeOperator, type OperatorArity, RELATIVE_DATE_VALUES, } from "./operators.js";
|
|
4
|
+
export { getColumnsForField, getDefaultOperator, getDynamicValueLabel, getDynamicValuesForField, getOperatorArity, getOperatorLabel, getOperatorsForField, getValueType, normalizeOperator, type OperatorArity, PERIOD_OPERATORS, RELATIVE_DATE_VALUES, supportsColumnValue, } from "./operators.js";
|
|
5
5
|
export { coerceToStandardLayout, isStandardLayout } from "./standardLayout.js";
|
|
6
6
|
export { canDeleteSavedFilter, canEditSavedFilter, canRenameSavedFilter, canSetCrossDomain, canShareSavedFilter, findSavedFilter, NO_FILTER_ID, partitionSavedFilters, type SavedFilter, type SavedFilterRecord, toSavedFilter, toSavedFilters, } from "./savedFilters.js";
|
|
7
|
-
export { addGroupAtPath, changeOperator, countExpressions, createEmptyFilter, createEmptyGroup, createExpression, type FilterNode, type FilterPath, getNodeAtPath, insertNodeAtPath, isCompleteExpression, isEmptyFilter, isExpression, isGroup, pruneFilter, removeNodeAtPath, setGroupMode, toFilterTree, unwrapSingleAndGroup, updateNodeAtPath, } from "./tree.js";
|
|
8
|
-
export type { DynamicValue, FieldMetadata, FilterAccessLevel, FilterExpression, FilterFieldType, FilterGroup, FilterOperator, FilterTree, FilterValueType, } from "./types.js";
|
|
7
|
+
export { addGroupAtPath, changeOperator, countExpressions, createEmptyFilter, createEmptyGroup, createExpression, type FilterNode, type FilterPath, getNodeAtPath, insertNodeAtPath, isCompleteExpression, isEmptyFilter, isExpression, isGroup, isRangeEnd, needsFilterString, pruneFilter, removeNodeAtPath, setGroupMode, toFilterTree, unwrapSingleAndGroup, updateNodeAtPath, } from "./tree.js";
|
|
8
|
+
export type { DynamicValue, FieldMetadata, FilterAccessLevel, FilterExpression, FilterFieldType, FilterGroup, FilterOperator, FilterRangeEnd, FilterTree, FilterValueType, } from "./types.js";
|
package/es/filter/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { convertFilter } from "./convertFilter.js";
|
|
2
2
|
export { describeFilter } from "./describeFilter.js";
|
|
3
3
|
export { toFieldMetadata, toFieldMetadataList, toFilterFieldType } from "./fields.js";
|
|
4
|
-
export { getDefaultOperator, getDynamicValueLabel, getDynamicValuesForField, getOperatorArity, getOperatorLabel, getOperatorsForField, getValueType, normalizeOperator, RELATIVE_DATE_VALUES, } from "./operators.js";
|
|
4
|
+
export { getColumnsForField, getDefaultOperator, getDynamicValueLabel, getDynamicValuesForField, getOperatorArity, getOperatorLabel, getOperatorsForField, getValueType, normalizeOperator, PERIOD_OPERATORS, RELATIVE_DATE_VALUES, supportsColumnValue, } from "./operators.js";
|
|
5
5
|
export { coerceToStandardLayout, isStandardLayout } from "./standardLayout.js";
|
|
6
6
|
export { canDeleteSavedFilter, canEditSavedFilter, canRenameSavedFilter, canSetCrossDomain, canShareSavedFilter, findSavedFilter, NO_FILTER_ID, partitionSavedFilters, toSavedFilter, toSavedFilters, } from "./savedFilters.js";
|
|
7
|
-
export { addGroupAtPath, changeOperator, countExpressions, createEmptyFilter, createEmptyGroup, createExpression, getNodeAtPath, insertNodeAtPath, isCompleteExpression, isEmptyFilter, isExpression, isGroup, pruneFilter, removeNodeAtPath, setGroupMode, toFilterTree, unwrapSingleAndGroup, updateNodeAtPath, } from "./tree.js";
|
|
7
|
+
export { addGroupAtPath, changeOperator, countExpressions, createEmptyFilter, createEmptyGroup, createExpression, getNodeAtPath, insertNodeAtPath, isCompleteExpression, isEmptyFilter, isExpression, isGroup, isRangeEnd, needsFilterString, pruneFilter, removeNodeAtPath, setGroupMode, toFilterTree, unwrapSingleAndGroup, updateNodeAtPath, } from "./tree.js";
|
package/es/filter/operators.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { DynamicValue, FieldMetadata, FilterFieldType, FilterOperator, FilterValueType } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whole-period checks for dates, such as "Is last week", in the order of the
|
|
4
|
+
* Windows filter editor. They take no value. The framework has no operator for
|
|
5
|
+
* them, so a filter using one is sent as a filter string (`needsFilterString`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const PERIOD_OPERATORS: FilterOperator[];
|
|
2
8
|
/** How many values an operator needs. */
|
|
3
9
|
export type OperatorArity = "none" | "single" | "pair" | "list";
|
|
4
10
|
export declare function getOperatorArity(operator: FilterOperator): OperatorArity;
|
|
@@ -30,6 +36,14 @@ export declare function getDynamicValueLabel(value: DynamicValue): string;
|
|
|
30
36
|
* user name.
|
|
31
37
|
*/
|
|
32
38
|
export declare function getDynamicValuesForField(field: Pick<FieldMetadata, "type">): DynamicValue[];
|
|
39
|
+
/** Whether an operator can take another column as its value. */
|
|
40
|
+
export declare function supportsColumnValue(operator: FilterOperator): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The columns a field can be compared with: the other fields of the same type,
|
|
43
|
+
* as in the Windows filter editor. Booleans have only true and false checks,
|
|
44
|
+
* so they get none.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getColumnsForField(field: Pick<FieldMetadata, "name" | "type">, fields: FieldMetadata[]): FieldMetadata[];
|
|
33
47
|
/**
|
|
34
48
|
* Normalizes an operator string coming from the server or from another filter
|
|
35
49
|
* producer (`AfGrid` emits PascalCase names) to the canonical lower-case form.
|
package/es/filter/operators.js
CHANGED
|
@@ -5,12 +5,32 @@ import { getLocalizedString } from "../getLocalizedString.js";
|
|
|
5
5
|
* `eOperators.ParameterLessChecks` plus the blank checks.
|
|
6
6
|
*/
|
|
7
7
|
const PARAMETERLESS_OPERATORS = ["isnull", "isnotnull", "istrue", "isfalse", "isblank", "isnotblank"];
|
|
8
|
+
/**
|
|
9
|
+
* Whole-period checks for dates, such as "Is last week", in the order of the
|
|
10
|
+
* Windows filter editor. They take no value. The framework has no operator for
|
|
11
|
+
* them, so a filter using one is sent as a filter string (`needsFilterString`).
|
|
12
|
+
*/
|
|
13
|
+
export const PERIOD_OPERATORS = [
|
|
14
|
+
"isbeyondthisyear",
|
|
15
|
+
"islaterthisyear",
|
|
16
|
+
"islaterthismonth",
|
|
17
|
+
"isnextweek",
|
|
18
|
+
"islaterthisweek",
|
|
19
|
+
"istomorrow",
|
|
20
|
+
"istoday",
|
|
21
|
+
"isyesterday",
|
|
22
|
+
"isearlierthisweek",
|
|
23
|
+
"islastweek",
|
|
24
|
+
"isearlierthismonth",
|
|
25
|
+
"isearlierthisyear",
|
|
26
|
+
"ispriorthisyear",
|
|
27
|
+
];
|
|
8
28
|
/** Operators whose value is a two element `[from, to]` array. */
|
|
9
29
|
const PAIR_OPERATORS = ["between", "notbetween", "datebetween", "datenotbetween"];
|
|
10
30
|
/** Operators whose value is an array of values. */
|
|
11
31
|
const LIST_OPERATORS = ["inlist", "notinlist"];
|
|
12
32
|
export function getOperatorArity(operator) {
|
|
13
|
-
if (PARAMETERLESS_OPERATORS.includes(operator))
|
|
33
|
+
if (PARAMETERLESS_OPERATORS.includes(operator) || PERIOD_OPERATORS.includes(operator))
|
|
14
34
|
return "none";
|
|
15
35
|
if (PAIR_OPERATORS.includes(operator))
|
|
16
36
|
return "pair";
|
|
@@ -57,6 +77,19 @@ const OPERATOR_LABELS = {
|
|
|
57
77
|
datelessthanorequal: "Up to",
|
|
58
78
|
datebetween: "Between",
|
|
59
79
|
datenotbetween: "Not between",
|
|
80
|
+
isbeyondthisyear: "Is beyond this year",
|
|
81
|
+
islaterthisyear: "Is later this year",
|
|
82
|
+
islaterthismonth: "Is later this month",
|
|
83
|
+
isnextweek: "Is next week",
|
|
84
|
+
islaterthisweek: "Is later this week",
|
|
85
|
+
istomorrow: "Is tomorrow",
|
|
86
|
+
istoday: "Is today",
|
|
87
|
+
isyesterday: "Is yesterday",
|
|
88
|
+
isearlierthisweek: "Is earlier this week",
|
|
89
|
+
islastweek: "Is last week",
|
|
90
|
+
isearlierthismonth: "Is earlier this month",
|
|
91
|
+
isearlierthisyear: "Is earlier this year",
|
|
92
|
+
ispriorthisyear: "Is prior to this year",
|
|
60
93
|
};
|
|
61
94
|
/** Comparison operator labels used for date and time fields. */
|
|
62
95
|
const DATE_OPERATOR_LABELS = {
|
|
@@ -93,8 +126,8 @@ const STRING_CHECKS = [
|
|
|
93
126
|
* presents them.
|
|
94
127
|
*/
|
|
95
128
|
const OPERATORS_BY_TYPE = {
|
|
96
|
-
date: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
97
|
-
datetime: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
129
|
+
date: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS, ...PERIOD_OPERATORS],
|
|
130
|
+
datetime: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS, ...PERIOD_OPERATORS],
|
|
98
131
|
number: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
99
132
|
boolean: ["istrue", "isfalse"],
|
|
100
133
|
string: [...EQUALITY, ...STRING_CHECKS, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
@@ -157,7 +190,10 @@ export function getValueType(field, operator) {
|
|
|
157
190
|
export const RELATIVE_DATE_VALUES = [
|
|
158
191
|
"Yesterday",
|
|
159
192
|
"Today",
|
|
193
|
+
"Now",
|
|
160
194
|
"Tomorrow",
|
|
195
|
+
"DayAfterTomorrow",
|
|
196
|
+
"TwoWeeksAway",
|
|
161
197
|
"ThisWeek",
|
|
162
198
|
"ThisMonth",
|
|
163
199
|
"ThisYear",
|
|
@@ -172,7 +208,10 @@ const DYNAMIC_VALUE_LABELS = {
|
|
|
172
208
|
CurrentUser: "Current user",
|
|
173
209
|
Yesterday: "Yesterday",
|
|
174
210
|
Today: "Today",
|
|
211
|
+
Now: "Now",
|
|
175
212
|
Tomorrow: "Tomorrow",
|
|
213
|
+
DayAfterTomorrow: "Day after tomorrow",
|
|
214
|
+
TwoWeeksAway: "Two weeks away",
|
|
176
215
|
ThisWeek: "This week",
|
|
177
216
|
ThisMonth: "This month",
|
|
178
217
|
ThisYear: "This year",
|
|
@@ -210,6 +249,27 @@ export function getDynamicValuesForField(field) {
|
|
|
210
249
|
return ["CurrentUser"];
|
|
211
250
|
return [];
|
|
212
251
|
}
|
|
252
|
+
/** Operators that can compare a field with another column. */
|
|
253
|
+
const COLUMN_OPERATORS = [...EQUALITY, ...RANGE, ...COMPARISONS];
|
|
254
|
+
/** Whether an operator can take another column as its value. */
|
|
255
|
+
export function supportsColumnValue(operator) {
|
|
256
|
+
return COLUMN_OPERATORS.includes(operator);
|
|
257
|
+
}
|
|
258
|
+
/** Dates and datetimes compare with each other; other types only with their own. */
|
|
259
|
+
function getComparableType(type) {
|
|
260
|
+
return type === "datetime" ? "date" : type;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The columns a field can be compared with: the other fields of the same type,
|
|
264
|
+
* as in the Windows filter editor. Booleans have only true and false checks,
|
|
265
|
+
* so they get none.
|
|
266
|
+
*/
|
|
267
|
+
export function getColumnsForField(field, fields) {
|
|
268
|
+
if (field.type === "boolean")
|
|
269
|
+
return [];
|
|
270
|
+
let type = getComparableType(field.type);
|
|
271
|
+
return fields.filter((other) => other.name !== field.name && !other.excludeFromFilter && getComparableType(other.type) === type);
|
|
272
|
+
}
|
|
213
273
|
/**
|
|
214
274
|
* Aliases for names `eOperators.ToString()` can produce that are not operators
|
|
215
275
|
* in their own right. `Not` is the bare negation flag (i.e. "not equal to"),
|
package/es/filter/tree.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldMetadata, FilterExpression, FilterGroup, FilterOperator, FilterTree } from "./types.js";
|
|
1
|
+
import type { FieldMetadata, FilterExpression, FilterGroup, FilterOperator, FilterRangeEnd, FilterTree } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Path to a node in a filter tree, as a list of item indexes from the root
|
|
4
4
|
* group. The root itself is the empty path.
|
|
@@ -49,6 +49,15 @@ export declare function pruneFilter(filter: FilterNode | null | undefined): Filt
|
|
|
49
49
|
* True when an expression has everything it needs to be sent to the server.
|
|
50
50
|
*/
|
|
51
51
|
export declare function isCompleteExpression(item: FilterNode): boolean;
|
|
52
|
+
/** Whether a range value end is a `FilterRangeEnd` rather than a plain value. */
|
|
53
|
+
export declare function isRangeEnd(value: unknown): value is FilterRangeEnd;
|
|
54
|
+
/**
|
|
55
|
+
* True when the filter holds something the framework's data API cannot read
|
|
56
|
+
* from a filter object: another column, a range end that is not a plain value,
|
|
57
|
+
* one of the dates only the Windows editor offers, or a whole-period check. Such a filter must be
|
|
58
|
+
* converted with `convertFilter` and sent as a filter string.
|
|
59
|
+
*/
|
|
60
|
+
export declare function needsFilterString(filter: FilterNode | null | undefined): boolean;
|
|
52
61
|
/**
|
|
53
62
|
* Creates an expression for a field, using the field's default operator and an
|
|
54
63
|
* empty value of the right shape.
|
package/es/filter/tree.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDefaultOperator, getOperatorArity, getValueType, normalizeOperator } from "./operators.js";
|
|
1
|
+
import { getDefaultOperator, getOperatorArity, getValueType, normalizeOperator, PERIOD_OPERATORS, supportsColumnValue, } from "./operators.js";
|
|
2
2
|
/**
|
|
3
3
|
* Creates the default empty filter: a single `and` group with no conditions.
|
|
4
4
|
*/
|
|
@@ -86,13 +86,46 @@ export function isCompleteExpression(item) {
|
|
|
86
86
|
if (item.value === null || item.value === undefined || item.value === "")
|
|
87
87
|
return false;
|
|
88
88
|
if (arity === "pair") {
|
|
89
|
-
return Array.isArray(item.value) &&
|
|
89
|
+
return (Array.isArray(item.value) &&
|
|
90
|
+
item.value.length === 2 &&
|
|
91
|
+
item.value.every((v) => {
|
|
92
|
+
let end = isRangeEnd(v) ? v.value : v;
|
|
93
|
+
return end !== "" && end != null;
|
|
94
|
+
}));
|
|
90
95
|
}
|
|
91
96
|
if (arity === "list") {
|
|
92
97
|
return Array.isArray(item.value) && item.value.length > 0;
|
|
93
98
|
}
|
|
94
99
|
return true;
|
|
95
100
|
}
|
|
101
|
+
/** Whether a range value end is a `FilterRangeEnd` rather than a plain value. */
|
|
102
|
+
export function isRangeEnd(value) {
|
|
103
|
+
return typeof value === "object" && value !== null && !(value instanceof Date) && "valueType" in value;
|
|
104
|
+
}
|
|
105
|
+
/** Dynamic values the framework has no `eDynamicValue` member for. */
|
|
106
|
+
const DYNAMIC_VALUES_WITHOUT_FRAMEWORK_SUPPORT = new Set(["Now", "DayAfterTomorrow", "TwoWeeksAway"]);
|
|
107
|
+
function needsConversion(valueType, value) {
|
|
108
|
+
if (valueType === "column")
|
|
109
|
+
return true;
|
|
110
|
+
return valueType === "special" && typeof value === "string" && DYNAMIC_VALUES_WITHOUT_FRAMEWORK_SUPPORT.has(value);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* True when the filter holds something the framework's data API cannot read
|
|
114
|
+
* from a filter object: another column, a range end that is not a plain value,
|
|
115
|
+
* one of the dates only the Windows editor offers, or a whole-period check. Such a filter must be
|
|
116
|
+
* converted with `convertFilter` and sent as a filter string.
|
|
117
|
+
*/
|
|
118
|
+
export function needsFilterString(filter) {
|
|
119
|
+
if (!filter)
|
|
120
|
+
return false;
|
|
121
|
+
if (isGroup(filter))
|
|
122
|
+
return filter.items.some(needsFilterString);
|
|
123
|
+
if (PERIOD_OPERATORS.includes(normalizeOperator(filter.operator)))
|
|
124
|
+
return true;
|
|
125
|
+
if (needsConversion(filter.valueType, filter.value))
|
|
126
|
+
return true;
|
|
127
|
+
return Array.isArray(filter.value) && filter.value.some((item) => isRangeEnd(item));
|
|
128
|
+
}
|
|
96
129
|
/**
|
|
97
130
|
* Creates an expression for a field, using the field's default operator and an
|
|
98
131
|
* empty value of the right shape.
|
|
@@ -127,11 +160,16 @@ export function changeOperator(expression, operator, field) {
|
|
|
127
160
|
let previousArity = getOperatorArity(normalizeOperator(expression.operator));
|
|
128
161
|
let arity = getOperatorArity(operator);
|
|
129
162
|
let value = expression.value;
|
|
130
|
-
|
|
163
|
+
// A column only makes sense for comparisons, not for text checks such as
|
|
164
|
+
// "contains".
|
|
165
|
+
let holdsColumn = expression.valueType === "column" ||
|
|
166
|
+
(Array.isArray(value) && value.some((item) => isRangeEnd(item) && item.valueType === "column"));
|
|
167
|
+
if (arity !== previousArity || (holdsColumn && !supportsColumnValue(operator))) {
|
|
131
168
|
value = createEmptyValue(operator);
|
|
132
169
|
}
|
|
133
|
-
// A dynamic value stays valid across operators of the same arity.
|
|
134
|
-
let
|
|
170
|
+
// A dynamic value or a column stays valid across operators of the same arity.
|
|
171
|
+
let keepsValueType = (expression.valueType === "special" || expression.valueType === "column") && value === expression.value;
|
|
172
|
+
let valueType = keepsValueType ? expression.valueType : getValueType(field, operator);
|
|
135
173
|
return { ...expression, operator, value, valueType };
|
|
136
174
|
}
|
|
137
175
|
/** Returns the node at `path`, or null when the path does not exist. */
|
package/es/filter/types.d.ts
CHANGED
|
@@ -15,31 +15,52 @@ export type FilterExpression = {
|
|
|
15
15
|
type: "expression";
|
|
16
16
|
column: string;
|
|
17
17
|
operator: string;
|
|
18
|
-
value: string | string[] | Date | number | null
|
|
18
|
+
value: string | string[] | Date | number | null | Array<string | FilterRangeEnd>;
|
|
19
19
|
valueType: string;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* One end of a range whose ends are not both plain values, such as a fixed
|
|
23
|
+
* date and another column. Plain ends may stay plain values next to it; they
|
|
24
|
+
* then take the expression's `valueType`.
|
|
25
|
+
*
|
|
26
|
+
* The framework's data API cannot read these, so a filter holding one is sent
|
|
27
|
+
* as a filter string. See `needsFilterString`.
|
|
28
|
+
*/
|
|
29
|
+
export type FilterRangeEnd = {
|
|
30
|
+
valueType: FilterValueType;
|
|
31
|
+
value: string | number | null;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The value types understood by the Appframe filter element converter, plus
|
|
35
|
+
* `column`, which compares with another column. Only the `api/filter/convert`
|
|
36
|
+
* endpoint reads `column`, so a filter using it is sent as a filter string.
|
|
23
37
|
*
|
|
24
38
|
* @see `appframe/Appframe.Web.Core/Filtering/FilterElementConverter.vb`
|
|
39
|
+
* @see `lib/FilterConversion/FilterObjectReader.cs`
|
|
25
40
|
*/
|
|
26
|
-
export type FilterValueType = "string" | "number" | "boolean" | "uniqueidentifier" | "array" | "datetime" | "special" | "null";
|
|
41
|
+
export type FilterValueType = "string" | "number" | "boolean" | "uniqueidentifier" | "array" | "datetime" | "special" | "column" | "null";
|
|
27
42
|
/**
|
|
28
43
|
* Canonical operator names. These are the lower-cased `eOperators` enum member
|
|
29
44
|
* names as emitted by the server, including the two aliases the server
|
|
30
|
-
* special-cases (`notequals` for `Not`, `notdatequals` for `NotDateEquals`)
|
|
45
|
+
* special-cases (`notequals` for `Not`, `notdatequals` for `NotDateEquals`),
|
|
46
|
+
* plus the whole-period checks (`islastweek` and so on), which only the
|
|
47
|
+
* `api/filter/convert` endpoint reads.
|
|
31
48
|
*
|
|
32
49
|
* @see `appframe/Appframe3.Common.Data/Filtering/eOperators.vb`
|
|
33
50
|
*/
|
|
34
|
-
export type FilterOperator = "equals" | "notequals" | "greaterthan" | "greaterthanorequal" | "lessthan" | "lessthanorequal" | "beginswith" | "notbeginswith" | "endswith" | "notendswith" | "contains" | "notcontains" | "like" | "notlike" | "isnull" | "isnotnull" | "isblank" | "isnotblank" | "istrue" | "isfalse" | "inlist" | "notinlist" | "between" | "notbetween" | "dateequals" | "notdatequals" | "dategreaterthan" | "dategreaterthanorequal" | "datelessthan" | "datelessthanorequal" | "datebetween" | "datenotbetween";
|
|
51
|
+
export type FilterOperator = "equals" | "notequals" | "greaterthan" | "greaterthanorequal" | "lessthan" | "lessthanorequal" | "beginswith" | "notbeginswith" | "endswith" | "notendswith" | "contains" | "notcontains" | "like" | "notlike" | "isnull" | "isnotnull" | "isblank" | "isnotblank" | "istrue" | "isfalse" | "inlist" | "notinlist" | "between" | "notbetween" | "dateequals" | "notdatequals" | "dategreaterthan" | "dategreaterthanorequal" | "datelessthan" | "datelessthanorequal" | "datebetween" | "datenotbetween" | "isbeyondthisyear" | "islaterthisyear" | "islaterthismonth" | "isnextweek" | "islaterthisweek" | "istomorrow" | "istoday" | "isyesterday" | "isearlierthisweek" | "islastweek" | "isearlierthismonth" | "isearlierthisyear" | "ispriorthisyear";
|
|
35
52
|
/**
|
|
36
53
|
* Dynamic ("special") filter values. These are resolved server-side into a
|
|
37
54
|
* relative date or the current user name, and are sent with
|
|
38
55
|
* `valueType: "special"`.
|
|
39
56
|
*
|
|
57
|
+
* `Now`, `DayAfterTomorrow` and `TwoWeeksAway` are offered by the Windows
|
|
58
|
+
* filter editor but have no `eDynamicValue` member, so a filter using them is
|
|
59
|
+
* sent as a filter string.
|
|
60
|
+
*
|
|
40
61
|
* @see `appframe/Appframe3.Common.Data/Filtering/eDynamicValue.vb`
|
|
41
62
|
*/
|
|
42
|
-
export type DynamicValue = "CurrentUser" | "Yesterday" | "Today" | "Tomorrow" | "ThisWeek" | "ThisMonth" | "ThisYear" | "LastWeek" | "LastMonth" | "LastYear" | "NextWeek" | "NextMonth" | "NextYear" | "UserYesterday" | "UserToday" | "UserTomorrow" | "UserThisWeek" | "UserThisMonth" | "UserThisYear" | "UserLastWeek" | "UserLastMonth" | "UserLastYear" | "UserNextWeek" | "UserNextMonth" | "UserNextYear";
|
|
63
|
+
export type DynamicValue = "CurrentUser" | "Yesterday" | "Today" | "Now" | "Tomorrow" | "DayAfterTomorrow" | "TwoWeeksAway" | "ThisWeek" | "ThisMonth" | "ThisYear" | "LastWeek" | "LastMonth" | "LastYear" | "NextWeek" | "NextMonth" | "NextYear" | "UserYesterday" | "UserToday" | "UserTomorrow" | "UserThisWeek" | "UserThisMonth" | "UserThisYear" | "UserLastWeek" | "UserLastMonth" | "UserLastYear" | "UserNextWeek" | "UserNextMonth" | "UserNextYear";
|
|
43
64
|
/**
|
|
44
65
|
* The simplified data types the filter builder groups fields into. Mirrors the
|
|
45
66
|
* normalized `DataType` column on `sviw_WebSiteCMS_FilterBuilder_ViewFields`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Javascript libraries to utilize the Appframe framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -40,9 +40,11 @@
|
|
|
40
40
|
"mitt": "^3.0.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@happy-dom/global-registrator": "
|
|
44
|
-
"@types/bun": "^1.4.
|
|
45
|
-
"chalk": "^6.0.1"
|
|
43
|
+
"@happy-dom/global-registrator": "21.0.4",
|
|
44
|
+
"@types/bun": "^1.4.4",
|
|
45
|
+
"chalk": "^6.0.1",
|
|
46
|
+
"typescript": "7.0.2",
|
|
47
|
+
"vitest": "5.0.5"
|
|
46
48
|
},
|
|
47
49
|
"peerDependencies": {},
|
|
48
50
|
"optionalDependencies": {},
|