@olenbetong/appframe-core 2.11.9 → 2.12.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 +1979 -609
- package/dist/esm/af.common.legacy.js +1933 -609
- package/dist/esm/af.common.legacy.min.js +4 -3
- package/dist/esm/af.common.legacy.min.js.map +1 -7
- package/dist/esm/af.common.min.js +4 -3
- package/dist/esm/af.common.min.js.map +1 -7
- package/dist/iife/af.common.js +2140 -694
- package/dist/iife/af.common.legacy.js +2094 -694
- package/dist/iife/af.common.legacy.min.js +4 -3
- package/dist/iife/af.common.legacy.min.js.map +1 -7
- package/dist/iife/af.common.min.js +4 -3
- package/dist/iife/af.common.min.js.map +1 -7
- package/es/alert.js +16 -1
- package/es/filter/convertFilter.d.ts +23 -0
- package/es/filter/convertFilter.js +26 -0
- package/es/filter/describeFilter.d.ts +13 -0
- package/es/filter/describeFilter.js +60 -0
- package/es/filter/describeFilter.test.d.ts +1 -0
- package/es/filter/describeFilter.test.js +68 -0
- package/es/filter/fields.d.ts +28 -0
- package/es/filter/fields.js +37 -0
- package/es/filter/fields.test.d.ts +1 -0
- package/es/filter/fields.test.js +49 -0
- package/es/filter/index.d.ts +8 -0
- package/es/filter/index.js +7 -0
- package/es/filter/operators.d.ts +37 -0
- package/es/filter/operators.js +229 -0
- package/es/filter/operators.test.d.ts +1 -0
- package/es/filter/operators.test.js +106 -0
- package/es/filter/savedFilters.d.ts +111 -0
- package/es/filter/savedFilters.js +118 -0
- package/es/filter/savedFilters.test.d.ts +1 -0
- package/es/filter/savedFilters.test.js +108 -0
- package/es/filter/standardLayout.d.ts +18 -0
- package/es/filter/standardLayout.js +55 -0
- package/es/filter/standardLayout.test.d.ts +1 -0
- package/es/filter/standardLayout.test.js +66 -0
- package/es/filter/tree.d.ts +70 -0
- package/es/filter/tree.js +185 -0
- package/es/filter/tree.test.d.ts +1 -0
- package/es/filter/tree.test.js +184 -0
- package/es/filter/types.d.ts +73 -0
- package/es/filter/types.js +1 -0
- package/es/index.d.ts +2 -0
- package/es/index.js +4 -2
- package/es/parseFunc.js +19 -2
- package/es/parseFunc.test.js +48 -4
- package/es/report/index.d.ts +6 -0
- package/es/report/index.js +5 -0
- package/es/report/parameterDefaults.d.ts +26 -0
- package/es/report/parameterDefaults.js +77 -0
- package/es/report/parameters.d.ts +46 -0
- package/es/report/parameters.js +188 -0
- package/es/report/parameters.test.d.ts +1 -0
- package/es/report/parameters.test.js +161 -0
- package/es/report/recordSource.d.ts +30 -0
- package/es/report/recordSource.js +105 -0
- package/es/report/render.d.ts +81 -0
- package/es/report/render.js +219 -0
- package/es/report/render.test.d.ts +1 -0
- package/es/report/render.test.js +144 -0
- package/es/report/report.d.ts +16 -0
- package/es/report/report.js +50 -0
- package/es/report/report.test.d.ts +1 -0
- package/es/report/report.test.js +86 -0
- package/es/report/types.d.ts +124 -0
- package/es/report/types.js +8 -0
- package/package.json +7 -7
- package/src/alert.ts +16 -1
- package/src/filter/convertFilter.ts +47 -0
- package/src/filter/describeFilter.test.ts +103 -0
- package/src/filter/describeFilter.ts +79 -0
- package/src/filter/fields.test.ts +58 -0
- package/src/filter/fields.ts +56 -0
- package/src/filter/index.ts +62 -0
- package/src/filter/operators.test.ts +136 -0
- package/src/filter/operators.ts +252 -0
- package/src/filter/savedFilters.test.ts +145 -0
- package/src/filter/savedFilters.ts +192 -0
- package/src/filter/standardLayout.test.ts +88 -0
- package/src/filter/standardLayout.ts +65 -0
- package/src/filter/tree.test.ts +258 -0
- package/src/filter/tree.ts +238 -0
- package/src/filter/types.ts +146 -0
- package/src/index.ts +3 -1
- package/src/parseFunc.test.ts +61 -4
- package/src/parseFunc.ts +29 -2
- package/src/report/index.ts +37 -0
- package/src/report/parameterDefaults.ts +101 -0
- package/src/report/parameters.test.ts +218 -0
- package/src/report/parameters.ts +230 -0
- package/src/report/recordSource.ts +123 -0
- package/src/report/render.test.ts +230 -0
- package/src/report/render.ts +269 -0
- package/src/report/report.test.ts +104 -0
- package/src/report/report.ts +51 -0
- package/src/report/types.ts +135 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getDefaultOperator,
|
|
5
|
+
getDynamicValuesForField,
|
|
6
|
+
getOperatorArity,
|
|
7
|
+
getOperatorLabel,
|
|
8
|
+
getOperatorsForField,
|
|
9
|
+
getValueType,
|
|
10
|
+
normalizeOperator,
|
|
11
|
+
} from "./operators.js";
|
|
12
|
+
|
|
13
|
+
describe("getOperatorsForField", () => {
|
|
14
|
+
test("boolean fields only get truth checks", () => {
|
|
15
|
+
expect(getOperatorsForField({ type: "boolean" })).toEqual(["istrue", "isfalse"]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("boolean fields never get null checks", () => {
|
|
19
|
+
expect(getOperatorsForField({ type: "boolean", nullable: true })).toEqual(["istrue", "isfalse"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("nullable fields get null checks last", () => {
|
|
23
|
+
let operators = getOperatorsForField({ type: "number", nullable: true });
|
|
24
|
+
|
|
25
|
+
expect(operators.slice(-2)).toEqual(["isnull", "isnotnull"]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("string fields get the string comparison operators", () => {
|
|
29
|
+
let operators = getOperatorsForField({ type: "string" });
|
|
30
|
+
|
|
31
|
+
expect(operators).toContain("contains");
|
|
32
|
+
expect(operators).toContain("beginswith");
|
|
33
|
+
expect(operators).toContain("isblank");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("number fields do not get string comparison operators", () => {
|
|
37
|
+
let operators = getOperatorsForField({ type: "number" });
|
|
38
|
+
|
|
39
|
+
expect(operators).not.toContain("contains");
|
|
40
|
+
expect(operators).not.toContain("beginswith");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("unknown field types fall back to the string operators", () => {
|
|
44
|
+
let operators = getOperatorsForField({ type: "something" as never });
|
|
45
|
+
|
|
46
|
+
expect(operators).toEqual(getOperatorsForField({ type: "string" }));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("getDefaultOperator", () => {
|
|
51
|
+
test.each([
|
|
52
|
+
["string", "beginswith"],
|
|
53
|
+
["number", "equals"],
|
|
54
|
+
["date", "equals"],
|
|
55
|
+
["datetime", "equals"],
|
|
56
|
+
["boolean", "istrue"],
|
|
57
|
+
] as const)("%s defaults to %s", (type, expected) => {
|
|
58
|
+
expect(getDefaultOperator({ type })).toBe(expected);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("getOperatorArity", () => {
|
|
63
|
+
test.each([
|
|
64
|
+
["isnull", "none"],
|
|
65
|
+
["istrue", "none"],
|
|
66
|
+
["isblank", "none"],
|
|
67
|
+
["between", "pair"],
|
|
68
|
+
["datenotbetween", "pair"],
|
|
69
|
+
["inlist", "list"],
|
|
70
|
+
["notinlist", "list"],
|
|
71
|
+
["equals", "single"],
|
|
72
|
+
["contains", "single"],
|
|
73
|
+
] as const)("%s has arity %s", (operator, expected) => {
|
|
74
|
+
expect(getOperatorArity(operator)).toBe(expected);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("getOperatorLabel", () => {
|
|
79
|
+
test("date fields get the relative wording for comparisons", () => {
|
|
80
|
+
expect(getOperatorLabel("greaterthan", "date")).toBe("After");
|
|
81
|
+
expect(getOperatorLabel("lessthanorequal", "datetime")).toBe("Up to");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("number fields get the numeric wording", () => {
|
|
85
|
+
expect(getOperatorLabel("greaterthan", "number")).toBe("Greater than");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("non-comparison operators are unaffected by the field type", () => {
|
|
89
|
+
expect(getOperatorLabel("inlist", "date")).toBe("In list");
|
|
90
|
+
expect(getOperatorLabel("inlist", "number")).toBe("In list");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("getValueType", () => {
|
|
95
|
+
test("parameterless operators send a null value type", () => {
|
|
96
|
+
expect(getValueType({ type: "string" }, "isnull")).toBe("null");
|
|
97
|
+
expect(getValueType({ type: "boolean" }, "istrue")).toBe("null");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test.each([
|
|
101
|
+
["string", "string"],
|
|
102
|
+
["number", "number"],
|
|
103
|
+
["date", "datetime"],
|
|
104
|
+
["datetime", "datetime"],
|
|
105
|
+
["uniqueidentifier", "uniqueidentifier"],
|
|
106
|
+
] as const)("%s fields send %s", (type, expected) => {
|
|
107
|
+
expect(getValueType({ type }, "equals")).toBe(expected);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("normalizeOperator", () => {
|
|
112
|
+
test.each([
|
|
113
|
+
["Equals", "equals"],
|
|
114
|
+
["BeginsWith", "beginswith"],
|
|
115
|
+
["NotDateEquals", "notdatequals"],
|
|
116
|
+
["Not", "notequals"],
|
|
117
|
+
["Tweens", "notbetween"],
|
|
118
|
+
["Not, Date", "notdatequals"],
|
|
119
|
+
])("%s normalizes to %s", (input, expected) => {
|
|
120
|
+
expect(normalizeOperator(input)).toBe(expected);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("getDynamicValuesForField", () => {
|
|
125
|
+
test("date fields get relative dates", () => {
|
|
126
|
+
expect(getDynamicValuesForField({ type: "date" })).toContain("ThisMonth");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("string fields only get the current user", () => {
|
|
130
|
+
expect(getDynamicValuesForField({ type: "string" })).toEqual(["CurrentUser"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("number fields get none", () => {
|
|
134
|
+
expect(getDynamicValuesForField({ type: "number" })).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { getLocalizedString } from "../getLocalizedString.js";
|
|
2
|
+
import type { DynamicValue, FieldMetadata, FilterFieldType, FilterOperator, FilterValueType } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Operators that take no value at all.
|
|
6
|
+
*
|
|
7
|
+
* `eOperators.ParameterLessChecks` plus the blank checks.
|
|
8
|
+
*/
|
|
9
|
+
const PARAMETERLESS_OPERATORS: FilterOperator[] = ["isnull", "isnotnull", "istrue", "isfalse", "isblank", "isnotblank"];
|
|
10
|
+
|
|
11
|
+
/** Operators whose value is a two element `[from, to]` array. */
|
|
12
|
+
const PAIR_OPERATORS: FilterOperator[] = ["between", "notbetween", "datebetween", "datenotbetween"];
|
|
13
|
+
|
|
14
|
+
/** Operators whose value is an array of values. */
|
|
15
|
+
const LIST_OPERATORS: FilterOperator[] = ["inlist", "notinlist"];
|
|
16
|
+
|
|
17
|
+
/** How many values an operator needs. */
|
|
18
|
+
export type OperatorArity = "none" | "single" | "pair" | "list";
|
|
19
|
+
|
|
20
|
+
export function getOperatorArity(operator: FilterOperator): OperatorArity {
|
|
21
|
+
if (PARAMETERLESS_OPERATORS.includes(operator)) return "none";
|
|
22
|
+
if (PAIR_OPERATORS.includes(operator)) return "pair";
|
|
23
|
+
if (LIST_OPERATORS.includes(operator)) return "list";
|
|
24
|
+
return "single";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Operator labels, keyed by operator. Comparison operators have a different
|
|
29
|
+
* label for dates than for numbers, matching the Windows filter editor.
|
|
30
|
+
*
|
|
31
|
+
* @see `appframe/Appframe3.Win/Controls/afFilterEditor/Utils/FilterLineControlUtils.vb`
|
|
32
|
+
*/
|
|
33
|
+
const OPERATOR_LABELS: Record<FilterOperator, string> = {
|
|
34
|
+
equals: "Equal to",
|
|
35
|
+
notequals: "Not equal to",
|
|
36
|
+
greaterthan: "Greater than",
|
|
37
|
+
greaterthanorequal: "Greater than or equal to",
|
|
38
|
+
lessthan: "Less than",
|
|
39
|
+
lessthanorequal: "Less than or equal to",
|
|
40
|
+
beginswith: "Begins with",
|
|
41
|
+
notbeginswith: "Does not begin with",
|
|
42
|
+
endswith: "Ends with",
|
|
43
|
+
notendswith: "Does not end with",
|
|
44
|
+
contains: "Contains",
|
|
45
|
+
notcontains: "Does not contain",
|
|
46
|
+
like: "Like",
|
|
47
|
+
notlike: "Not like",
|
|
48
|
+
isnull: "Is null",
|
|
49
|
+
isnotnull: "Is not null",
|
|
50
|
+
isblank: "Is empty",
|
|
51
|
+
isnotblank: "Is not empty",
|
|
52
|
+
istrue: "Is true",
|
|
53
|
+
isfalse: "Is false",
|
|
54
|
+
inlist: "In list",
|
|
55
|
+
notinlist: "Not in list",
|
|
56
|
+
between: "Between",
|
|
57
|
+
notbetween: "Not between",
|
|
58
|
+
dateequals: "Equal to",
|
|
59
|
+
notdatequals: "Not equal to",
|
|
60
|
+
dategreaterthan: "After",
|
|
61
|
+
dategreaterthanorequal: "On or after",
|
|
62
|
+
datelessthan: "Before",
|
|
63
|
+
datelessthanorequal: "Up to",
|
|
64
|
+
datebetween: "Between",
|
|
65
|
+
datenotbetween: "Not between",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Comparison operator labels used for date and time fields. */
|
|
69
|
+
const DATE_OPERATOR_LABELS: Partial<Record<FilterOperator, string>> = {
|
|
70
|
+
greaterthan: "After",
|
|
71
|
+
greaterthanorequal: "On or after",
|
|
72
|
+
lessthan: "Before",
|
|
73
|
+
lessthanorequal: "Up to",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the localized label for an operator. Date fields get the
|
|
78
|
+
* "After"/"Before" wording instead of "Greater than"/"Less than".
|
|
79
|
+
*/
|
|
80
|
+
export function getOperatorLabel(operator: FilterOperator, fieldType?: FilterFieldType): string {
|
|
81
|
+
let isDate = fieldType === "date" || fieldType === "datetime";
|
|
82
|
+
let label = (isDate ? DATE_OPERATOR_LABELS[operator] : undefined) ?? OPERATOR_LABELS[operator];
|
|
83
|
+
|
|
84
|
+
return getLocalizedString(label ?? operator);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const EQUALITY: FilterOperator[] = ["equals", "notequals"];
|
|
88
|
+
const LIST_CHECKS: FilterOperator[] = ["inlist", "notinlist"];
|
|
89
|
+
const RANGE: FilterOperator[] = ["between", "notbetween"];
|
|
90
|
+
const COMPARISONS: FilterOperator[] = ["greaterthan", "greaterthanorequal", "lessthan", "lessthanorequal"];
|
|
91
|
+
const STRING_CHECKS: FilterOperator[] = [
|
|
92
|
+
"contains",
|
|
93
|
+
"notcontains",
|
|
94
|
+
"beginswith",
|
|
95
|
+
"notbeginswith",
|
|
96
|
+
"endswith",
|
|
97
|
+
"notendswith",
|
|
98
|
+
"isblank",
|
|
99
|
+
"isnotblank",
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Operators offered per field type, in the order the Windows filter editor
|
|
104
|
+
* presents them.
|
|
105
|
+
*/
|
|
106
|
+
const OPERATORS_BY_TYPE: Record<FilterFieldType, FilterOperator[]> = {
|
|
107
|
+
date: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
108
|
+
datetime: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
109
|
+
number: [...EQUALITY, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
110
|
+
boolean: ["istrue", "isfalse"],
|
|
111
|
+
string: [...EQUALITY, ...STRING_CHECKS, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
112
|
+
uniqueidentifier: [...EQUALITY, ...STRING_CHECKS, ...LIST_CHECKS, ...RANGE, ...COMPARISONS],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Default operator per field type. */
|
|
116
|
+
const DEFAULT_OPERATOR_BY_TYPE: Record<FilterFieldType, FilterOperator> = {
|
|
117
|
+
date: "equals",
|
|
118
|
+
datetime: "equals",
|
|
119
|
+
number: "equals",
|
|
120
|
+
boolean: "istrue",
|
|
121
|
+
string: "beginswith",
|
|
122
|
+
uniqueidentifier: "beginswith",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the operators available for a field. Nullable fields additionally get
|
|
127
|
+
* the null checks. Boolean fields never get null checks, since `istrue` and
|
|
128
|
+
* `isfalse` already cover the parameterless cases.
|
|
129
|
+
*/
|
|
130
|
+
export function getOperatorsForField(field: Pick<FieldMetadata, "type" | "nullable">): FilterOperator[] {
|
|
131
|
+
let operators = [...(OPERATORS_BY_TYPE[field.type] ?? OPERATORS_BY_TYPE.string)];
|
|
132
|
+
|
|
133
|
+
if (field.nullable && field.type !== "boolean") {
|
|
134
|
+
operators.push("isnull", "isnotnull");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return operators;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function getDefaultOperator(field: Pick<FieldMetadata, "type">): FilterOperator {
|
|
141
|
+
return DEFAULT_OPERATOR_BY_TYPE[field.type] ?? "equals";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The `valueType` to send for a field/operator combination.
|
|
146
|
+
*
|
|
147
|
+
* Parameterless operators send `"null"`, list and range operators send
|
|
148
|
+
* `"array"`, everything else sends the field's own value type.
|
|
149
|
+
*/
|
|
150
|
+
export function getValueType(field: Pick<FieldMetadata, "type">, operator: FilterOperator): FilterValueType {
|
|
151
|
+
if (getOperatorArity(operator) === "none") return "null";
|
|
152
|
+
|
|
153
|
+
switch (field.type) {
|
|
154
|
+
case "number":
|
|
155
|
+
return "number";
|
|
156
|
+
case "boolean":
|
|
157
|
+
return "boolean";
|
|
158
|
+
case "date":
|
|
159
|
+
case "datetime":
|
|
160
|
+
return "datetime";
|
|
161
|
+
case "uniqueidentifier":
|
|
162
|
+
return "uniqueidentifier";
|
|
163
|
+
default:
|
|
164
|
+
return "string";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Relative date values offered in the date value editor, in menu order. */
|
|
169
|
+
export const RELATIVE_DATE_VALUES: DynamicValue[] = [
|
|
170
|
+
"Yesterday",
|
|
171
|
+
"Today",
|
|
172
|
+
"Tomorrow",
|
|
173
|
+
"ThisWeek",
|
|
174
|
+
"ThisMonth",
|
|
175
|
+
"ThisYear",
|
|
176
|
+
"LastWeek",
|
|
177
|
+
"LastMonth",
|
|
178
|
+
"LastYear",
|
|
179
|
+
"NextWeek",
|
|
180
|
+
"NextMonth",
|
|
181
|
+
"NextYear",
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const DYNAMIC_VALUE_LABELS: Record<DynamicValue, string> = {
|
|
185
|
+
CurrentUser: "Current user",
|
|
186
|
+
Yesterday: "Yesterday",
|
|
187
|
+
Today: "Today",
|
|
188
|
+
Tomorrow: "Tomorrow",
|
|
189
|
+
ThisWeek: "This week",
|
|
190
|
+
ThisMonth: "This month",
|
|
191
|
+
ThisYear: "This year",
|
|
192
|
+
LastWeek: "Last week",
|
|
193
|
+
LastMonth: "Last month",
|
|
194
|
+
LastYear: "Last year",
|
|
195
|
+
NextWeek: "Next week",
|
|
196
|
+
NextMonth: "Next month",
|
|
197
|
+
NextYear: "Next year",
|
|
198
|
+
UserYesterday: "Yesterday",
|
|
199
|
+
UserToday: "Today",
|
|
200
|
+
UserTomorrow: "Tomorrow",
|
|
201
|
+
UserThisWeek: "This week",
|
|
202
|
+
UserThisMonth: "This month",
|
|
203
|
+
UserThisYear: "This year",
|
|
204
|
+
UserLastWeek: "Last week",
|
|
205
|
+
UserLastMonth: "Last month",
|
|
206
|
+
UserLastYear: "Last year",
|
|
207
|
+
UserNextWeek: "Next week",
|
|
208
|
+
UserNextMonth: "Next month",
|
|
209
|
+
UserNextYear: "Next year",
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export function getDynamicValueLabel(value: DynamicValue): string {
|
|
213
|
+
return getLocalizedString(DYNAMIC_VALUE_LABELS[value] ?? value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Returns the dynamic ("special") values that can be used with a field. Date
|
|
218
|
+
* fields get the relative dates; string fields can be compared to the current
|
|
219
|
+
* user name.
|
|
220
|
+
*/
|
|
221
|
+
export function getDynamicValuesForField(field: Pick<FieldMetadata, "type">): DynamicValue[] {
|
|
222
|
+
if (field.type === "date" || field.type === "datetime") return [...RELATIVE_DATE_VALUES];
|
|
223
|
+
if (field.type === "string") return ["CurrentUser"];
|
|
224
|
+
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Aliases for names `eOperators.ToString()` can produce that are not operators
|
|
230
|
+
* in their own right. `Not` is the bare negation flag (i.e. "not equal to"),
|
|
231
|
+
* `Tweens` shares its numeric value with `NotBetween`, and combined flags are
|
|
232
|
+
* rendered as a comma separated list.
|
|
233
|
+
*
|
|
234
|
+
* @see `FilterElementConverter.FilterExpressionToDictionary`
|
|
235
|
+
*/
|
|
236
|
+
const OPERATOR_ALIASES: Record<string, FilterOperator> = {
|
|
237
|
+
not: "notequals",
|
|
238
|
+
tweens: "notbetween",
|
|
239
|
+
notdate: "notdatequals",
|
|
240
|
+
// The enum member is `NotDateEquals`, but the wire format drops an "e".
|
|
241
|
+
notdateequals: "notdatequals",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Normalizes an operator string coming from the server or from another filter
|
|
246
|
+
* producer (`AfGrid` emits PascalCase names) to the canonical lower-case form.
|
|
247
|
+
*/
|
|
248
|
+
export function normalizeOperator(operator: string): FilterOperator {
|
|
249
|
+
let normalized = operator.toLowerCase().replace(/[\s,]/g, "");
|
|
250
|
+
|
|
251
|
+
return OPERATOR_ALIASES[normalized] ?? (normalized as FilterOperator);
|
|
252
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canDeleteSavedFilter,
|
|
5
|
+
canEditSavedFilter,
|
|
6
|
+
canRenameSavedFilter,
|
|
7
|
+
canShareSavedFilter,
|
|
8
|
+
findSavedFilter,
|
|
9
|
+
NO_FILTER_ID,
|
|
10
|
+
partitionSavedFilters,
|
|
11
|
+
type SavedFilterRecord,
|
|
12
|
+
toSavedFilter,
|
|
13
|
+
toSavedFilters,
|
|
14
|
+
} from "./savedFilters.js";
|
|
15
|
+
|
|
16
|
+
function record(overrides: SavedFilterRecord = {}): SavedFilterRecord {
|
|
17
|
+
return {
|
|
18
|
+
PrimKey: "11111111-1111-1111-1111-111111111111",
|
|
19
|
+
FilterID: 1,
|
|
20
|
+
DBObjectID: "aviw_Test_View",
|
|
21
|
+
FilterName: "My filter",
|
|
22
|
+
FilterCriteria: "[Name] = 'a'",
|
|
23
|
+
AccessLevel: "Manager",
|
|
24
|
+
CreatedByUser: "true",
|
|
25
|
+
CreatedBy: "bvh",
|
|
26
|
+
Domain: "OB",
|
|
27
|
+
CrossDomain: false,
|
|
28
|
+
IsPersonallyShared: 0,
|
|
29
|
+
HideFilterCriteria: 0,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("toSavedFilter", () => {
|
|
35
|
+
test("maps a record", () => {
|
|
36
|
+
let filter = toSavedFilter(record());
|
|
37
|
+
|
|
38
|
+
expect(filter).toMatchObject({
|
|
39
|
+
filterId: 1,
|
|
40
|
+
name: "My filter",
|
|
41
|
+
criteria: "[Name] = 'a'",
|
|
42
|
+
dbObjectId: "aviw_Test_View",
|
|
43
|
+
accessLevel: "Manager",
|
|
44
|
+
createdByUser: true,
|
|
45
|
+
crossDomain: false,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("CreatedByUser is a string, not a bit", () => {
|
|
50
|
+
expect(toSavedFilter(record({ CreatedByUser: "false" })).createdByUser).toBe(false);
|
|
51
|
+
expect(toSavedFilter(record({ CreatedByUser: "False" })).createdByUser).toBe(false);
|
|
52
|
+
expect(toSavedFilter(record({ CreatedByUser: "true" })).createdByUser).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("bit columns arrive as numbers", () => {
|
|
56
|
+
let filter = toSavedFilter(record({ CrossDomain: 1, IsPersonallyShared: 1, HideFilterCriteria: 1 }));
|
|
57
|
+
|
|
58
|
+
expect(filter.crossDomain).toBe(true);
|
|
59
|
+
expect(filter.isPersonallyShared).toBe(true);
|
|
60
|
+
expect(filter.hideCriteria).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("an unknown access level falls back to Reader", () => {
|
|
64
|
+
expect(toSavedFilter(record({ AccessLevel: null })).accessLevel).toBe("Reader");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("empty optional strings become null", () => {
|
|
68
|
+
let filter = toSavedFilter(record({ OptionalHeaderText: "", OptionalComment: "" }));
|
|
69
|
+
|
|
70
|
+
expect(filter.headerText).toBeNull();
|
|
71
|
+
expect(filter.comment).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("dates are parsed", () => {
|
|
75
|
+
expect(toSavedFilter(record({ Created: "2026-01-02T03:04:05Z" })).created).toBeInstanceOf(Date);
|
|
76
|
+
expect(toSavedFilter(record({ Created: null })).created).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("toSavedFilters", () => {
|
|
81
|
+
test("drops the synthetic No Filter row", () => {
|
|
82
|
+
let filters = toSavedFilters([record({ FilterID: NO_FILTER_ID, FilterName: "No Filter" }), record()]);
|
|
83
|
+
|
|
84
|
+
expect(filters).toHaveLength(1);
|
|
85
|
+
expect(filters[0].name).toBe("My filter");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("sorts by name", () => {
|
|
89
|
+
let filters = toSavedFilters([
|
|
90
|
+
record({ FilterID: 1, FilterName: "Beta" }),
|
|
91
|
+
record({ FilterID: 2, FilterName: "Alpha" }),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
expect(filters.map((filter) => filter.name)).toEqual(["Alpha", "Beta"]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("partitionSavedFilters", () => {
|
|
99
|
+
test("splits on createdByUser", () => {
|
|
100
|
+
let filters = toSavedFilters([
|
|
101
|
+
record({ FilterID: 1, FilterName: "Mine", CreatedByUser: "true" }),
|
|
102
|
+
record({ FilterID: 2, FilterName: "Theirs", CreatedByUser: "false" }),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
let { mine, shared } = partitionSavedFilters(filters);
|
|
106
|
+
|
|
107
|
+
expect(mine.map((filter) => filter.name)).toEqual(["Mine"]);
|
|
108
|
+
expect(shared.map((filter) => filter.name)).toEqual(["Theirs"]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("permissions", () => {
|
|
113
|
+
test("a reader can neither edit nor share", () => {
|
|
114
|
+
let filter = toSavedFilter(record({ AccessLevel: "Reader", CreatedByUser: "false" }));
|
|
115
|
+
|
|
116
|
+
expect(canEditSavedFilter(filter)).toBe(false);
|
|
117
|
+
expect(canShareSavedFilter(filter)).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("an editor can edit but not share", () => {
|
|
121
|
+
let filter = toSavedFilter(record({ AccessLevel: "Editor", CreatedByUser: "false" }));
|
|
122
|
+
|
|
123
|
+
expect(canEditSavedFilter(filter)).toBe(true);
|
|
124
|
+
expect(canShareSavedFilter(filter)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("renaming and deleting need ownership, not manager access", () => {
|
|
128
|
+
let managed = toSavedFilter(record({ AccessLevel: "Manager", CreatedByUser: "false" }));
|
|
129
|
+
|
|
130
|
+
expect(canShareSavedFilter(managed)).toBe(true);
|
|
131
|
+
expect(canRenameSavedFilter(managed)).toBe(false);
|
|
132
|
+
expect(canDeleteSavedFilter(managed)).toBe(false);
|
|
133
|
+
expect(canRenameSavedFilter(toSavedFilter(record()))).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("findSavedFilter", () => {
|
|
138
|
+
test("finds by id and tolerates null", () => {
|
|
139
|
+
let filters = toSavedFilters([record({ FilterID: 7, FilterName: "Seven" })]);
|
|
140
|
+
|
|
141
|
+
expect(findSavedFilter(filters, 7)?.name).toBe("Seven");
|
|
142
|
+
expect(findSavedFilter(filters, 8)).toBeUndefined();
|
|
143
|
+
expect(findSavedFilter(filters, null)).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
});
|