@reykjavik/webtools 0.3.5 → 0.3.7
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/CHANGELOG.md +17 -1
- package/README.md +355 -9
- package/alertsStore/index.d.ts +201 -0
- package/alertsStore/index.js +317 -0
- package/alertsStore/react.d.ts +36 -0
- package/alertsStore/react.js +96 -0
- package/async.d.ts +51 -0
- package/async.js +59 -1
- package/errorhandling.d.ts +1 -1
- package/esm/alertsStore/index.d.ts +201 -0
- package/esm/alertsStore/index.js +280 -0
- package/esm/alertsStore/react.d.ts +36 -0
- package/esm/alertsStore/react.js +58 -0
- package/esm/async.d.ts +51 -0
- package/esm/async.js +57 -0
- package/esm/errorhandling.d.ts +1 -1
- package/esm/index.d.ts +2 -0
- package/index.d.ts +2 -0
- package/package.json +11 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
declare const messageSchema: v.UnionSchema<[v.StringSchema<undefined>, v.ArraySchema<v.UnionSchema<[v.StringSchema<undefined>, v.ObjectSchema<{
|
|
3
|
+
readonly tag: v.LiteralSchema<"a", undefined>;
|
|
4
|
+
readonly text: v.StringSchema<undefined>;
|
|
5
|
+
readonly href: v.StringSchema<undefined>;
|
|
6
|
+
readonly target: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
7
|
+
readonly hrefLang: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
8
|
+
readonly lang: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
9
|
+
}, undefined>, v.ObjectSchema<{
|
|
10
|
+
readonly tag: v.LiteralSchema<"strong", undefined>;
|
|
11
|
+
readonly text: v.StringSchema<undefined>;
|
|
12
|
+
}, undefined>], undefined>, undefined>], undefined>;
|
|
13
|
+
export type AlertMessage = v.InferOutput<typeof messageSchema>;
|
|
14
|
+
type _AlertNotification<Level, Type, Flag> = {
|
|
15
|
+
level: Level;
|
|
16
|
+
message: AlertMessage;
|
|
17
|
+
type?: Type;
|
|
18
|
+
flags?: Array<Flag>;
|
|
19
|
+
duration?: number;
|
|
20
|
+
id: string;
|
|
21
|
+
};
|
|
22
|
+
declare const defaultAlertLevels: readonly ["info", "warning", "success", "error"];
|
|
23
|
+
declare const defaultDurations: {
|
|
24
|
+
BLINK: number;
|
|
25
|
+
SHORT: number;
|
|
26
|
+
MEDIUM: number;
|
|
27
|
+
LONG: number;
|
|
28
|
+
XLONG: number;
|
|
29
|
+
INDEFINITE: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* A configuration object for the `createAlerter` factory function, that allows
|
|
33
|
+
* the customization of all of the accepted alert values and durations.
|
|
34
|
+
*
|
|
35
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#type-alerterconfig
|
|
36
|
+
*/
|
|
37
|
+
export type AlerterConfig<Level extends string = (typeof defaultAlertLevels)[number], Type extends string = string, Flag extends string = string, Duration extends string = keyof typeof defaultDurations, Durations extends Record<Duration, number> = Record<Duration, number>> = {
|
|
38
|
+
/**
|
|
39
|
+
* Identifier for the alerts store, used to create the key to persist alerts
|
|
40
|
+
* in `sessionStorage` (or other provided storage).
|
|
41
|
+
*
|
|
42
|
+
* Only required if you need to create multiple independent alert stores in
|
|
43
|
+
* the same app.
|
|
44
|
+
*
|
|
45
|
+
* Default: `"app~alerts"`
|
|
46
|
+
*/
|
|
47
|
+
key?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The allowed alert "levels". The returned `alerter` object will have a
|
|
50
|
+
* named dispatcher method for each level.
|
|
51
|
+
*
|
|
52
|
+
* Default: `['info', 'warning', 'success', 'error']`
|
|
53
|
+
*/
|
|
54
|
+
levels?: Array<Level>;
|
|
55
|
+
/**
|
|
56
|
+
* The allowed alert "types", which can be used to, for example, to dispatch
|
|
57
|
+
* both "toasts" vs. "static alert banners" via the same store.
|
|
58
|
+
*
|
|
59
|
+
* This can also be used for more basic styling or categorization purposes.
|
|
60
|
+
*
|
|
61
|
+
* Default: no restrictions, any string value is allowed.
|
|
62
|
+
*/
|
|
63
|
+
types?: Array<Type>;
|
|
64
|
+
/**
|
|
65
|
+
* The allowed alert "flags", which can be changed during the lifetime of
|
|
66
|
+
* an alert using the `setFlags` function on the `AlertInfo` object.
|
|
67
|
+
*
|
|
68
|
+
* This can be used for styling or any other purpose you like.
|
|
69
|
+
*
|
|
70
|
+
* Default: no restriction, any string value is allowed.
|
|
71
|
+
*/
|
|
72
|
+
flags?: Array<Flag>;
|
|
73
|
+
/**
|
|
74
|
+
* Optionally controls the allowed alert "duration" names and their lengths
|
|
75
|
+
* in milliseconds.
|
|
76
|
+
*
|
|
77
|
+
* Default:
|
|
78
|
+
* ```ts
|
|
79
|
+
* {
|
|
80
|
+
BLINK: 2_000,
|
|
81
|
+
SHORT: 4_000,
|
|
82
|
+
MEDIUM: 8_000,
|
|
83
|
+
LONG: 16_000,
|
|
84
|
+
XLONG: 32_000,
|
|
85
|
+
INDEFINITE: 0,
|
|
86
|
+
}
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
durations?: Durations;
|
|
90
|
+
/**
|
|
91
|
+
* Default duration to use for alerts if no duration is specified when
|
|
92
|
+
* dispatching.
|
|
93
|
+
*
|
|
94
|
+
* Default: `SHORT` if using the default durations, otherwise the default
|
|
95
|
+
* is `0` (indefinite)
|
|
96
|
+
*/
|
|
97
|
+
defaultDuration?: Durations extends Record<infer D, number> ? D : never;
|
|
98
|
+
/**
|
|
99
|
+
* Optional custom storage object to use instead of `sessionStorage` (the
|
|
100
|
+
* default) for persisting alerts across page reloads, etc.
|
|
101
|
+
*/
|
|
102
|
+
storage?: {
|
|
103
|
+
getItem: (key: string) => string | undefined | null;
|
|
104
|
+
setItem: (key: string, value: string) => void;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Factory function that creates an alerter store singleton with optional
|
|
109
|
+
* configuration for the genarated alerts.
|
|
110
|
+
*
|
|
111
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
112
|
+
*/
|
|
113
|
+
export declare const createAlerterStore: <Level extends string = (typeof defaultAlertLevels)[number], Type extends string = string, Flag extends string = string, Duration extends string = keyof typeof defaultDurations>(cfg?: AlerterConfig<Level, Type, Flag, Duration>) => {
|
|
114
|
+
/**
|
|
115
|
+
* Singleton object with methods for showing alerts of different levels.
|
|
116
|
+
* Pass a payload object to the method of the level you want to dispatch,
|
|
117
|
+
* and the alert will be added to the store.
|
|
118
|
+
*
|
|
119
|
+
* Use `subscribeToAlerts` elsewhere in the app to subscribe to alert
|
|
120
|
+
* notifications and display them.
|
|
121
|
+
*
|
|
122
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
123
|
+
*/
|
|
124
|
+
alerter: Record<Level, (payload: {
|
|
125
|
+
/**
|
|
126
|
+
* A simple string containing the alert message.
|
|
127
|
+
*
|
|
128
|
+
* For sightly more complex alert messages pass an array of strings
|
|
129
|
+
* (representing text nodes), objects with `tag: 'a'` and hyperlink-related
|
|
130
|
+
* props, or `tag: 'strong'` objects for minimal rich formatting.
|
|
131
|
+
*
|
|
132
|
+
* (Assume that such array itmes will be rendered with whitespace between
|
|
133
|
+
* them.)
|
|
134
|
+
*/
|
|
135
|
+
message: AlertMessage;
|
|
136
|
+
/**
|
|
137
|
+
* Allows distinguishing between different "types" of alerts, for example,
|
|
138
|
+
* to dispatch both "toasts" vs. "static alert banners" via the same store.
|
|
139
|
+
*
|
|
140
|
+
* May also be used for more basic styling or categorization purposes.
|
|
141
|
+
*/
|
|
142
|
+
type?: Type;
|
|
143
|
+
/**
|
|
144
|
+
* Flag values can be changed during the lifetime of
|
|
145
|
+
* an alert using the `setFlags` function on each `AlertInfo` object.
|
|
146
|
+
*
|
|
147
|
+
* Flags may be used for styling or any other purpose you like.
|
|
148
|
+
*/
|
|
149
|
+
flags?: Array<Flag>;
|
|
150
|
+
/**
|
|
151
|
+
* Hint for how long the notification should remain displayed before
|
|
152
|
+
* auto-dismissing.
|
|
153
|
+
*
|
|
154
|
+
* **NOTE:** The alerter store does not implement auto-dismissing. However,
|
|
155
|
+
* this value can be used by UI component that actually render the alert,
|
|
156
|
+
* by calling each `AlertInfo`'s `dismiss` method.
|
|
157
|
+
*/
|
|
158
|
+
duration?: Duration;
|
|
159
|
+
delay?: number;
|
|
160
|
+
}) => void>;
|
|
161
|
+
/**
|
|
162
|
+
* Subscribes to alert events. The provided callback will be called whenever a
|
|
163
|
+
* alert is added or cleared.
|
|
164
|
+
*
|
|
165
|
+
* The callback is called immediately upon subscription if there are already
|
|
166
|
+
* active alerts.
|
|
167
|
+
*
|
|
168
|
+
* Returns an unsubscribe function that can be called to stop receiving alert
|
|
169
|
+
* events.
|
|
170
|
+
*
|
|
171
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
172
|
+
*/
|
|
173
|
+
subscribe: (callback: (notifications: Array<_AlertNotification<Level, Type, Flag> & {
|
|
174
|
+
/** Dispatcher function that dismisses/hides/removes the callback */
|
|
175
|
+
dismiss: () => void;
|
|
176
|
+
/**
|
|
177
|
+
* Dispatcher function that can be used to set a simple "flag" on the alert,
|
|
178
|
+
* which can be used for styling or other purposes.
|
|
179
|
+
*/
|
|
180
|
+
setFlags: (value: Flag | Array<Flag> | ((flags: Array<Flag> | undefined) => Array<Flag> | undefined)) => void;
|
|
181
|
+
}>, meta: {
|
|
182
|
+
type: "clear" | "add" | "change";
|
|
183
|
+
/** IDs of the alerts that were added or cleared in this event. */
|
|
184
|
+
ids: Array<string>;
|
|
185
|
+
}) => void) => () => void;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Utility type for inferring the payload shape of the dispatching methods of a
|
|
189
|
+
* specific `alerter` singleton.
|
|
190
|
+
*
|
|
191
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
192
|
+
*/
|
|
193
|
+
export type InferAlerterPayload<F extends Record<string, (...args: Array<any>) => void>> = F extends Record<string, (payload: infer P) => void> ? P : never;
|
|
194
|
+
/**
|
|
195
|
+
* Utility type for inferring the alert info object shape received by the
|
|
196
|
+
* callbacks of a specific alerter `subscribe` function.
|
|
197
|
+
*
|
|
198
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
199
|
+
*/
|
|
200
|
+
export type InferSubscriberAlerts<F extends (callback: (alerts: Array<unknown>, meta: unknown) => void) => () => void> = F extends (callback: (alerts: Array<infer A>) => void) => () => void ? A : never;
|
|
201
|
+
export {};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { dumbId, ObjectFromEntries } from '@reykjavik/hanna-utils';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
|
+
const messageSchema = v.union([
|
|
4
|
+
v.string(),
|
|
5
|
+
v.array(v.union([
|
|
6
|
+
// text nodes
|
|
7
|
+
v.string(),
|
|
8
|
+
// link elements
|
|
9
|
+
v.object({
|
|
10
|
+
tag: v.literal('a'),
|
|
11
|
+
text: v.string(),
|
|
12
|
+
href: v.string(),
|
|
13
|
+
target: v.optional(v.string()),
|
|
14
|
+
hrefLang: v.optional(v.string()),
|
|
15
|
+
lang: v.optional(v.string()),
|
|
16
|
+
}),
|
|
17
|
+
// strong/bold elements
|
|
18
|
+
v.object({
|
|
19
|
+
tag: v.literal('strong'),
|
|
20
|
+
text: v.string(),
|
|
21
|
+
}),
|
|
22
|
+
])),
|
|
23
|
+
]);
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
const defaultKey = 'app~alerts';
|
|
26
|
+
const defaultAlertLevels = ['info', 'warning', 'success', 'error'];
|
|
27
|
+
const defaultDurations = {
|
|
28
|
+
BLINK: 2000,
|
|
29
|
+
SHORT: 4000,
|
|
30
|
+
MEDIUM: 8000,
|
|
31
|
+
LONG: 16000,
|
|
32
|
+
XLONG: 32000,
|
|
33
|
+
INDEFINITE: 0,
|
|
34
|
+
};
|
|
35
|
+
const defaultDefaultDuration = 'SHORT';
|
|
36
|
+
const storeKeys = {};
|
|
37
|
+
/**
|
|
38
|
+
* Factory function that creates an alerter store singleton with optional
|
|
39
|
+
* configuration for the genarated alerts.
|
|
40
|
+
*
|
|
41
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
42
|
+
*/
|
|
43
|
+
/*#__NO_SIDE_EFFECTS__*/
|
|
44
|
+
export const createAlerterStore = (cfg = {}) => {
|
|
45
|
+
const STORE_KEY = cfg.key || defaultKey;
|
|
46
|
+
if (storeKeys[STORE_KEY]) {
|
|
47
|
+
throw new Error(`An alerter store with key "${STORE_KEY}" already exists.`);
|
|
48
|
+
}
|
|
49
|
+
storeKeys[STORE_KEY] = true;
|
|
50
|
+
const storgae = cfg.storage || (typeof sessionStorage !== 'undefined' ? sessionStorage : undefined);
|
|
51
|
+
const alertLevels = cfg.levels || defaultAlertLevels;
|
|
52
|
+
const durations = cfg.durations || defaultDurations;
|
|
53
|
+
const DEFAULT_DURATION = cfg.durations
|
|
54
|
+
? cfg.defaultDuration
|
|
55
|
+
: defaultDefaultDuration;
|
|
56
|
+
const _notificationSchema = v.object({
|
|
57
|
+
level: v.picklist(alertLevels),
|
|
58
|
+
message: messageSchema,
|
|
59
|
+
type: v.optional(cfg.types ? v.picklist(cfg.types) : v.string()),
|
|
60
|
+
flags: v.optional(v.array(cfg.flags ? v.picklist(cfg.flags) : v.string())),
|
|
61
|
+
duration: v.optional(v.number()),
|
|
62
|
+
id: v.string(),
|
|
63
|
+
});
|
|
64
|
+
const alertsSchema = v.object({
|
|
65
|
+
active: v.array(_notificationSchema),
|
|
66
|
+
pending: v.array(v.intersect([_notificationSchema, v.object({ showAt: v.number() })])),
|
|
67
|
+
});
|
|
68
|
+
/** Global array of inflight alert notifications */
|
|
69
|
+
const alerts = {
|
|
70
|
+
active: [],
|
|
71
|
+
pending: [],
|
|
72
|
+
};
|
|
73
|
+
const _saveAlertsToStorage = storgae
|
|
74
|
+
? () => storgae.setItem(STORE_KEY, JSON.stringify(alerts))
|
|
75
|
+
: () => undefined;
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/** Array of callbacks to call whenever alerts are activated or cleared */
|
|
78
|
+
const subscriptions = [];
|
|
79
|
+
let isEmitting;
|
|
80
|
+
/** Calls all subscribed callbacks with the currently active alerts and the event type. */
|
|
81
|
+
const emitEvent = (meta, callback) => {
|
|
82
|
+
if (callback) {
|
|
83
|
+
setTimeout(() => callback(alerts.active, meta));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
_saveAlertsToStorage();
|
|
87
|
+
clearTimeout(isEmitting);
|
|
88
|
+
// For consistency, always delay pinging the subscribed callbacks until next tick.
|
|
89
|
+
isEmitting = setTimeout(() => {
|
|
90
|
+
subscriptions.forEach((callback) => {
|
|
91
|
+
callback(alerts.active, meta);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const clearAlert = (id) => {
|
|
96
|
+
let found = false;
|
|
97
|
+
alerts.active.some((alert, idx) => {
|
|
98
|
+
if (alert.id === id) {
|
|
99
|
+
alerts.active = alerts.active.toSpliced(idx, 1);
|
|
100
|
+
found = true;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
alerts.pending.some((alert, idx) => {
|
|
105
|
+
if (alert.id === id) {
|
|
106
|
+
alerts.pending = alerts.pending.toSpliced(idx, 1);
|
|
107
|
+
found = true;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (found) {
|
|
112
|
+
emitEvent({ type: 'clear', ids: [id] });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
const _unsubscribe = (callback) => {
|
|
117
|
+
const idx = subscriptions.indexOf(callback);
|
|
118
|
+
if (idx > -1) {
|
|
119
|
+
subscriptions.splice(idx, 1);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const subscribe = (callback) => {
|
|
123
|
+
if (subscriptions.indexOf(callback) === -1) {
|
|
124
|
+
subscriptions.push(callback);
|
|
125
|
+
// Should we allow opting-out of immediate invocations via function param?
|
|
126
|
+
if (alerts.active.length) {
|
|
127
|
+
emitEvent({
|
|
128
|
+
type: 'add',
|
|
129
|
+
ids: alerts.active.map((t) => t.id),
|
|
130
|
+
}, callback);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return () => _unsubscribe(callback);
|
|
134
|
+
};
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
const buildNotification = (_payload, level) => {
|
|
137
|
+
// Strip away duration and delay (not part of the notification object)
|
|
138
|
+
const { duration, delay, ...payload } = _payload;
|
|
139
|
+
const durationMs = durations[(duration || DEFAULT_DURATION || '')] || 0;
|
|
140
|
+
return {
|
|
141
|
+
...payload,
|
|
142
|
+
level,
|
|
143
|
+
id: dumbId(), // Make unique ID for the notification
|
|
144
|
+
...(durationMs && { duration: durationMs }),
|
|
145
|
+
...(delay && delay > 50 && { showAt: Date.now() + delay }),
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
// add dismiss and setFlags dispatcher functions
|
|
149
|
+
// this way they stay stable across updates to the same alert, which can be useful for UI components that want to update or dismiss an alert after it's been rendered.
|
|
150
|
+
const addMethodsToAlertInfo = (notification) => {
|
|
151
|
+
const id = notification.id;
|
|
152
|
+
return {
|
|
153
|
+
...notification,
|
|
154
|
+
dismiss: () => clearAlert(id),
|
|
155
|
+
setFlags: (value) => {
|
|
156
|
+
const notification = alerts.active.find((t) => t.id === id);
|
|
157
|
+
if (!notification) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const oldFlags = notification.flags;
|
|
161
|
+
const flags = typeof value === 'string'
|
|
162
|
+
? [value]
|
|
163
|
+
: Array.isArray(value)
|
|
164
|
+
? [...value]
|
|
165
|
+
: value(notification.flags);
|
|
166
|
+
if (flags === oldFlags) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
alerts.active = alerts.active.toSpliced(alerts.active.findIndex((t) => t.id === id), 1, { ...notification, flags });
|
|
170
|
+
emitEvent({ type: 'change', ids: [id] });
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
const preparePendingAlertActivation = ({ id, showAt }) => {
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
const idx = alerts.pending.findIndex((t) => t.id === id);
|
|
177
|
+
if (idx === -1) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Find and remove the pending in alert in one fell swoop
|
|
181
|
+
const { showAt, ...clonedAsActive } = alerts.pending.splice(idx, 1)[0];
|
|
182
|
+
alerts.active = [...alerts.active, addMethodsToAlertInfo(clonedAsActive)];
|
|
183
|
+
emitEvent({ type: 'add', ids: [id] });
|
|
184
|
+
}, showAt - Date.now());
|
|
185
|
+
};
|
|
186
|
+
const _addAlert = (payload, level) => {
|
|
187
|
+
const notification = buildNotification(payload, level);
|
|
188
|
+
if (!notification.showAt) {
|
|
189
|
+
// Notification starts active. Clone the array.
|
|
190
|
+
alerts.active = [...alerts.active, addMethodsToAlertInfo(notification)];
|
|
191
|
+
emitEvent({ type: 'add', ids: [notification.id] });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Set up delayed dispatch of the notification
|
|
195
|
+
// Store it as pending
|
|
196
|
+
alerts.pending.push(notification);
|
|
197
|
+
// Persist the updated alerts state immediately
|
|
198
|
+
_saveAlertsToStorage();
|
|
199
|
+
// Set up a timer for make it active
|
|
200
|
+
preparePendingAlertActivation(notification);
|
|
201
|
+
};
|
|
202
|
+
const alerter = ObjectFromEntries(alertLevels.map((level) => [
|
|
203
|
+
level,
|
|
204
|
+
(payload) => _addAlert(payload, level),
|
|
205
|
+
]));
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// On module load, read saved alerts from the storage (def. `sessionStorage`),
|
|
208
|
+
// if available.
|
|
209
|
+
// This allows us to persist alerts across page reloads,
|
|
210
|
+
// but NOT across tabs or browser sessions.
|
|
211
|
+
storgae &&
|
|
212
|
+
(() => {
|
|
213
|
+
const storedAlerts = storgae.getItem(STORE_KEY);
|
|
214
|
+
if (!storedAlerts) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
// NOTE: This TypeAssertion is wrong rn and only becomes true after
|
|
219
|
+
// `addMethodsToAlertInfo` is called on the active alerts.
|
|
220
|
+
// Also TS has a hard time understanding the dynamically generated
|
|
221
|
+
// schema and that they're actually correct in terms of the configuration
|
|
222
|
+
// type params `Level`, `Type` and `Flag`.
|
|
223
|
+
const paesed = v.parse(alertsSchema, JSON.parse(storedAlerts));
|
|
224
|
+
alerts.pending = paesed.pending;
|
|
225
|
+
alerts.active = paesed.active.map(addMethodsToAlertInfo);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.error('Failed to parse stored alerts:', e);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
const newActive = [];
|
|
233
|
+
// Check pending alerts and set up timers, unless they should already
|
|
234
|
+
// be active, in which case move them to active immediately.
|
|
235
|
+
alerts.pending = alerts.pending.filter((alert) => {
|
|
236
|
+
if (alert.showAt <= now) {
|
|
237
|
+
// Remove pending alerts that should now be active. Store them in `newActive`
|
|
238
|
+
newActive.push(alert);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
// Keep the rest, and set up timers to activate them later.
|
|
242
|
+
preparePendingAlertActivation(alert);
|
|
243
|
+
return true;
|
|
244
|
+
});
|
|
245
|
+
// Append `newActive` to the active alerts array
|
|
246
|
+
alerts.active.push(...newActive
|
|
247
|
+
// Make sure alerts with earlier `showAt` are shown first
|
|
248
|
+
.sort((a, b) => a.showAt - b.showAt)
|
|
249
|
+
// remove showAt, not part of active alerts
|
|
250
|
+
.map(({ showAt, ...rest }) => addMethodsToAlertInfo(rest)));
|
|
251
|
+
// Save the (possibly) cleaned up alerts back to the storage, because why not. :-D
|
|
252
|
+
_saveAlertsToStorage();
|
|
253
|
+
})();
|
|
254
|
+
return {
|
|
255
|
+
/**
|
|
256
|
+
* Singleton object with methods for showing alerts of different levels.
|
|
257
|
+
* Pass a payload object to the method of the level you want to dispatch,
|
|
258
|
+
* and the alert will be added to the store.
|
|
259
|
+
*
|
|
260
|
+
* Use `subscribeToAlerts` elsewhere in the app to subscribe to alert
|
|
261
|
+
* notifications and display them.
|
|
262
|
+
*
|
|
263
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
264
|
+
*/
|
|
265
|
+
alerter,
|
|
266
|
+
/**
|
|
267
|
+
* Subscribes to alert events. The provided callback will be called whenever a
|
|
268
|
+
* alert is added or cleared.
|
|
269
|
+
*
|
|
270
|
+
* The callback is called immediately upon subscription if there are already
|
|
271
|
+
* active alerts.
|
|
272
|
+
*
|
|
273
|
+
* Returns an unsubscribe function that can be called to stop receiving alert
|
|
274
|
+
* events.
|
|
275
|
+
*
|
|
276
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#createalerterstore
|
|
277
|
+
*/
|
|
278
|
+
subscribe,
|
|
279
|
+
};
|
|
280
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { AlertMessage } from './index.js';
|
|
3
|
+
type SubsScriber<AlertInfo> = (callback: (alerts: Array<AlertInfo>, _type: string) => void) => () => void;
|
|
4
|
+
/**
|
|
5
|
+
* Factory function that creates a React subscription hook and a container
|
|
6
|
+
* component linked to a specific alerter store subscibe function.
|
|
7
|
+
*
|
|
8
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#makereactsubscription
|
|
9
|
+
*/
|
|
10
|
+
export declare const makeReactSubscription: <AlertInfo>(subscribe: SubsScriber<AlertInfo>) => {
|
|
11
|
+
useAlerter: () => AlertInfo[];
|
|
12
|
+
AlertsContainer: (props: {
|
|
13
|
+
children: (alerts: Array<AlertInfo>) => ReactNode;
|
|
14
|
+
}) => React.ReactNode;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Helper to render an alerter alert message, which can be a simple string or a
|
|
18
|
+
* more complex array of strings and objects representing links and rich (bold)
|
|
19
|
+
* text formatting.
|
|
20
|
+
*
|
|
21
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#renderalertmessage
|
|
22
|
+
*/
|
|
23
|
+
export declare const renderAlertMessage: {
|
|
24
|
+
(message: AlertMessage, linkComponent?: renderAlertMessage.LinkRenderer): ReactNode;
|
|
25
|
+
withLinkRenderer(LinkCompnent: renderAlertMessage.LinkRenderer): (message: AlertMessage) => React.ReactNode;
|
|
26
|
+
};
|
|
27
|
+
export declare namespace renderAlertMessage {
|
|
28
|
+
type LinkRendererProps = Omit<Extract<AlertMessage[number], {
|
|
29
|
+
tag: 'a';
|
|
30
|
+
}>, 'tag' | 'text'> & {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
};
|
|
33
|
+
export type LinkRenderer = (props: LinkRendererProps) => ReactNode;
|
|
34
|
+
export {};
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Factory function that creates a React subscription hook and a container
|
|
4
|
+
* component linked to a specific alerter store subscibe function.
|
|
5
|
+
*
|
|
6
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#makereactsubscription
|
|
7
|
+
*/
|
|
8
|
+
/*#__NO_SIDE_EFFECTS__*/
|
|
9
|
+
export const makeReactSubscription = (subscribe) => {
|
|
10
|
+
const useAlerter = () => {
|
|
11
|
+
const [alerts, setAlerts] = useState([]);
|
|
12
|
+
useEffect(() => subscribe((alerts) => setAlerts(alerts)), []);
|
|
13
|
+
return alerts;
|
|
14
|
+
};
|
|
15
|
+
const AlertsContainer = (props) => {
|
|
16
|
+
const alerts = useAlerter();
|
|
17
|
+
return props.children(alerts);
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
useAlerter,
|
|
21
|
+
AlertsContainer,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Helper to render an alerter alert message, which can be a simple string or a
|
|
26
|
+
* more complex array of strings and objects representing links and rich (bold)
|
|
27
|
+
* text formatting.
|
|
28
|
+
*
|
|
29
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#renderalertmessage
|
|
30
|
+
*/
|
|
31
|
+
/*#__NO_SIDE_EFFECTS__*/
|
|
32
|
+
export const renderAlertMessage = (message, linkComponent) => {
|
|
33
|
+
const Link = linkComponent || 'a';
|
|
34
|
+
return typeof message === 'string'
|
|
35
|
+
? message
|
|
36
|
+
: message.flatMap((part, i) => {
|
|
37
|
+
if (typeof part === 'string') {
|
|
38
|
+
return ` ${part}`;
|
|
39
|
+
}
|
|
40
|
+
if (part.tag === 'a') {
|
|
41
|
+
const { text, tag, ...linkProps } = part;
|
|
42
|
+
return [
|
|
43
|
+
' ',
|
|
44
|
+
React.createElement(Link, { key: i, ...linkProps }, text),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
return [' ', React.createElement(part.tag, { key: i }, part.text)];
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Retrns a curried version of `renderAlertMessage` that uses the provided
|
|
52
|
+
* `LinkComponent` for rendering links in alert messages.
|
|
53
|
+
*
|
|
54
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#renderalertmessagewithlinkrenderer
|
|
55
|
+
*/
|
|
56
|
+
/*#__NO_SIDE_EFFECTS__*/
|
|
57
|
+
renderAlertMessage.withLinkRenderer =
|
|
58
|
+
(LinkCompnent) => (message) => renderAlertMessage(message, LinkCompnent);
|
package/esm/async.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EitherObj } from '@reykjavik/hanna-utils';
|
|
2
|
+
import { Result } from './errorhandling.js';
|
|
3
|
+
import { TTL } from './http.js';
|
|
2
4
|
type PlainObj = Record<string, unknown>;
|
|
3
5
|
/**
|
|
4
6
|
* Simple sleep function. Returns a promise that resolves after `length`
|
|
@@ -70,4 +72,53 @@ export declare const throttle: {
|
|
|
70
72
|
<A extends Array<unknown>>(func: (...args: A) => void, delay: number, skipFirst?: boolean): Finishable<A>;
|
|
71
73
|
d(delay: number, skipFirst?: boolean): Finishable<[fn: (...args: Array<any>) => void, ...args: any[]]>;
|
|
72
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Wraps an async function with a simple, but fairly robust caching layer.
|
|
77
|
+
*
|
|
78
|
+
* Successful results are cached for `ttlMs`, while error results are
|
|
79
|
+
* throttled to avoid hammering the underlying function.
|
|
80
|
+
*
|
|
81
|
+
* Has no max size or eviction strategy; intended for caching a small,
|
|
82
|
+
* clearly bounded number of different cache "keys" (e.g. per language).
|
|
83
|
+
*
|
|
84
|
+
* @see https://github.com/reykjavikcity/webtools/blob/v0.3/README.md#cachifyasync
|
|
85
|
+
*/
|
|
86
|
+
export declare const cachifyAsync: <R, F extends (...args: Array<any>) => Promise<Result.TupleObj<R>>>(opts: {
|
|
87
|
+
/** The async function to cache. */
|
|
88
|
+
fn: F;
|
|
89
|
+
/** How long to cache successful results. Number values are treated as seconds */
|
|
90
|
+
ttl: TTL;
|
|
91
|
+
/**
|
|
92
|
+
* The minimum time between retries for error results, to avoid hammering
|
|
93
|
+
* the underlying function while waiting for the issue to be (hopefully)
|
|
94
|
+
* resolved.
|
|
95
|
+
*
|
|
96
|
+
* Raw numbers are treated as seconds.
|
|
97
|
+
*
|
|
98
|
+
* Default: '30s'
|
|
99
|
+
*/
|
|
100
|
+
throttle?: TTL;
|
|
101
|
+
/**
|
|
102
|
+
* Function to optionally set a custom TTL on success and/or error results,
|
|
103
|
+
* when the promise resolves.
|
|
104
|
+
*
|
|
105
|
+
* If `undefined` is returned, the default `ttl` and` `throttle` settings
|
|
106
|
+
* are used.
|
|
107
|
+
*/
|
|
108
|
+
customTtl?: (args: Parameters<F>, result: Result.TupleObj<R>) => TTL | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Creates a custom cache key for the current result set
|
|
111
|
+
*
|
|
112
|
+
* Default: `JSON.stringify` of the function arguments
|
|
113
|
+
*
|
|
114
|
+
*/
|
|
115
|
+
getKey?: (...args: Parameters<F>) => string;
|
|
116
|
+
/**
|
|
117
|
+
* Whether to return stale (last successful) result when `fn` resolves to an
|
|
118
|
+
* error result.
|
|
119
|
+
*
|
|
120
|
+
* Default: `true`
|
|
121
|
+
*/
|
|
122
|
+
returnStale?: boolean;
|
|
123
|
+
}) => F;
|
|
73
124
|
export {};
|