@allurereport/web-commons 3.0.0 → 3.1.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/attachments.d.ts +27 -14
- package/dist/attachments.js +20 -45
- package/dist/data.d.ts +5 -1
- package/dist/data.js +8 -2
- package/dist/filters/builders.d.ts +4 -0
- package/dist/filters/builders.js +160 -0
- package/dist/filters/index.d.ts +3 -0
- package/dist/filters/index.js +2 -0
- package/dist/filters/model.d.ts +39 -0
- package/dist/filters/model.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/stores/loadableStore/constants.d.ts +1 -0
- package/dist/stores/loadableStore/constants.js +1 -0
- package/dist/stores/loadableStore/index.d.ts +3 -0
- package/dist/stores/loadableStore/index.js +2 -0
- package/dist/stores/loadableStore/store.d.ts +13 -0
- package/dist/stores/loadableStore/store.js +47 -0
- package/dist/stores/loadableStore/types.d.ts +7 -0
- package/dist/stores/loadableStore/types.js +1 -0
- package/dist/stores/loadableStore/utils.d.ts +2 -0
- package/dist/stores/loadableStore/utils.js +7 -0
- package/dist/stores/persister/index.d.ts +12 -0
- package/dist/stores/persister/index.js +36 -0
- package/dist/stores/router/index.d.ts +18 -0
- package/dist/stores/router/index.js +95 -0
- package/dist/stores/theme/actions.d.ts +3 -0
- package/dist/stores/theme/actions.js +30 -0
- package/dist/stores/theme/constants.d.ts +6 -0
- package/dist/stores/theme/constants.js +5 -0
- package/dist/stores/theme/index.d.ts +2 -0
- package/dist/stores/theme/index.js +2 -0
- package/dist/stores/theme/store.d.ts +8 -0
- package/dist/stores/theme/store.js +41 -0
- package/dist/stores/theme/types.d.ts +2 -0
- package/dist/stores/theme/types.js +1 -0
- package/dist/stores/theme/utils.d.ts +4 -0
- package/dist/stores/theme/utils.js +23 -0
- package/dist/stores/url/helpers.d.ts +12 -0
- package/dist/stores/url/helpers.js +74 -0
- package/dist/stores/url/index.d.ts +2 -0
- package/dist/stores/url/index.js +2 -0
- package/dist/stores/url/store.d.ts +19 -0
- package/dist/stores/url/store.js +45 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +9 -0
- package/package.json +10 -5
package/dist/attachments.d.ts
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
type AttachmentImageData = {
|
|
2
|
+
img: string;
|
|
3
|
+
id?: string;
|
|
4
|
+
};
|
|
5
|
+
type AttachmentTextData = {
|
|
6
|
+
text: string;
|
|
7
|
+
};
|
|
8
|
+
type AttachmentVideoData = {
|
|
9
|
+
src: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
contentType?: string;
|
|
12
|
+
};
|
|
13
|
+
type AttachmentImageDiffData = {
|
|
14
|
+
diff: {
|
|
15
|
+
actual?: string;
|
|
16
|
+
expected?: string;
|
|
17
|
+
diff?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type AttachmentData = AttachmentImageData | AttachmentTextData | AttachmentVideoData | AttachmentImageDiffData;
|
|
21
|
+
type AttachmentPayload = {
|
|
2
22
|
id?: string;
|
|
3
23
|
ext?: string;
|
|
4
24
|
contentType?: string;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
export declare const fetchFromUrl: ({ id, ext, contentType }: Attachments) => Promise<Response>;
|
|
10
|
-
export declare const fetchAttachment: (id: string, ext: string, contentType?: string) => Promise<Attachments | null>;
|
|
25
|
+
};
|
|
26
|
+
export declare const fetchFromUrl: ({ id, ext, contentType }: AttachmentPayload) => Promise<Response>;
|
|
27
|
+
export declare const fetchAttachment: (id: string, ext: string, contentType?: string) => Promise<AttachmentData | null>;
|
|
11
28
|
export declare const blobAttachment: (id: string, ext: string, contentType: string) => Promise<Blob>;
|
|
12
29
|
export declare const downloadAttachment: (id: string, ext: string, contentType: string) => Promise<void>;
|
|
13
30
|
export declare const openAttachmentInNewTab: (id: string, ext: string, contentType: string) => Promise<void>;
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
icon: string;
|
|
17
|
-
} | {
|
|
18
|
-
type: null;
|
|
19
|
-
icon: string;
|
|
20
|
-
};
|
|
31
|
+
export type AttachmentType = "css" | "json" | "image" | "svg" | "code" | "text" | "html" | "table" | "video" | "uri" | "archive" | "image-diff";
|
|
32
|
+
export declare const attachmentType: (type?: string) => AttachmentType | null;
|
|
21
33
|
export declare const restrictedContentTypes: string[];
|
|
34
|
+
export {};
|
package/dist/attachments.js
CHANGED
|
@@ -9,7 +9,10 @@ export const fetchAttachment = async (id, ext, contentType) => {
|
|
|
9
9
|
}
|
|
10
10
|
const response = await fetchFromUrl({ id, ext, contentType });
|
|
11
11
|
const fileType = attachmentType(contentType);
|
|
12
|
-
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error("Failed to fetch");
|
|
14
|
+
}
|
|
15
|
+
switch (fileType) {
|
|
13
16
|
case "svg":
|
|
14
17
|
case "image": {
|
|
15
18
|
const blob = await response.blob();
|
|
@@ -29,6 +32,10 @@ export const fetchAttachment = async (id, ext, contentType) => {
|
|
|
29
32
|
const src = URL.createObjectURL(blob);
|
|
30
33
|
return { src, id, contentType };
|
|
31
34
|
}
|
|
35
|
+
case "image-diff": {
|
|
36
|
+
const json = await response.json();
|
|
37
|
+
return { diff: json };
|
|
38
|
+
}
|
|
32
39
|
default:
|
|
33
40
|
return null;
|
|
34
41
|
}
|
|
@@ -67,10 +74,7 @@ export const attachmentType = (type) => {
|
|
|
67
74
|
case "image/jpg":
|
|
68
75
|
case "image/png":
|
|
69
76
|
case "image/*":
|
|
70
|
-
return
|
|
71
|
-
type: "image",
|
|
72
|
-
icon: "file",
|
|
73
|
-
};
|
|
77
|
+
return "image";
|
|
74
78
|
case "text/xml":
|
|
75
79
|
case "text/json":
|
|
76
80
|
case "text/yaml":
|
|
@@ -100,63 +104,34 @@ export const attachmentType = (type) => {
|
|
|
100
104
|
case "application/x-yaml":
|
|
101
105
|
case "application/xml":
|
|
102
106
|
case "application/json":
|
|
103
|
-
return
|
|
104
|
-
type: "code",
|
|
105
|
-
icon: "file",
|
|
106
|
-
};
|
|
107
|
+
return "code";
|
|
107
108
|
case "text/plain":
|
|
108
109
|
case "text/markdown":
|
|
109
110
|
case "text/*":
|
|
110
|
-
return
|
|
111
|
-
type: "text",
|
|
112
|
-
icon: "txt",
|
|
113
|
-
};
|
|
111
|
+
return "text";
|
|
114
112
|
case "text/html":
|
|
115
|
-
return
|
|
116
|
-
type: "html",
|
|
117
|
-
icon: "file",
|
|
118
|
-
};
|
|
113
|
+
return "html";
|
|
119
114
|
case "text/csv":
|
|
120
|
-
return {
|
|
121
|
-
type: "table",
|
|
122
|
-
icon: "csv",
|
|
123
|
-
};
|
|
124
115
|
case "text/tab-separated-values":
|
|
125
|
-
return
|
|
126
|
-
type: "table",
|
|
127
|
-
icon: "table",
|
|
128
|
-
};
|
|
116
|
+
return "table";
|
|
129
117
|
case "image/svg+xml":
|
|
130
|
-
return
|
|
131
|
-
type: "svg",
|
|
132
|
-
icon: "file",
|
|
133
|
-
};
|
|
118
|
+
return "svg";
|
|
134
119
|
case "video/mp4":
|
|
135
120
|
case "video/ogg":
|
|
136
121
|
case "video/webm":
|
|
137
|
-
return
|
|
138
|
-
type: "video",
|
|
139
|
-
icon: "file",
|
|
140
|
-
};
|
|
122
|
+
return "video";
|
|
141
123
|
case "text/uri-list":
|
|
142
|
-
return
|
|
143
|
-
type: "uri",
|
|
144
|
-
icon: "list",
|
|
145
|
-
};
|
|
124
|
+
return "uri";
|
|
146
125
|
case "application/x-tar":
|
|
147
126
|
case "application/x-gtar":
|
|
148
127
|
case "application/x-bzip2":
|
|
149
128
|
case "application/gzip":
|
|
150
129
|
case "application/zip":
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
130
|
+
return "archive";
|
|
131
|
+
case "application/vnd.allure.image.diff":
|
|
132
|
+
return "image-diff";
|
|
155
133
|
default:
|
|
156
|
-
return
|
|
157
|
-
type: null,
|
|
158
|
-
icon: "file",
|
|
159
|
-
};
|
|
134
|
+
return null;
|
|
160
135
|
}
|
|
161
136
|
};
|
|
162
137
|
export const restrictedContentTypes = ["application/gzip"];
|
package/dist/data.d.ts
CHANGED
|
@@ -4,8 +4,12 @@ export declare const loadReportData: (name: string) => Promise<string>;
|
|
|
4
4
|
export declare const reportDataUrl: (path: string, contentType?: string, params?: {
|
|
5
5
|
bustCache: boolean;
|
|
6
6
|
}) => Promise<string>;
|
|
7
|
+
export declare class ReportFetchError extends Error {
|
|
8
|
+
readonly response: Response;
|
|
9
|
+
constructor(message: string, response: Response);
|
|
10
|
+
}
|
|
7
11
|
export declare const fetchReportJsonData: <T>(path: string, params?: {
|
|
8
12
|
bustCache: boolean;
|
|
9
13
|
}) => Promise<T>;
|
|
10
14
|
export declare const fetchReportAttachment: (path: string, contentType?: string) => Promise<Response>;
|
|
11
|
-
export declare const getReportOptions: <T>() => T
|
|
15
|
+
export declare const getReportOptions: <T>() => Readonly<T>;
|
package/dist/data.js
CHANGED
|
@@ -28,7 +28,7 @@ export const reportDataUrl = async (path, contentType = "application/octet-strea
|
|
|
28
28
|
const baseEl = globalThis.document.head.querySelector("base")?.href ?? "https://localhost";
|
|
29
29
|
const url = new URL(path, baseEl);
|
|
30
30
|
const liveReloadHash = globalThis.localStorage.getItem(ALLURE_LIVE_RELOAD_HASH_STORAGE_KEY);
|
|
31
|
-
const cacheKey =
|
|
31
|
+
const cacheKey = getReportOptions()?.cacheKey;
|
|
32
32
|
if (liveReloadHash) {
|
|
33
33
|
url.searchParams.set("live_reload_hash", liveReloadHash);
|
|
34
34
|
}
|
|
@@ -37,11 +37,17 @@ export const reportDataUrl = async (path, contentType = "application/octet-strea
|
|
|
37
37
|
}
|
|
38
38
|
return url.toString();
|
|
39
39
|
};
|
|
40
|
+
export class ReportFetchError extends Error {
|
|
41
|
+
constructor(message, response) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.response = response;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
40
46
|
export const fetchReportJsonData = async (path, params) => {
|
|
41
47
|
const url = await reportDataUrl(path, undefined, params);
|
|
42
48
|
const res = await globalThis.fetch(url);
|
|
43
49
|
if (!res.ok) {
|
|
44
|
-
throw new
|
|
50
|
+
throw new ReportFetchError(`Failed to fetch ${url}, response status: ${res.status}`, res);
|
|
45
51
|
}
|
|
46
52
|
const data = res.json();
|
|
47
53
|
return data;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AqlExpression } from "@allurereport/aql";
|
|
2
|
+
import { type Filter } from "./model.js";
|
|
3
|
+
export declare const buildFieldFilters: <T extends string = string>(filters: Filter<T>[]) => AqlExpression;
|
|
4
|
+
export declare const buildFilterPredicate: <Keys extends string = string>(filters: Filter<Keys>[]) => (item: Record<Keys, any>) => boolean;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { aqlArrayConditionExpression, aqlBinaryExpression, aqlConditionExpression, aqlParenExpression, createAqlPredicate, } from "@allurereport/aql";
|
|
2
|
+
import { MAX_ARRAY_FIELD_VALUES, } from "./model.js";
|
|
3
|
+
const buildAqlFromFieldFilter = (field) => {
|
|
4
|
+
const { value: fieldValue } = field;
|
|
5
|
+
const { key, type, strict, value } = fieldValue;
|
|
6
|
+
if (type === "array") {
|
|
7
|
+
if (value.length === 0) {
|
|
8
|
+
throw new Error("ArrayField value cannot be empty");
|
|
9
|
+
}
|
|
10
|
+
if (strict === false) {
|
|
11
|
+
return buildArrayIntersectionFilter(key, value, MAX_ARRAY_FIELD_VALUES);
|
|
12
|
+
}
|
|
13
|
+
return aqlArrayConditionExpression({
|
|
14
|
+
type: "arrayCondition",
|
|
15
|
+
left: {
|
|
16
|
+
identifier: key,
|
|
17
|
+
},
|
|
18
|
+
operator: "IN",
|
|
19
|
+
right: value.map((item) => ({
|
|
20
|
+
value: item,
|
|
21
|
+
type: "STRING",
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
let expressionValue;
|
|
26
|
+
let valueType;
|
|
27
|
+
let operator = "EQ";
|
|
28
|
+
switch (type) {
|
|
29
|
+
case "number": {
|
|
30
|
+
expressionValue = String(value);
|
|
31
|
+
operator = (strict ?? true) ? "EQ" : "CONTAINS";
|
|
32
|
+
valueType = "NUMBER";
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "boolean": {
|
|
36
|
+
expressionValue = value ? "true" : "false";
|
|
37
|
+
valueType = "BOOLEAN";
|
|
38
|
+
operator = "EQ";
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "string": {
|
|
42
|
+
expressionValue = String(value);
|
|
43
|
+
operator = (strict ?? true) ? "EQ" : "CONTAINS";
|
|
44
|
+
valueType = "STRING";
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
default: {
|
|
48
|
+
const exhaustiveCheck = type;
|
|
49
|
+
throw new Error(`Unsupported field type: ${String(exhaustiveCheck)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return aqlConditionExpression({
|
|
53
|
+
type: "condition",
|
|
54
|
+
left: {
|
|
55
|
+
identifier: key,
|
|
56
|
+
},
|
|
57
|
+
operator,
|
|
58
|
+
right: {
|
|
59
|
+
value: expressionValue,
|
|
60
|
+
type: valueType,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
export const buildFieldFilters = (filters) => {
|
|
65
|
+
if (filters.length === 0) {
|
|
66
|
+
throw new Error("chainFieldFilters: filters array cannot be empty");
|
|
67
|
+
}
|
|
68
|
+
const buildAqlFromFilterGroup = (group) => {
|
|
69
|
+
const { value } = group;
|
|
70
|
+
if (value.length === 0) {
|
|
71
|
+
throw new Error("buildFieldFilters: value array cannot be empty");
|
|
72
|
+
}
|
|
73
|
+
if (value.length === 1) {
|
|
74
|
+
return buildFieldFilters([value[0]]);
|
|
75
|
+
}
|
|
76
|
+
return aqlParenExpression({
|
|
77
|
+
type: "paren",
|
|
78
|
+
expression: buildFieldFilters(value),
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
if (filters.length === 1) {
|
|
82
|
+
const [filter] = filters;
|
|
83
|
+
if (filter.type === "field") {
|
|
84
|
+
return buildAqlFromFieldFilter(filter);
|
|
85
|
+
}
|
|
86
|
+
return buildAqlFromFilterGroup(filter);
|
|
87
|
+
}
|
|
88
|
+
const [first, second, ...rest] = filters;
|
|
89
|
+
if (rest.length === 0) {
|
|
90
|
+
return aqlBinaryExpression({
|
|
91
|
+
type: "binary",
|
|
92
|
+
operator: first.logicalOperator ?? "AND",
|
|
93
|
+
left: buildFieldFilters([first]),
|
|
94
|
+
right: buildFieldFilters([second]),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return rest.reduce((acc, filter) => {
|
|
98
|
+
return aqlBinaryExpression({
|
|
99
|
+
type: "binary",
|
|
100
|
+
operator: filter.logicalOperator ?? "AND",
|
|
101
|
+
left: acc,
|
|
102
|
+
right: buildFieldFilters([filter]),
|
|
103
|
+
});
|
|
104
|
+
}, buildFieldFilters([first, second]));
|
|
105
|
+
};
|
|
106
|
+
const buildArrayIntersectionFilter = (key, values, maxIndex = 20) => {
|
|
107
|
+
if (values.length === 0) {
|
|
108
|
+
throw new Error("buildArrayIntersectionFilter: values array cannot be empty");
|
|
109
|
+
}
|
|
110
|
+
const conditionsPerValue = values.map((value) => {
|
|
111
|
+
const indexConditions = Array.from({ length: maxIndex + 1 }, (_, index) => {
|
|
112
|
+
return aqlConditionExpression({
|
|
113
|
+
type: "condition",
|
|
114
|
+
left: {
|
|
115
|
+
identifier: key,
|
|
116
|
+
param: {
|
|
117
|
+
value: index,
|
|
118
|
+
type: "number",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
operator: "EQ",
|
|
122
|
+
right: {
|
|
123
|
+
value,
|
|
124
|
+
type: "STRING",
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
if (indexConditions.length === 1) {
|
|
129
|
+
return indexConditions[0];
|
|
130
|
+
}
|
|
131
|
+
return indexConditions.reduce((acc, condition, index) => {
|
|
132
|
+
if (index === 0) {
|
|
133
|
+
return condition;
|
|
134
|
+
}
|
|
135
|
+
return aqlBinaryExpression({
|
|
136
|
+
type: "binary",
|
|
137
|
+
operator: "OR",
|
|
138
|
+
left: acc,
|
|
139
|
+
right: condition,
|
|
140
|
+
});
|
|
141
|
+
}, indexConditions[0]);
|
|
142
|
+
});
|
|
143
|
+
if (conditionsPerValue.length === 1) {
|
|
144
|
+
return conditionsPerValue[0];
|
|
145
|
+
}
|
|
146
|
+
return conditionsPerValue.reduce((acc, condition, index) => {
|
|
147
|
+
if (index === 0) {
|
|
148
|
+
return condition;
|
|
149
|
+
}
|
|
150
|
+
return aqlBinaryExpression({
|
|
151
|
+
type: "binary",
|
|
152
|
+
operator: "OR",
|
|
153
|
+
left: acc,
|
|
154
|
+
right: condition,
|
|
155
|
+
});
|
|
156
|
+
}, conditionsPerValue[0]);
|
|
157
|
+
};
|
|
158
|
+
export const buildFilterPredicate = (filters) => {
|
|
159
|
+
return createAqlPredicate(buildFieldFilters(filters));
|
|
160
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type FieldFilter<T extends string = string> = {
|
|
2
|
+
type: "field";
|
|
3
|
+
value: Field<T>;
|
|
4
|
+
logicalOperator?: LogicalOperator;
|
|
5
|
+
};
|
|
6
|
+
export type FieldFilterGroup<T extends string = string> = {
|
|
7
|
+
type: "group";
|
|
8
|
+
value: Filter<T>[];
|
|
9
|
+
logicalOperator?: LogicalOperator;
|
|
10
|
+
};
|
|
11
|
+
export type Filter<T extends string = string> = FieldFilter<T> | FieldFilterGroup<T>;
|
|
12
|
+
export type StringField<T extends string = string> = {
|
|
13
|
+
key: T;
|
|
14
|
+
value: string;
|
|
15
|
+
strict?: boolean;
|
|
16
|
+
type: "string";
|
|
17
|
+
};
|
|
18
|
+
export type NumberField<T extends string = string> = {
|
|
19
|
+
key: T;
|
|
20
|
+
value: number;
|
|
21
|
+
strict?: boolean;
|
|
22
|
+
type: "number";
|
|
23
|
+
};
|
|
24
|
+
export type BooleanField<T extends string = string> = {
|
|
25
|
+
key: T;
|
|
26
|
+
value: boolean;
|
|
27
|
+
strict?: never;
|
|
28
|
+
type: "boolean";
|
|
29
|
+
};
|
|
30
|
+
export type ArrayField<T extends string = string> = {
|
|
31
|
+
key: T;
|
|
32
|
+
value: string[];
|
|
33
|
+
strict?: boolean;
|
|
34
|
+
type: "array";
|
|
35
|
+
};
|
|
36
|
+
export type Field<T extends string = string> = StringField<T> | NumberField<T> | BooleanField<T> | ArrayField<T>;
|
|
37
|
+
export type LogicalOperator = "AND" | "OR";
|
|
38
|
+
export type AqlValueType = "STRING" | "NUMBER" | "BOOLEAN";
|
|
39
|
+
export declare const MAX_ARRAY_FIELD_VALUES = 20;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_ARRAY_FIELD_VALUES = 20;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,10 @@ export * from "./i18n.js";
|
|
|
4
4
|
export * from "./charts/index.js";
|
|
5
5
|
export * from "./strings.js";
|
|
6
6
|
export * from "./sanitize.js";
|
|
7
|
+
export * from "./stores/url/index.js";
|
|
8
|
+
export * from "./stores/router/index.js";
|
|
9
|
+
export * from "./filters/index.js";
|
|
10
|
+
export * from "./stores/theme/index.js";
|
|
11
|
+
export * from "./stores/loadableStore/index.js";
|
|
12
|
+
export * from "./stores/persister/index.js";
|
|
13
|
+
export * from "./utils.js";
|
package/dist/index.js
CHANGED
|
@@ -4,3 +4,10 @@ export * from "./i18n.js";
|
|
|
4
4
|
export * from "./charts/index.js";
|
|
5
5
|
export * from "./strings.js";
|
|
6
6
|
export * from "./sanitize.js";
|
|
7
|
+
export * from "./stores/url/index.js";
|
|
8
|
+
export * from "./stores/router/index.js";
|
|
9
|
+
export * from "./filters/index.js";
|
|
10
|
+
export * from "./stores/theme/index.js";
|
|
11
|
+
export * from "./stores/loadableStore/index.js";
|
|
12
|
+
export * from "./stores/persister/index.js";
|
|
13
|
+
export * from "./utils.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const LOADABLE_STORE_BRAND: unique symbol;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LOADABLE_STORE_BRAND = Symbol("loadableStore");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LOADABLE_STORE_BRAND } from "./constants.js";
|
|
2
|
+
import type { LoadableStoreValue } from "./types.js";
|
|
3
|
+
export type LoadableStore<T> = ReturnType<typeof loadableStore<T>>;
|
|
4
|
+
export declare const loadableStore: <T>(options: {
|
|
5
|
+
initialState: T;
|
|
6
|
+
}) => {
|
|
7
|
+
readonly value: LoadableStoreValue<T>;
|
|
8
|
+
readonly onInit: () => void;
|
|
9
|
+
readonly onLoad: (silent?: boolean) => void;
|
|
10
|
+
readonly onSuccess: (data: T) => void;
|
|
11
|
+
readonly onError: (error?: Error) => void;
|
|
12
|
+
readonly brand: typeof LOADABLE_STORE_BRAND;
|
|
13
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { batch, computed, signal } from "@preact/signals-core";
|
|
2
|
+
import { LOADABLE_STORE_BRAND } from "./constants.js";
|
|
3
|
+
export const loadableStore = (options) => {
|
|
4
|
+
const loadingSignal = signal(false);
|
|
5
|
+
const errorSignal = signal(undefined);
|
|
6
|
+
const dataSignal = signal(options.initialState);
|
|
7
|
+
return {
|
|
8
|
+
value: {
|
|
9
|
+
data: computed(() => dataSignal.value),
|
|
10
|
+
loading: computed(() => loadingSignal.value),
|
|
11
|
+
error: computed(() => errorSignal.value),
|
|
12
|
+
errorMessage: computed(() => {
|
|
13
|
+
if (errorSignal.value) {
|
|
14
|
+
return errorSignal.value.message;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
onInit: () => {
|
|
20
|
+
batch(() => {
|
|
21
|
+
loadingSignal.value = false;
|
|
22
|
+
errorSignal.value = undefined;
|
|
23
|
+
dataSignal.value = options.initialState;
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
onLoad: (silent = false) => {
|
|
27
|
+
batch(() => {
|
|
28
|
+
loadingSignal.value = !silent;
|
|
29
|
+
errorSignal.value = undefined;
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
onSuccess: (data) => {
|
|
33
|
+
batch(() => {
|
|
34
|
+
loadingSignal.value = false;
|
|
35
|
+
errorSignal.value = undefined;
|
|
36
|
+
dataSignal.value = data;
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
onError: (error = new Error("Unknown error")) => {
|
|
40
|
+
batch(() => {
|
|
41
|
+
loadingSignal.value = false;
|
|
42
|
+
errorSignal.value = error;
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
brand: LOADABLE_STORE_BRAND,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Signal } from "@preact/signals-core";
|
|
2
|
+
export declare const persistSignal: <S extends Signal<unknown>>(options: {
|
|
3
|
+
signal: S;
|
|
4
|
+
key: string;
|
|
5
|
+
shouldPersist?: (v: S extends Signal<infer U> ? U : never) => boolean;
|
|
6
|
+
}) => (() => void) | undefined;
|
|
7
|
+
export declare const restoreSignal: <S extends Signal<unknown>, V = S extends Signal<infer U> ? U : unknown>(options: {
|
|
8
|
+
signal: S;
|
|
9
|
+
key: string;
|
|
10
|
+
defaultValue?: V;
|
|
11
|
+
onRestore?: (value: any) => V | undefined;
|
|
12
|
+
}) => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { effect } from "@preact/signals-core";
|
|
2
|
+
export const persistSignal = (options) => {
|
|
3
|
+
if (typeof window === "undefined") {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const { signal, key, shouldPersist = () => true } = options;
|
|
7
|
+
return effect(() => {
|
|
8
|
+
const value = signal.value;
|
|
9
|
+
if (!shouldPersist(value)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
window.localStorage.setItem(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
export const restoreSignal = (options) => {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const { signal, key, defaultValue, onRestore = (v) => v } = options;
|
|
23
|
+
const value = window.localStorage.getItem(key);
|
|
24
|
+
if (value !== null) {
|
|
25
|
+
let parsedValue = value;
|
|
26
|
+
try {
|
|
27
|
+
parsedValue = JSON.parse(value);
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
signal.value = onRestore(parsedValue);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if ("defaultValue" in options) {
|
|
34
|
+
signal.value = defaultValue;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const router: import("@preact/signals-core").ReadonlySignal<{
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly pathParts: string[];
|
|
4
|
+
}>;
|
|
5
|
+
type NavigateTo = {
|
|
6
|
+
path: string;
|
|
7
|
+
keepSearchParams?: boolean;
|
|
8
|
+
searchParams?: Record<string, string | string[] | number | undefined>;
|
|
9
|
+
params?: Record<string, string | undefined>;
|
|
10
|
+
replace?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare const navigateTo: (to: NavigateTo) => void;
|
|
13
|
+
type Route<Params extends Record<string, string | undefined>> = {
|
|
14
|
+
matches: boolean;
|
|
15
|
+
params: Params;
|
|
16
|
+
};
|
|
17
|
+
export declare const createRoute: <Params extends Record<string, string | undefined>>(path: string, validate?: (route: Route<Params>) => boolean) => Route<Params>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { computed } from "@preact/signals-core";
|
|
2
|
+
import { paramsToSearchParams } from "../url/helpers.js";
|
|
3
|
+
import { currentUrl, goTo } from "../url/index.js";
|
|
4
|
+
export const router = computed(() => {
|
|
5
|
+
const hash = currentUrl.value.hash.startsWith("#") ? currentUrl.value.hash.slice(1) : currentUrl.value.hash;
|
|
6
|
+
return {
|
|
7
|
+
path: hash,
|
|
8
|
+
pathParts: hash.split("/").filter(Boolean),
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
const createRouteUrl = (path, params) => {
|
|
12
|
+
return path
|
|
13
|
+
.split("/")
|
|
14
|
+
.map((part) => {
|
|
15
|
+
if (part.startsWith(":")) {
|
|
16
|
+
const isOptional = part.endsWith("?");
|
|
17
|
+
const paramKey = isOptional ? part.slice(1, -1) : part.slice(1);
|
|
18
|
+
const value = params[paramKey];
|
|
19
|
+
if (value) {
|
|
20
|
+
return value.toString();
|
|
21
|
+
}
|
|
22
|
+
if (isOptional) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return part;
|
|
26
|
+
}
|
|
27
|
+
return part;
|
|
28
|
+
})
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join("/");
|
|
31
|
+
};
|
|
32
|
+
export const navigateTo = (to) => {
|
|
33
|
+
const { path, params = {}, replace = false, searchParams = {}, keepSearchParams = false } = to;
|
|
34
|
+
const currentPathname = currentUrl.value.pathname;
|
|
35
|
+
const newUrl = new URL(currentPathname, currentUrl.value.origin);
|
|
36
|
+
const routeUrl = createRouteUrl(path, params);
|
|
37
|
+
newUrl.hash = routeUrl === "" || routeUrl === "/" ? "" : `#${routeUrl}`;
|
|
38
|
+
if (keepSearchParams) {
|
|
39
|
+
paramsToSearchParams(currentUrl.value.params, newUrl.searchParams);
|
|
40
|
+
}
|
|
41
|
+
Object.entries(searchParams).forEach(([key, value]) => {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
for (const v of value) {
|
|
47
|
+
newUrl.searchParams.set(key, v.toString());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
newUrl.searchParams.set(key, value.toString());
|
|
51
|
+
});
|
|
52
|
+
goTo(newUrl, { replace });
|
|
53
|
+
};
|
|
54
|
+
export const createRoute = (path, validate = () => true) => {
|
|
55
|
+
if (path === "/" && router.value.pathParts.length === 0) {
|
|
56
|
+
return { matches: true, params: {} };
|
|
57
|
+
}
|
|
58
|
+
const routeParts = path.split("/").filter(Boolean);
|
|
59
|
+
const currentParts = router.value.pathParts;
|
|
60
|
+
const params = {};
|
|
61
|
+
let routeIndex = 0;
|
|
62
|
+
let currentIndex = 0;
|
|
63
|
+
while (routeIndex < routeParts.length && currentIndex < currentParts.length) {
|
|
64
|
+
const routePart = routeParts[routeIndex];
|
|
65
|
+
const currentPart = currentParts[currentIndex];
|
|
66
|
+
if (routePart.startsWith(":")) {
|
|
67
|
+
const isOptional = routePart.endsWith("?");
|
|
68
|
+
const paramKey = isOptional ? routePart.slice(1, -1) : routePart.slice(1);
|
|
69
|
+
params[paramKey] = currentPart;
|
|
70
|
+
routeIndex++;
|
|
71
|
+
currentIndex++;
|
|
72
|
+
}
|
|
73
|
+
else if (routePart === currentPart) {
|
|
74
|
+
routeIndex++;
|
|
75
|
+
currentIndex++;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return { matches: false, params: {} };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
while (routeIndex < routeParts.length) {
|
|
82
|
+
const routePart = routeParts[routeIndex];
|
|
83
|
+
if (routePart.startsWith(":") && routePart.endsWith("?")) {
|
|
84
|
+
const paramKey = routePart.slice(1, -1);
|
|
85
|
+
params[paramKey] = undefined;
|
|
86
|
+
routeIndex++;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return { matches: false, params: {} };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const matches = currentIndex === currentParts.length;
|
|
93
|
+
const route = { matches, params };
|
|
94
|
+
return { matches: matches && validate(route), params };
|
|
95
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { effect } from "@preact/signals-core";
|
|
2
|
+
import { SELECTED_THEMES, THEME_DARK, THEME_LIGHT } from "./constants.js";
|
|
3
|
+
import { currentTheme, preferredTheme, userTheme } from "./store.js";
|
|
4
|
+
import { getPrefersColorSchemeMQ } from "./utils.js";
|
|
5
|
+
const initThemeStore = () => {
|
|
6
|
+
if (typeof window === "undefined") {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
effect(() => {
|
|
10
|
+
document.documentElement.setAttribute("data-theme", currentTheme.value);
|
|
11
|
+
});
|
|
12
|
+
const preffersMediaQuery = getPrefersColorSchemeMQ();
|
|
13
|
+
preffersMediaQuery.addEventListener("change", (event) => {
|
|
14
|
+
if (event.matches) {
|
|
15
|
+
preferredTheme.value = THEME_DARK;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
preferredTheme.value = THEME_LIGHT;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
export const setUserTheme = (theme) => {
|
|
23
|
+
userTheme.value = theme;
|
|
24
|
+
};
|
|
25
|
+
export const toggleUserTheme = () => {
|
|
26
|
+
const current = userTheme.peek();
|
|
27
|
+
const next = SELECTED_THEMES[(SELECTED_THEMES.indexOf(current) + 1) % SELECTED_THEMES.length];
|
|
28
|
+
setUserTheme(next);
|
|
29
|
+
};
|
|
30
|
+
initThemeStore();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { UserTheme } from "./types.js";
|
|
2
|
+
export declare const THEME_AUTO = "auto";
|
|
3
|
+
export declare const THEME_LIGHT = "light";
|
|
4
|
+
export declare const THEME_DARK = "dark";
|
|
5
|
+
export declare const STORAGE_KEY = "theme";
|
|
6
|
+
export declare const SELECTED_THEMES: UserTheme[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UITheme, UserTheme } from "./types.js";
|
|
2
|
+
export declare const currentTheme: import("@preact/signals-core").ReadonlySignal<UITheme>;
|
|
3
|
+
export declare const userTheme: import("@preact/signals-core").Signal<UserTheme>;
|
|
4
|
+
export declare const preferredTheme: import("@preact/signals-core").Signal<UITheme>;
|
|
5
|
+
export declare const themeStore: import("@preact/signals-core").ReadonlySignal<{
|
|
6
|
+
readonly current: UITheme;
|
|
7
|
+
readonly selected: UserTheme;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { computed, signal } from "@preact/signals-core";
|
|
2
|
+
import { getReportOptions } from "../../data.js";
|
|
3
|
+
import { persistSignal, restoreSignal } from "../persister/index.js";
|
|
4
|
+
import { STORAGE_KEY, THEME_AUTO, THEME_DARK, THEME_LIGHT } from "./constants.js";
|
|
5
|
+
import { getPrefersColorSchemeMQ, isAcceptedThemeValue, isAutoTheme } from "./utils.js";
|
|
6
|
+
const reportConfigTheme = getReportOptions()?.theme;
|
|
7
|
+
const getInitialPreferredTheme = () => {
|
|
8
|
+
if (typeof window === "undefined") {
|
|
9
|
+
return THEME_LIGHT;
|
|
10
|
+
}
|
|
11
|
+
if (getPrefersColorSchemeMQ().matches) {
|
|
12
|
+
return THEME_DARK;
|
|
13
|
+
}
|
|
14
|
+
return THEME_LIGHT;
|
|
15
|
+
};
|
|
16
|
+
export const currentTheme = computed(() => {
|
|
17
|
+
if (isAutoTheme(userTheme.value)) {
|
|
18
|
+
return preferredTheme.value;
|
|
19
|
+
}
|
|
20
|
+
return userTheme.value;
|
|
21
|
+
});
|
|
22
|
+
export const userTheme = signal(THEME_AUTO);
|
|
23
|
+
export const preferredTheme = signal(getInitialPreferredTheme());
|
|
24
|
+
restoreSignal({
|
|
25
|
+
signal: userTheme,
|
|
26
|
+
key: STORAGE_KEY,
|
|
27
|
+
onRestore: (value) => {
|
|
28
|
+
if (isAcceptedThemeValue(value)) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return reportConfigTheme ?? THEME_AUTO;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
persistSignal({
|
|
35
|
+
signal: userTheme,
|
|
36
|
+
key: STORAGE_KEY,
|
|
37
|
+
});
|
|
38
|
+
export const themeStore = computed(() => ({
|
|
39
|
+
current: currentTheme.value,
|
|
40
|
+
selected: userTheme.value,
|
|
41
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { UserTheme } from "./types.js";
|
|
2
|
+
export declare const getPrefersColorSchemeMQ: () => MediaQueryList;
|
|
3
|
+
export declare const isAcceptedThemeValue: (value: unknown) => value is UserTheme;
|
|
4
|
+
export declare const isAutoTheme: (theme: UserTheme) => theme is "auto";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SELECTED_THEMES, THEME_AUTO } from "./constants.js";
|
|
2
|
+
const nullMediaQueryList = {
|
|
3
|
+
matches: false,
|
|
4
|
+
addEventListener: () => { },
|
|
5
|
+
removeEventListener: () => { },
|
|
6
|
+
media: "",
|
|
7
|
+
onchange: () => { },
|
|
8
|
+
addListener: () => { },
|
|
9
|
+
removeListener: () => { },
|
|
10
|
+
dispatchEvent: () => true,
|
|
11
|
+
};
|
|
12
|
+
export const getPrefersColorSchemeMQ = () => {
|
|
13
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
14
|
+
return nullMediaQueryList;
|
|
15
|
+
}
|
|
16
|
+
return window.matchMedia("(prefers-color-scheme: dark)");
|
|
17
|
+
};
|
|
18
|
+
export const isAcceptedThemeValue = (value) => {
|
|
19
|
+
return SELECTED_THEMES.includes(value);
|
|
20
|
+
};
|
|
21
|
+
export const isAutoTheme = (theme) => {
|
|
22
|
+
return theme === THEME_AUTO;
|
|
23
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const subscribeToUrlChange: (callback: () => void) => (() => void) | undefined;
|
|
2
|
+
type NavigateTo = URL | string | {
|
|
3
|
+
path: string;
|
|
4
|
+
};
|
|
5
|
+
type NavigateToOptions = {
|
|
6
|
+
replace?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const goTo: (to: NavigateTo, options?: NavigateToOptions) => void;
|
|
9
|
+
export declare const getCurrentUrl: () => string;
|
|
10
|
+
export declare const paramsToSearchParams: (params: Record<string, string | string[]>, searchParams?: URLSearchParams) => URLSearchParams;
|
|
11
|
+
export declare const searchParamsToParams: (searchParams: URLSearchParams) => Record<string, string | string[]>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const subscribeToUrlChange = (callback) => {
|
|
2
|
+
if (typeof window === "undefined") {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
window.addEventListener("popstate", callback);
|
|
6
|
+
window.addEventListener("replaceState", callback);
|
|
7
|
+
window.addEventListener("pushState", callback);
|
|
8
|
+
window.addEventListener("hashchange", callback);
|
|
9
|
+
return () => {
|
|
10
|
+
window.removeEventListener("popstate", callback);
|
|
11
|
+
window.removeEventListener("replaceState", callback);
|
|
12
|
+
window.removeEventListener("pushState", callback);
|
|
13
|
+
window.removeEventListener("hashchange", callback);
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
const getUrl = (to) => {
|
|
17
|
+
if (typeof to === "string") {
|
|
18
|
+
return to;
|
|
19
|
+
}
|
|
20
|
+
if (to instanceof URL) {
|
|
21
|
+
return to;
|
|
22
|
+
}
|
|
23
|
+
return new URL(to.path, getCurrentUrl());
|
|
24
|
+
};
|
|
25
|
+
export const goTo = (to, options) => {
|
|
26
|
+
if (typeof window === "undefined") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const url = getUrl(to);
|
|
30
|
+
if (options?.replace) {
|
|
31
|
+
window.history.replaceState(null, "", url);
|
|
32
|
+
window.dispatchEvent(new Event("replaceState"));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
window.history.pushState(null, "", url);
|
|
36
|
+
window.dispatchEvent(new Event("pushState"));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export const getCurrentUrl = () => {
|
|
40
|
+
if (typeof window === "undefined") {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
return window.location.href;
|
|
44
|
+
};
|
|
45
|
+
export const paramsToSearchParams = (params, searchParams = new URLSearchParams()) => {
|
|
46
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
for (const v of value) {
|
|
49
|
+
searchParams.append(key, v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
searchParams.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return searchParams;
|
|
57
|
+
};
|
|
58
|
+
export const searchParamsToParams = (searchParams) => {
|
|
59
|
+
const params = {};
|
|
60
|
+
searchParams.forEach((value, key) => {
|
|
61
|
+
if (key in params) {
|
|
62
|
+
if (Array.isArray(params[key])) {
|
|
63
|
+
params[key].push(value);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
params[key] = [params[key], value];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
params[key] = value;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return params;
|
|
74
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const currentUrlSignal: import("@preact/signals-core").Signal<string>;
|
|
2
|
+
export declare const urlSearchParams: import("@preact/signals-core").ReadonlySignal<URLSearchParams>;
|
|
3
|
+
export type Param = {
|
|
4
|
+
key: string;
|
|
5
|
+
value: string | string[] | undefined;
|
|
6
|
+
};
|
|
7
|
+
export declare const setParams: (...params: Param[]) => void;
|
|
8
|
+
export declare const currentUrl: import("@preact/signals-core").ReadonlySignal<{
|
|
9
|
+
readonly hash: string;
|
|
10
|
+
readonly pathname: string;
|
|
11
|
+
readonly origin: string;
|
|
12
|
+
readonly params: Record<string, string | string[]>;
|
|
13
|
+
}>;
|
|
14
|
+
export declare const getParamValue: (key: string) => string | null;
|
|
15
|
+
export declare const getParamValues: (key: string) => string[];
|
|
16
|
+
export declare const hasParam: (key: string) => boolean;
|
|
17
|
+
export declare const getParamValueComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<string | null>;
|
|
18
|
+
export declare const getParamValuesComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<string[]>;
|
|
19
|
+
export declare const hasParamComputed: (key: string) => import("@preact/signals-core").ReadonlySignal<boolean>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { computed, signal } from "@preact/signals-core";
|
|
2
|
+
import { getCurrentUrl, goTo, searchParamsToParams, subscribeToUrlChange } from "./helpers.js";
|
|
3
|
+
export const currentUrlSignal = signal(getCurrentUrl());
|
|
4
|
+
subscribeToUrlChange(() => {
|
|
5
|
+
if (currentUrlSignal.peek() === getCurrentUrl()) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
currentUrlSignal.value = getCurrentUrl();
|
|
9
|
+
});
|
|
10
|
+
const urlSignal = computed(() => new URL(currentUrlSignal.value));
|
|
11
|
+
export const urlSearchParams = computed(() => urlSignal.value.searchParams);
|
|
12
|
+
export const setParams = (...params) => {
|
|
13
|
+
const newUrl = new URL(getCurrentUrl());
|
|
14
|
+
for (const param of params) {
|
|
15
|
+
newUrl.searchParams.delete(param.key);
|
|
16
|
+
if (Array.isArray(param.value)) {
|
|
17
|
+
for (const value of param.value) {
|
|
18
|
+
newUrl.searchParams.append(param.key, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (typeof param.value === "string") {
|
|
22
|
+
newUrl.searchParams.set(param.key, param.value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (newUrl.href === urlSignal.peek().href) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
goTo(newUrl.href, {
|
|
29
|
+
replace: true,
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
export const currentUrl = computed(() => {
|
|
33
|
+
return {
|
|
34
|
+
hash: urlSignal.value.hash,
|
|
35
|
+
pathname: urlSignal.value.pathname,
|
|
36
|
+
origin: urlSignal.value.origin,
|
|
37
|
+
params: searchParamsToParams(urlSearchParams.value),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
export const getParamValue = (key) => urlSearchParams.value.get(key);
|
|
41
|
+
export const getParamValues = (key) => urlSearchParams.value.getAll(key);
|
|
42
|
+
export const hasParam = (key) => urlSearchParams.value.has(key);
|
|
43
|
+
export const getParamValueComputed = (key) => computed(() => getParamValue(key));
|
|
44
|
+
export const getParamValuesComputed = (key) => computed(() => getParamValues(key));
|
|
45
|
+
export const hasParamComputed = (key) => computed(() => hasParam(key));
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { signal } from "@preact/signals-core";
|
|
2
|
+
const signalExample = signal(1);
|
|
3
|
+
export const isSignal = (value) => {
|
|
4
|
+
return (typeof value === "object" &&
|
|
5
|
+
value !== null &&
|
|
6
|
+
"brand" in value &&
|
|
7
|
+
typeof value.brand === "symbol" &&
|
|
8
|
+
value.brand === signalExample.brand);
|
|
9
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allurereport/web-commons",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Collection of utilities used across the web Allure reports",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"allure",
|
|
@@ -20,12 +20,16 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "run clean && tsc --project ./tsconfig.json",
|
|
23
|
-
"clean": "rimraf ./dist"
|
|
23
|
+
"clean": "rimraf ./dist",
|
|
24
|
+
"test": "vitest run"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@allurereport/
|
|
27
|
-
"@allurereport/
|
|
28
|
-
"@allurereport/
|
|
27
|
+
"@allurereport/aql": "3.1.0",
|
|
28
|
+
"@allurereport/charts-api": "3.1.0",
|
|
29
|
+
"@allurereport/core-api": "3.1.0",
|
|
30
|
+
"@allurereport/plugin-api": "3.1.0",
|
|
31
|
+
"@preact/signals": "^2.6.1",
|
|
32
|
+
"@preact/signals-core": "^1.12.2",
|
|
29
33
|
"ansi-to-html": "^0.7.2",
|
|
30
34
|
"d3-interpolate": "^3.0.1",
|
|
31
35
|
"d3-scale": "^4.0.2",
|
|
@@ -51,6 +55,7 @@
|
|
|
51
55
|
"eslint-plugin-n": "^17.10.1",
|
|
52
56
|
"eslint-plugin-no-null": "^1.0.2",
|
|
53
57
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
58
|
+
"jsdom": "^26.1.0",
|
|
54
59
|
"rimraf": "^6.0.1",
|
|
55
60
|
"tslib": "^2.7.0",
|
|
56
61
|
"typescript": "^5.6.3",
|