@olenbetong/appframe-core 2.14.0 → 2.15.1
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 +22 -7
- package/dist/esm/af.common.legacy.js +18 -3
- 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 +22 -6
- package/dist/iife/af.common.legacy.js +18 -2
- 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/index.d.ts +1 -1
- package/es/filter/index.js +1 -1
- package/es/filter/tree.d.ts +9 -0
- package/es/filter/tree.js +14 -0
- package/es/filter/tree.test.js +23 -1
- package/es/index.js +1 -1
- package/es/report/render.js +5 -1
- package/es/report/render.test.js +26 -2
- package/package.json +1 -1
- package/src/filter/index.ts +1 -0
- package/src/filter/tree.test.ts +33 -0
- package/src/filter/tree.ts +16 -0
- package/src/report/render.test.ts +36 -1
- package/src/report/render.ts +6 -1
package/es/filter/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { toFieldMetadata, toFieldMetadataList, toFilterFieldType, type ViewField
|
|
|
4
4
|
export { getDefaultOperator, getDynamicValueLabel, getDynamicValuesForField, getOperatorArity, getOperatorLabel, getOperatorsForField, getValueType, normalizeOperator, type OperatorArity, RELATIVE_DATE_VALUES, } 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, updateNodeAtPath, } from "./tree.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
8
|
export type { DynamicValue, FieldMetadata, FilterAccessLevel, FilterExpression, FilterFieldType, FilterGroup, FilterOperator, FilterTree, FilterValueType, } from "./types.js";
|
package/es/filter/index.js
CHANGED
|
@@ -4,4 +4,4 @@ export { toFieldMetadata, toFieldMetadataList, toFilterFieldType } from "./field
|
|
|
4
4
|
export { getDefaultOperator, getDynamicValueLabel, getDynamicValuesForField, getOperatorArity, getOperatorLabel, getOperatorsForField, getValueType, normalizeOperator, RELATIVE_DATE_VALUES, } 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, updateNodeAtPath, } from "./tree.js";
|
|
7
|
+
export { addGroupAtPath, changeOperator, countExpressions, createEmptyFilter, createEmptyGroup, createExpression, getNodeAtPath, insertNodeAtPath, isCompleteExpression, isEmptyFilter, isExpression, isGroup, pruneFilter, removeNodeAtPath, setGroupMode, toFilterTree, unwrapSingleAndGroup, updateNodeAtPath, } from "./tree.js";
|
package/es/filter/tree.d.ts
CHANGED
|
@@ -24,6 +24,15 @@ export declare function isExpression(item: FilterNode): item is FilterExpression
|
|
|
24
24
|
* becomes the default layout.
|
|
25
25
|
*/
|
|
26
26
|
export declare function toFilterTree(filter: FilterNode | null | undefined): FilterTree;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the `and` group alone when it is the only item of an `or` group.
|
|
29
|
+
*
|
|
30
|
+
* The server parses filter strings into the standard layout, which wraps even
|
|
31
|
+
* a plain list of conditions in an `or` group of one `and` group. That extra
|
|
32
|
+
* level means nothing, so the builder shows the `and` group on its own, as the
|
|
33
|
+
* Windows filter editor does.
|
|
34
|
+
*/
|
|
35
|
+
export declare function unwrapSingleAndGroup(filter: FilterTree): FilterTree;
|
|
27
36
|
/**
|
|
28
37
|
* True when the filter contains no expressions. An empty filter must not be
|
|
29
38
|
* saved or applied.
|
package/es/filter/tree.js
CHANGED
|
@@ -26,6 +26,20 @@ export function toFilterTree(filter) {
|
|
|
26
26
|
return { type: "group", mode: "and", items: [filter] };
|
|
27
27
|
return filter;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the `and` group alone when it is the only item of an `or` group.
|
|
31
|
+
*
|
|
32
|
+
* The server parses filter strings into the standard layout, which wraps even
|
|
33
|
+
* a plain list of conditions in an `or` group of one `and` group. That extra
|
|
34
|
+
* level means nothing, so the builder shows the `and` group on its own, as the
|
|
35
|
+
* Windows filter editor does.
|
|
36
|
+
*/
|
|
37
|
+
export function unwrapSingleAndGroup(filter) {
|
|
38
|
+
if (filter.mode !== "or" || filter.items.length !== 1)
|
|
39
|
+
return filter;
|
|
40
|
+
let [only] = filter.items;
|
|
41
|
+
return isGroup(only) && only.mode === "and" ? only : filter;
|
|
42
|
+
}
|
|
29
43
|
/**
|
|
30
44
|
* True when the filter contains no expressions. An empty filter must not be
|
|
31
45
|
* saved or applied.
|
package/es/filter/tree.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { addGroupAtPath, changeOperator, countExpressions, createEmptyFilter, createExpression, getNodeAtPath, insertNodeAtPath, isCompleteExpression, isEmptyFilter, pruneFilter, removeNodeAtPath, setGroupMode, toFilterTree, updateNodeAtPath, } from "./tree.js";
|
|
2
|
+
import { addGroupAtPath, changeOperator, countExpressions, createEmptyFilter, createExpression, getNodeAtPath, insertNodeAtPath, isCompleteExpression, isEmptyFilter, pruneFilter, removeNodeAtPath, setGroupMode, toFilterTree, unwrapSingleAndGroup, updateNodeAtPath, } from "./tree.js";
|
|
3
3
|
function expression(column, operator = "equals", value = "x") {
|
|
4
4
|
return { type: "expression", column, operator, value, valueType: "string" };
|
|
5
5
|
}
|
|
@@ -182,3 +182,25 @@ describe("path helpers", () => {
|
|
|
182
182
|
expect(countExpressions(filter)).toBe(0);
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
|
+
describe("unwrapSingleAndGroup", () => {
|
|
186
|
+
test("returns the and group when it is the only item of an or group", () => {
|
|
187
|
+
let inner = group("and", expression("A"), expression("B"));
|
|
188
|
+
expect(unwrapSingleAndGroup(group("or", inner))).toBe(inner);
|
|
189
|
+
});
|
|
190
|
+
test("keeps an or group with several items", () => {
|
|
191
|
+
let filter = group("or", group("and", expression("A")), group("and", expression("B")));
|
|
192
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
193
|
+
});
|
|
194
|
+
test("keeps an or group holding a single expression", () => {
|
|
195
|
+
let filter = group("or", expression("A"));
|
|
196
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
197
|
+
});
|
|
198
|
+
test("keeps an or group holding a single or group", () => {
|
|
199
|
+
let filter = group("or", group("or", expression("A"), expression("B")));
|
|
200
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
201
|
+
});
|
|
202
|
+
test("keeps an and group as it is", () => {
|
|
203
|
+
let filter = group("and", expression("A"));
|
|
204
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
205
|
+
});
|
|
206
|
+
});
|
package/es/index.js
CHANGED
|
@@ -19,4 +19,4 @@ export { UserSession };
|
|
|
19
19
|
export { alert, ConfirmButtons, confirm, openAlertDialog, waitForDialog } from "./alert.js";
|
|
20
20
|
export { getSettings, saveSettings } from "./settings.js";
|
|
21
21
|
// oxlint-disable-next-line no-use-before-define -- this is injected build time
|
|
22
|
-
export const version = "2.
|
|
22
|
+
export const version = "2.15.1";
|
package/es/report/render.js
CHANGED
|
@@ -151,11 +151,15 @@ export async function openReport(options) {
|
|
|
151
151
|
// The route serves some formats as types a browser downloads rather than
|
|
152
152
|
// displays; the format's own media type is what decides how it opens.
|
|
153
153
|
let url = URL.createObjectURL(blob.type ? blob : new Blob([blob], { type: getReportMediaType(options.format) }));
|
|
154
|
-
|
|
154
|
+
// Not opened with the `noopener` feature: `open` then returns null even when
|
|
155
|
+
// the tab opens, which read as a blocked pop-up and revoked the URL before
|
|
156
|
+
// the tab could load it. The opener is cut off by hand instead.
|
|
157
|
+
let opened = globalThis.open(url, "_blank");
|
|
155
158
|
if (!opened) {
|
|
156
159
|
URL.revokeObjectURL(url);
|
|
157
160
|
throw new Error("The report could not be opened. Allow pop-ups for this site and try again.");
|
|
158
161
|
}
|
|
162
|
+
opened.opener = null;
|
|
159
163
|
// Revoking immediately would cancel the load in some browsers, and never
|
|
160
164
|
// revoking keeps the whole rendered report in memory.
|
|
161
165
|
globalThis.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
package/es/report/render.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { fetchParameterDefault, resolveParameterDefaults } from "./parameterDefaults.js";
|
|
3
|
-
import { getReportExtension, getReportFileName, getReportRequest, printReport, renderReport, } from "./render.js";
|
|
3
|
+
import { getReportExtension, getReportFileName, getReportRequest, openReport, printReport, renderReport, } from "./render.js";
|
|
4
4
|
function options(overrides = {}) {
|
|
5
5
|
return { format: "PDF", reportId: "arpt_Test_Report", timezone: 2, ...overrides };
|
|
6
6
|
}
|
|
@@ -100,7 +100,31 @@ describe("printReport", () => {
|
|
|
100
100
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("%PDF-", { headers: { "Content-Type": "application/pdf" } })));
|
|
101
101
|
vi.stubGlobal("open", openMock);
|
|
102
102
|
await printReport(options());
|
|
103
|
-
expect(openMock).toHaveBeenCalledWith(expect.stringContaining("blob:"), "_blank"
|
|
103
|
+
expect(openMock).toHaveBeenCalledWith(expect.stringContaining("blob:"), "_blank");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe("openReport", () => {
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
vi.restoreAllMocks();
|
|
109
|
+
});
|
|
110
|
+
function stubRender() {
|
|
111
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("%PDF-", { headers: { "Content-Type": "application/pdf" } })));
|
|
112
|
+
}
|
|
113
|
+
it("keeps the file available to the tab it opened, and cuts the tab off from the opener", async () => {
|
|
114
|
+
let tab = { opener: {} };
|
|
115
|
+
let revoke = vi.spyOn(URL, "revokeObjectURL");
|
|
116
|
+
stubRender();
|
|
117
|
+
vi.stubGlobal("open", vi.fn().mockReturnValue(tab));
|
|
118
|
+
await openReport(options());
|
|
119
|
+
expect(revoke).not.toHaveBeenCalled();
|
|
120
|
+
expect(tab.opener).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
it("reports a blocked pop-up and releases the file", async () => {
|
|
123
|
+
let revoke = vi.spyOn(URL, "revokeObjectURL");
|
|
124
|
+
stubRender();
|
|
125
|
+
vi.stubGlobal("open", vi.fn().mockReturnValue(null));
|
|
126
|
+
await expect(openReport(options())).rejects.toThrow("Allow pop-ups");
|
|
127
|
+
expect(revoke).toHaveBeenCalled();
|
|
104
128
|
});
|
|
105
129
|
});
|
|
106
130
|
describe("fetchParameterDefault", () => {
|
package/package.json
CHANGED
package/src/filter/index.ts
CHANGED
package/src/filter/tree.test.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
removeNodeAtPath,
|
|
16
16
|
setGroupMode,
|
|
17
17
|
toFilterTree,
|
|
18
|
+
unwrapSingleAndGroup,
|
|
18
19
|
updateNodeAtPath,
|
|
19
20
|
} from "./tree.js";
|
|
20
21
|
|
|
@@ -256,3 +257,35 @@ describe("path helpers", () => {
|
|
|
256
257
|
expect(countExpressions(filter)).toBe(0);
|
|
257
258
|
});
|
|
258
259
|
});
|
|
260
|
+
|
|
261
|
+
describe("unwrapSingleAndGroup", () => {
|
|
262
|
+
test("returns the and group when it is the only item of an or group", () => {
|
|
263
|
+
let inner = group("and", expression("A"), expression("B"));
|
|
264
|
+
|
|
265
|
+
expect(unwrapSingleAndGroup(group("or", inner))).toBe(inner);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("keeps an or group with several items", () => {
|
|
269
|
+
let filter = group("or", group("and", expression("A")), group("and", expression("B")));
|
|
270
|
+
|
|
271
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("keeps an or group holding a single expression", () => {
|
|
275
|
+
let filter = group("or", expression("A"));
|
|
276
|
+
|
|
277
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("keeps an or group holding a single or group", () => {
|
|
281
|
+
let filter = group("or", group("or", expression("A"), expression("B")));
|
|
282
|
+
|
|
283
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("keeps an and group as it is", () => {
|
|
287
|
+
let filter = group("and", expression("A"));
|
|
288
|
+
|
|
289
|
+
expect(unwrapSingleAndGroup(filter)).toBe(filter);
|
|
290
|
+
});
|
|
291
|
+
});
|
package/src/filter/tree.ts
CHANGED
|
@@ -46,6 +46,22 @@ export function toFilterTree(filter: FilterNode | null | undefined): FilterTree
|
|
|
46
46
|
return filter;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Returns the `and` group alone when it is the only item of an `or` group.
|
|
51
|
+
*
|
|
52
|
+
* The server parses filter strings into the standard layout, which wraps even
|
|
53
|
+
* a plain list of conditions in an `or` group of one `and` group. That extra
|
|
54
|
+
* level means nothing, so the builder shows the `and` group on its own, as the
|
|
55
|
+
* Windows filter editor does.
|
|
56
|
+
*/
|
|
57
|
+
export function unwrapSingleAndGroup(filter: FilterTree): FilterTree {
|
|
58
|
+
if (filter.mode !== "or" || filter.items.length !== 1) return filter;
|
|
59
|
+
|
|
60
|
+
let [only] = filter.items;
|
|
61
|
+
|
|
62
|
+
return isGroup(only) && only.mode === "and" ? only : filter;
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* True when the filter contains no expressions. An empty filter must not be
|
|
51
67
|
* saved or applied.
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getReportExtension,
|
|
6
6
|
getReportFileName,
|
|
7
7
|
getReportRequest,
|
|
8
|
+
openReport,
|
|
8
9
|
printReport,
|
|
9
10
|
type ReportRenderOptions,
|
|
10
11
|
renderReport,
|
|
@@ -145,7 +146,41 @@ describe("printReport", () => {
|
|
|
145
146
|
|
|
146
147
|
await printReport(options());
|
|
147
148
|
|
|
148
|
-
expect(openMock).toHaveBeenCalledWith(expect.stringContaining("blob:"), "_blank"
|
|
149
|
+
expect(openMock).toHaveBeenCalledWith(expect.stringContaining("blob:"), "_blank");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("openReport", () => {
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
vi.restoreAllMocks();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
function stubRender() {
|
|
159
|
+
vi.stubGlobal(
|
|
160
|
+
"fetch",
|
|
161
|
+
vi.fn().mockResolvedValue(new Response("%PDF-", { headers: { "Content-Type": "application/pdf" } })),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
it("keeps the file available to the tab it opened, and cuts the tab off from the opener", async () => {
|
|
166
|
+
let tab = { opener: {} as unknown };
|
|
167
|
+
let revoke = vi.spyOn(URL, "revokeObjectURL");
|
|
168
|
+
stubRender();
|
|
169
|
+
vi.stubGlobal("open", vi.fn().mockReturnValue(tab));
|
|
170
|
+
|
|
171
|
+
await openReport(options());
|
|
172
|
+
|
|
173
|
+
expect(revoke).not.toHaveBeenCalled();
|
|
174
|
+
expect(tab.opener).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("reports a blocked pop-up and releases the file", async () => {
|
|
178
|
+
let revoke = vi.spyOn(URL, "revokeObjectURL");
|
|
179
|
+
stubRender();
|
|
180
|
+
vi.stubGlobal("open", vi.fn().mockReturnValue(null));
|
|
181
|
+
|
|
182
|
+
await expect(openReport(options())).rejects.toThrow("Allow pop-ups");
|
|
183
|
+
expect(revoke).toHaveBeenCalled();
|
|
149
184
|
});
|
|
150
185
|
});
|
|
151
186
|
|
package/src/report/render.ts
CHANGED
|
@@ -193,13 +193,18 @@ export async function openReport(options: ReportRenderOptions): Promise<void> {
|
|
|
193
193
|
// The route serves some formats as types a browser downloads rather than
|
|
194
194
|
// displays; the format's own media type is what decides how it opens.
|
|
195
195
|
let url = URL.createObjectURL(blob.type ? blob : new Blob([blob], { type: getReportMediaType(options.format) }));
|
|
196
|
-
|
|
196
|
+
// Not opened with the `noopener` feature: `open` then returns null even when
|
|
197
|
+
// the tab opens, which read as a blocked pop-up and revoked the URL before
|
|
198
|
+
// the tab could load it. The opener is cut off by hand instead.
|
|
199
|
+
let opened = globalThis.open(url, "_blank");
|
|
197
200
|
|
|
198
201
|
if (!opened) {
|
|
199
202
|
URL.revokeObjectURL(url);
|
|
200
203
|
throw new Error("The report could not be opened. Allow pop-ups for this site and try again.");
|
|
201
204
|
}
|
|
202
205
|
|
|
206
|
+
opened.opener = null;
|
|
207
|
+
|
|
203
208
|
// Revoking immediately would cancel the load in some browsers, and never
|
|
204
209
|
// revoking keeps the whole rendered report in memory.
|
|
205
210
|
globalThis.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|