@akc42/app-utils 4.2.2 → 5.0.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/README.md +153 -128
- package/api.js +94 -0
- package/app-utils.js +17 -0
- package/colour.js +34 -0
- package/debug.js +36 -120
- package/package.json +7 -5
- package/partMap.js +117 -0
- package/switch-path.js +7 -2
- package/post-api.js +0 -61
package/README.md
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
# app-utils
|
|
2
2
|
General utilities that I use for building SPAs
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Release 5.0 adds a lot of new functions and some major changes to existing ones. It also has been packaged into an export from a single file.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
## api and ApiError
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The key function is **api** which handles a post request to a `/api/<url>` server, managing the response. ApiError are
|
|
9
|
+
how problems with this are reported. The api call takes two essential parameters and one optional.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
1. url - the url appended to `/api/` to the endpoint of the server.
|
|
12
|
+
2. params - an object containing the parameters to be passed to the endpoint
|
|
13
|
+
3. optional flag to indicate the response is a blob (normally a pdf) to be opened in a new window rather than as a direct response.
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
The response is aysnchronousally returned as an object. Errors are throw using the **ApiError** object.
|
|
16
|
+
|
|
17
|
+
## ApiKeys
|
|
18
|
+
|
|
19
|
+
**ApiKeys** is a class where the new
|
|
20
|
+
|
|
21
|
+
ApiKeys is a class that provides keyboard support by normalising key
|
|
13
22
|
handling amongst browsers and providing a simple interface that can be used
|
|
14
23
|
by the application. Each keyboard usage request the user create a new
|
|
15
24
|
instance of the AppKeys class passing the key target in to the constructor.
|
|
@@ -33,7 +42,7 @@ debug has been replaced with a version that works differently and is incompatibl
|
|
|
33
42
|
NOTE: If you take this approach limit the keys to non function keys as the main app, with its main
|
|
34
43
|
menu may add a handler for the function keys and both menus, and any actions you choose would run at the same time.
|
|
35
44
|
|
|
36
|
-
```
|
|
45
|
+
```javascript
|
|
37
46
|
constructor() {
|
|
38
47
|
super();
|
|
39
48
|
...
|
|
@@ -62,7 +71,7 @@ debug has been replaced with a version that works differently and is incompatibl
|
|
|
62
71
|
if there are different areas of the page needing the respond depending which area
|
|
63
72
|
has focus. This is to create the AppKeys object in the firstUpdated function. Here is a possible example (just one area)
|
|
64
73
|
|
|
65
|
-
```
|
|
74
|
+
```javascript
|
|
66
75
|
connectedCallback() {
|
|
67
76
|
super.connectedCallback();
|
|
68
77
|
if (this.keys !== undefined) this.keys.connect();
|
|
@@ -85,106 +94,104 @@ debug has been replaced with a version that works differently and is incompatibl
|
|
|
85
94
|
If for any reason you want more info about the keyPress, access
|
|
86
95
|
`this.keys.lastPressed`, and it will return the complete binding object
|
|
87
96
|
|
|
97
|
+
## calcTextColour
|
|
88
98
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
the modules default export is a function which can be called with a name and optionally a parameters object. The name is added to `/api/csv/` to make a download request and if they exist the parameters object is turned into a query string. The response from that uri is downloaded (expected to be a csv file).
|
|
92
|
-
|
|
93
|
-
# debug
|
|
94
|
-
|
|
95
|
-
The purpose of this module is to provide a debugable capability which can be
|
|
96
|
-
dynamically switched on and off browser by setting a key in the config
|
|
97
|
-
returned by the server. It will post request to `/api/log` url with an
|
|
98
|
-
`application/json` body part containing message, topic and gap, where message is
|
|
99
|
-
the concatenation of the debug parameters separated by space, topic is the
|
|
100
|
-
topic of this debug message and gap is the number of milliseconds since the
|
|
101
|
-
last debug message with the same topic.
|
|
102
|
-
|
|
103
|
-
Topic data is held in a map, so this module can be used in multiple modules in
|
|
104
|
-
the client and if its the same topic then the gap will be since the last call
|
|
105
|
-
from **any** module.
|
|
106
|
-
|
|
107
|
-
To use the debug function in your code, import this module then set the topic
|
|
108
|
-
as shown.
|
|
109
|
-
|
|
110
|
-
import Debug from 'debug.js';
|
|
111
|
-
|
|
112
|
-
const debug = Debug('topic') topic should only contain the characters a-z or
|
|
113
|
-
A-Z as is converted to lowercase.
|
|
114
|
-
|
|
115
|
-
debug(..messages) //messages will be concatenated by space
|
|
116
|
-
|
|
117
|
-
the debug function will only log the message if config.debug (see
|
|
118
|
-
config-promise) is set to a string which is a comma separated list of topics
|
|
119
|
-
and that list has the topic for this debug call in it.
|
|
120
|
-
|
|
121
|
-
**Note**: the debug function checks with the sessionStorage.getItem('debug) (via an await for the Config Promise)
|
|
122
|
-
to see if the topic is enabled (assumes the result is a ':' separated string of topics). This allows the server to
|
|
123
|
-
change what topics are available via the config api call.
|
|
124
|
-
|
|
125
|
-
# dom-host
|
|
126
|
-
|
|
127
|
-
the default function of this module is called with a single parameter (normally `this` inside a custom element)
|
|
128
|
-
and it finds the parent. Its useful for custom elements to sit at the top level of the custom element above and listen for
|
|
129
|
-
events bubbling up from below. It uses this function to find the element to add an event listener to.
|
|
130
|
-
|
|
131
|
-
# master-tab-promise
|
|
132
|
-
|
|
133
|
-
the default function returns a promise, which when resolves will tell you if you are the first (and therefore master) tab in a particular application.
|
|
134
|
-
|
|
135
|
-
if the master tab closes an custom Event 'master-close' is dispatched on the window. You can use that to recheck the promise to see if you (or perhaps some other tab also still open is now master);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# pdf
|
|
99
|
+
This is a utility function to determine the correct foreground colour (black or white) to use with a background colour
|
|
100
|
+
passed as the parameter (as a hex strings with an optional proceeding `#` symbol.). The colour is returned as either `#000000` or `#ffffff`
|
|
139
101
|
|
|
140
|
-
|
|
102
|
+
## config and related
|
|
141
103
|
|
|
142
|
-
|
|
104
|
+
By importing the **config** constant you are infact resolving from promise (this `import` statement does the resolving) to make an api call to the server `/api/config` to return config data as an object. What is returned is obviously server dependant. It has two independant functions. From an application point of view `import {config} from '@akc42/app-utils` produces a config constant with the config data set.
|
|
143
105
|
|
|
144
|
-
|
|
106
|
+
Additional support is provided with
|
|
107
|
+
|
|
108
|
+
1. **setConfig** which can optionally be given a promise which resolves to a new config (useful in testing scenarios), or causes a re-read of the config from the server, although it does not return this reread..
|
|
109
|
+
2. **reReadConfig** actualy causes a re-read from the server and returns the promise for its eventual resolution (i.e
|
|
110
|
+
`const config = await reReadConfig()`).
|
|
145
111
|
|
|
146
|
-
|
|
112
|
+
## csv
|
|
147
113
|
|
|
148
|
-
|
|
149
|
-
In doing this it can traverse inside custom elements looking for input elements. It will also check for custom elements and if they have both a name and value property if can pretend to be in input. Also slots are also traversed for all assigned nodes.
|
|
114
|
+
the modules default export is a function which can be called with a name and optionally a parameters object. The name is formed into url to `/api/csv/<name>` to make a download request and if they exist the parameters object is turned into a query string. The response from that uri is downloaded (expected to be a csv file).
|
|
150
115
|
|
|
116
|
+
## debug
|
|
151
117
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
118
|
+
The purpose of this module is to provide a debugable capability which can be
|
|
119
|
+
dynamically switched on and off browser by setting a key in the config
|
|
120
|
+
returned by the server. It will post request to `/api/debuglog/:immediate` url with an
|
|
121
|
+
`application/json` body part containing debug data. Its purpose is to emulate what it's
|
|
122
|
+
server counterpart does locally.
|
|
123
|
+
|
|
124
|
+
It consists of two functions **Debug** and **Logger**.
|
|
125
|
+
|
|
126
|
+
**Debug** creates an instance of a debug function and its called with parameters:
|
|
127
|
+
|
|
128
|
+
- **topic** - a value that can be searched for. Useful for dividing into different sections
|
|
129
|
+
- **colourspec** - One of name of standard colors [app,db,api,client,log,mail,auth,error],
|
|
130
|
+
a hex color string, an rgb, comma seperated, string of three values 0-255
|
|
131
|
+
- **shortdate** - if true, then dates will be output as YYYY-MM-DD hh:mm else
|
|
132
|
+
YYYY-MM-DD hh:mm:ss.ms
|
|
133
|
+
- **immediate** - if true, the message is output (formatted) to the console, both in the
|
|
134
|
+
browser and in the server.
|
|
135
|
+
|
|
136
|
+
Calling this function returns a function that will send a row to the server (for writing into the log there), using the
|
|
137
|
+
parameters above and some optional extra values these extra parameters (just skip if not provided, the function
|
|
138
|
+
dynamically checks them) are:-
|
|
139
|
+
|
|
140
|
+
- **crash** - the literal word "crash". if set, then this will be highlighted in the output. Don't provide this as
|
|
141
|
+
the first parameter if a normal call
|
|
142
|
+
- **logtime**- a unix millisecond timestamp. If provided if must be for today, otherwise it will be as
|
|
143
|
+
though it were not provided. If provided it will be the `logtime`, otherwise `Date.now()` will be used.
|
|
144
|
+
- **ipaddress** - an optional parameter container a string representation of an ip address. Ignored if not
|
|
145
|
+
a valid adddress. If provided its value will be highlighted and surrounded in "[]"
|
|
146
|
+
- **...messages** - As many parameters containing parts of the message. The message will be joined together
|
|
147
|
+
with a space separation and displayed with the colourspec parameter.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
**Logger** is like Debug (indeed its a wrapper for it) except it doesn't need short date, or immediate parameters as
|
|
152
|
+
that is what is assumed.
|
|
153
|
+
|
|
154
|
+
## dom-host
|
|
155
|
+
|
|
156
|
+
The **domHost** function is called with a single parameter (normally `this` inside a custom element)
|
|
157
|
+
and it finds the parent. Its useful for custom elements to sit at the top level of the custom element above and listen for
|
|
158
|
+
events bubbling up from below. It uses this function to find the element to add an event listener to.
|
|
159
|
+
|
|
160
|
+
## Client Side Routing.
|
|
161
|
+
|
|
162
|
+
A Distributed Client Side Router for use with a hierarchy of custom components in an Single Page Application. The
|
|
163
|
+
functions of this subsystem links the browsers URL bar into a chained list of `Route` objects that process a part of the
|
|
164
|
+
segmented url. It takes in a `route` object and passes out a `subRoute` object. These are chained together, the
|
|
165
|
+
subRoute at one level being fed into route of the next level down the hierarchy.
|
|
166
|
+
|
|
167
|
+
At the top level is a pair of functions **connectUrl** and **disconnectUrl**. Which ever element will be your master
|
|
168
|
+
controller (generally a `<main-app>` element which has a `<session-manager>` and `<page-manager>`, both of which are
|
|
169
|
+
controlling pages). A `<session-manager>` page doesn't reflect in the url bar at all as the user navigates the log in
|
|
170
|
+
process. But once authorised, the `<page-manager>` takes over and controls which page is displayed based on the url. So
|
|
171
|
+
it is the `<page-manager>` custom element that calls **connectUrl** in its `connectedCallback` function, and
|
|
172
|
+
**disconnectUrl** in its `disconnectedCallback` function. **connectUrl** uses a callback function as its only parameter and
|
|
173
|
+
this callback function gets called on url change, passing the top level `route` object.
|
|
174
|
+
|
|
175
|
+
The next piece in this arrangement is a router. This is a class called **Route** and is instanciated with one required
|
|
176
|
+
parameter and one optional parameter. The required parameter is a string containing "/" separated segments, which must
|
|
177
|
+
either literally match the part of the url, or can start with a ":" character followed by a name, in which case we
|
|
178
|
+
assume that that part of the url should be interpreted as a parameter. We process a new `route` (however we receive it
|
|
179
|
+
- either via the `connectUrl` callback, or being passed into a custom element via a property/attribute) by calling the
|
|
180
|
+
`routeChange` method, this returns a `route` object which the part of the url segment checked against the specification
|
|
181
|
+
provdied in the `new Route()` call. `route` has an `active` property to determine if it matched and a `params` property
|
|
182
|
+
the value of any of the ":" segments. Any queryString is also decoded and placed in the `query` property of objects.
|
|
183
|
+
|
|
184
|
+
If the `active` propery of a route is false, the subRoute will also have an `active` value of false. A `query` property
|
|
185
|
+
is always passed straight through and it is up to the application to decided how and when to use it.
|
|
186
|
+
|
|
187
|
+
The optional second parameter to the `new Route()` call is a matching string for the previous route up the chain. It
|
|
188
|
+
consists of a string which contains a single ":" character. The left of the ":" character is a parameter name, and to
|
|
189
|
+
the right a parameter value. The incoming route's `params` property must contain the "name" and it must have the value
|
|
190
|
+
"value" for the subRoute to be active (as well as matching the url).
|
|
185
191
|
|
|
186
192
|
This is usually used with something like this
|
|
187
|
-
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
188
195
|
const topLevel = new Route('/:page');
|
|
189
196
|
const firstLevel = new Route('/:id', 'page:appointments');
|
|
190
197
|
connectUrl(route => {
|
|
@@ -201,13 +208,15 @@ This is usually used with something like this
|
|
|
201
208
|
});
|
|
202
209
|
|
|
203
210
|
```
|
|
204
|
-
(I have simplified what happens - subRoute would probably be passed in as the `route` property to a custom element which
|
|
205
|
-
read a database record based on id).
|
|
211
|
+
(I have simplified what happens - subRoute would probably be passed in as the `route` property to a custom element which
|
|
212
|
+
might at some point want read a database record based on id).
|
|
206
213
|
|
|
207
|
-
In this example we only want to read the (lets say) the appointment record from the database if the
|
|
208
|
-
with a url of the form "/appointements/53" and not (say) when the url
|
|
214
|
+
In this example we only want to read the (lets say) the appointment record from the database if the
|
|
215
|
+
`<appointment-manager>` element had been activated with a url of the form "/appointements/53" and not (say) when the url
|
|
216
|
+
was "/user/53", when the `<user-manager>` element is in the dom and the `<appointment-manager>` is still in the dom, but
|
|
217
|
+
not doing anything. The other obvious question is why not do this:-
|
|
209
218
|
|
|
210
|
-
```
|
|
219
|
+
```javascript
|
|
211
220
|
const firstLevel = new Route('/appointments/:id');
|
|
212
221
|
connectUrl(route => {
|
|
213
222
|
const subRoute = topLevel.routeChange(route);
|
|
@@ -225,41 +234,57 @@ with a url of the form "/appointements/53" and not (say) when the url was "/user
|
|
|
225
234
|
```
|
|
226
235
|
and the answer to that is that I have an element `<route-manager>` which in fact something like `<page-manager>` extends
|
|
227
236
|
which then allows me to do (in `lit-element`s `render` function)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
${
|
|
240
|
+
{
|
|
241
|
+
home: html`<app-home></app-home>`,
|
|
242
|
+
user: html`<app-user managed-page .route=${this.subRoute}></app-user>`,
|
|
243
|
+
appointments: html`<app-appointments managed-page .route=${this.subRoute}></app-appointments`
|
|
244
|
+
}[this.page]
|
|
245
|
+
}
|
|
235
246
|
```
|
|
236
247
|
|
|
237
248
|
The route manager users `new Route('/:page')` to translate the incoming `route` to the `page` property.
|
|
238
249
|
|
|
239
|
-
Internally the `Route` class uses a `route-changed` event which this overall module listens to on the window and this
|
|
240
|
-
the `Route` class has three properties that can be set and which can change the url.
|
|
250
|
+
Internally the `Route` class uses a `route-changed` event which this overall module listens to on the window and this
|
|
251
|
+
can be used to change the url. the `Route` class has three properties that can be set and which can change the url.
|
|
241
252
|
|
|
242
|
-
- `connection` which if set `true` join the input and output of the route managed by this instance provided only that
|
|
243
|
-
and change the url accordingly. If set to `false` it will always make the
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
- `connection` which if set `true` join the input and output of the route managed by this instance provided only that
|
|
254
|
+
the route doesn't have any ":" segment, and change the url accordingly. If set to `false` it will always make the
|
|
255
|
+
output disconnected.
|
|
256
|
+
- `params` which when set with an object which maps the properties of an active `params` in the `subRoute` will change
|
|
257
|
+
the url - so for instance in the example above calling `firstlevel.params = {id: 20}` will change the url to
|
|
258
|
+
`/appointments/20`.
|
|
246
259
|
- `query` we can set a query set of parameters and these will then change the url to have those query parameters.
|
|
247
260
|
|
|
248
|
-
Other modules that wish to change the url can do so, but they need to dispatch a `location-altered` event in the window.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
Other modules that wish to change the url can do so, but they need to dispatch a `location-altered` event in the window.
|
|
262
|
+
A helper function **switchPath** can do this for you. It takes two parameters, `path` and a `params` object, the latter
|
|
263
|
+
of which is formed into a query string and appended (with the appropriate "?") into a string. A helper function
|
|
264
|
+
**generateUri** takes the same two parameters and creates the final url, it just doesn't apply the result.
|
|
265
|
+
|
|
266
|
+
**navigate** is a function to be used as an event handler on an element (normally for `@click`). It calls `switchPath` with the
|
|
267
|
+
`path` set to the `path` attribute of the element which fired the event.
|
|
268
|
+
|
|
269
|
+
## master-tab-promise
|
|
270
|
+
|
|
271
|
+
The **getMasterTabPromise** function returns a promise, which when resolves will tell you if you are the first (and therefore master) tab in a particular application.
|
|
272
|
+
|
|
273
|
+
if the master tab closes an custom Event 'master-close' is dispatched on the window. You can use that to recheck the promise to see if you (or perhaps some other tab also still open is now master);
|
|
274
|
+
|
|
275
|
+
## partMap
|
|
276
|
+
|
|
277
|
+
**partMap** is a `lit` "directive", exactly analogous to the provided `classMap`, but for the `part` attribute. Use it in the same way to dynamically assign parts to an element.
|
|
278
|
+
|
|
279
|
+
## pdf
|
|
255
280
|
|
|
256
|
-
|
|
281
|
+
the modules default export is a function which can be called with a name and optionally a parameters object. The name is added to `/api/pdf/` to post a message to the server, expecting it to stream a precreated, or created on the fly pdf document (as Blob). This is opened in a new window.
|
|
257
282
|
|
|
258
|
-
The default export, normally named config (ie the client does `import config from @akc42/app-utils/config.js`) and allows the normal await mechanisms in `import` to
|
|
259
|
-
allow this module to make an get request to the server of `/api/config` and for the server to return it. The `config` variable then holds (without any further waiting)
|
|
260
|
-
the client config object returned by the server.
|
|
261
283
|
|
|
262
|
-
|
|
263
|
-
that promise to be resolved with a config (useful in testing). The other async function is `reReadConfig` with essentially causes the config to be reRead, but does also returns a promise resolving to the config.
|
|
284
|
+
## submit-function
|
|
264
285
|
|
|
286
|
+
Acts as the on-submit handler for a form. But instead of allowing the form to send itself it creates an ajax request
|
|
287
|
+
with all the correct parameters. In doing this it can traverse inside custom elements looking for input elements. It
|
|
288
|
+
will also check for custom elements and if they have both a name and value property if can pretend to be in input. Also
|
|
289
|
+
slots are also traversed for all assigned nodes.
|
|
265
290
|
|
package/api.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@licence
|
|
3
|
+
Copyright (c) 2020 Alan Chandler, all rights reserved
|
|
4
|
+
|
|
5
|
+
This file is part of @akc42/app-utils.
|
|
6
|
+
|
|
7
|
+
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import Debug from './debug.js';
|
|
22
|
+
|
|
23
|
+
const debug = Debug('client-api', 'api');
|
|
24
|
+
|
|
25
|
+
class ApiError extends Error {
|
|
26
|
+
constructor(address, code) {
|
|
27
|
+
super('API Error: ' + address)
|
|
28
|
+
this.name = 'API' + code.toString();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function api(url, params, bl) {
|
|
33
|
+
const blob = bl;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const address = '/api/' + url;
|
|
36
|
+
const options = {
|
|
37
|
+
mode: 'cors',
|
|
38
|
+
credentials: 'include',
|
|
39
|
+
method: 'post',
|
|
40
|
+
headers: new Headers({
|
|
41
|
+
'content-type': 'application/json'
|
|
42
|
+
}),
|
|
43
|
+
body: new Blob([JSON.stringify(params ?? {})], { type: 'application/json' }),
|
|
44
|
+
signal: controller.signal
|
|
45
|
+
};
|
|
46
|
+
let text;
|
|
47
|
+
const timer = setTimeout(() => controller.abort('timeout'),60000); //just a protection against complete loss of response.
|
|
48
|
+
try {
|
|
49
|
+
const response = await window.fetch(address, options)
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
if (blob) {
|
|
53
|
+
text = '---502---'; //Simulate a 502 (bad gateway) incase there is an error in following.
|
|
54
|
+
const b = await response.blob();
|
|
55
|
+
window.open(
|
|
56
|
+
URL.createObjectURL(b),
|
|
57
|
+
'_blank',
|
|
58
|
+
'chrome=yes,centerscreen,resizable,scrollbars,status,height=800,width=800');
|
|
59
|
+
|
|
60
|
+
document.body.dispatchEvent(new CustomEvent('wait-request',{bubbles: true, composed: true, detail: false}));
|
|
61
|
+
debug('request returned blob');
|
|
62
|
+
return {};
|
|
63
|
+
} else {
|
|
64
|
+
text = await response.text();
|
|
65
|
+
if (text.length > 0) {
|
|
66
|
+
const j = JSON.parse(text);
|
|
67
|
+
document.body.dispatchEvent(new CustomEvent('wait-request',{bubbles: true, composed: true, detail: false}));
|
|
68
|
+
debug('request returned JSON', text);
|
|
69
|
+
return j;
|
|
70
|
+
}
|
|
71
|
+
document.body.dispatchEvent(new CustomEvent('wait-request',{bubbles: true, composed: true, detail: false}));
|
|
72
|
+
debug('empty return');
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
} else if (response.status < 500) throw new ApiError(address, response.status);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
//no debug needed - will report at where its thrown
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
if (!(err instanceof TypeError)) {
|
|
81
|
+
//not network failure so no retry
|
|
82
|
+
if (err instanceof SyntaxError) {
|
|
83
|
+
const code = Number((text?? '---502---').slice(-6, -3));
|
|
84
|
+
if (code > 299) throw new ApiError(address,code);
|
|
85
|
+
} else if (err.name === 'AbortError') throw new ApiError(address, 504); //simulate gateway timeout
|
|
86
|
+
throw err; //just throw what we have
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { api , ApiError };
|
|
94
|
+
|
package/app-utils.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {api, ApiError} from './api.js';
|
|
2
|
+
import AppKeys from './app-keys.js';
|
|
3
|
+
import calcTextColor from './colour.js';
|
|
4
|
+
import config, {setConfig, reReadConfig}from './config.js'
|
|
5
|
+
import csv from './csv.js';
|
|
6
|
+
import {Debug,Logger} from './debug.js';
|
|
7
|
+
import domHost from './dom-host.js';
|
|
8
|
+
import { connectUrl, disconnectUrl} from './location.js';
|
|
9
|
+
import getMasterTabPromise from './master-tab-promise.js';
|
|
10
|
+
import {partMap} from './partMap.js';
|
|
11
|
+
import pdf from './pdf.js';
|
|
12
|
+
import Route from './route.js';
|
|
13
|
+
import submit from './submit-function.js';
|
|
14
|
+
import {switchPath, generateUri, nagivate} from './switch-path.js';
|
|
15
|
+
|
|
16
|
+
export {api,ApiError,AppKeys,calcTextColor,config, connectUrl, csv, Debug, disconnectUrl, domHost, generateUri,
|
|
17
|
+
getMasterTabPromise, Logger, navigate, partMap, pdf, reReadConfig, Route,setConfig, submit, switchPath };
|
package/colour.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@licence
|
|
3
|
+
Copyright (c) 2023 Alan Chandler, all rights reserved
|
|
4
|
+
|
|
5
|
+
This file is part of @akc42/app-utils.
|
|
6
|
+
|
|
7
|
+
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
/*
|
|
21
|
+
Given a background colour, this function calculates what the color string from
|
|
22
|
+
the text should be. Backgroundcolour should be a hex string, optionally
|
|
23
|
+
proceeded by #
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
function calcTextColor(backgroundColor) {
|
|
27
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(backgroundColor);
|
|
28
|
+
if (result) {
|
|
29
|
+
const luminance = (0.2126 * parseInt(result[1], 16) + 0.7152 * parseInt(result[2], 16) + 0.0722 * parseInt(result[3], 16));
|
|
30
|
+
return (luminance < 140) ? "#ffffff" : "#000000";
|
|
31
|
+
}
|
|
32
|
+
return "#000000"
|
|
33
|
+
}
|
|
34
|
+
export default calcTextColor;
|
package/debug.js
CHANGED
|
@@ -19,138 +19,54 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
22
|
-
|
|
23
|
-
by setting sessionStorage value 'debug' to the config returned by the server. It will post request to '/api/debuglog'
|
|
24
|
-
url with application/json body part containing message, topic and gap, where message is the concatenation of the debug
|
|
25
|
-
parameters separated by space, topic is the topic of this debug message and gap is the number of milliseconds since
|
|
26
|
-
the last debug message with the same topic.
|
|
22
|
+
import {Debug} from '@akc42/app-utils'
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
the client and if its the same topic then the gap will be since the last call
|
|
30
|
-
from ANY module.
|
|
24
|
+
Debug creates an instance of a debug function
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
parameters:
|
|
27
|
+
topic - a value that can be searched for. Useful for dividing into different sections
|
|
28
|
+
colourspec - One of name of standard colors [app,db,api,client,log,mail,auth,error], a hex color string, an rgb,
|
|
29
|
+
comma seperated, string of three values 0-255
|
|
30
|
+
shortdate - if true, then dates will be output as YYYY-MM-DD hh:mm else YYYY-MM-DD hh:mm:ss.ms
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const debug = Debug('topic') topic should only contain the characters a-z or
|
|
38
|
-
A-Z as is converted to lowercase.
|
|
39
|
-
|
|
40
|
-
debug(..messages) //messages will be concatenated by space
|
|
41
|
-
|
|
42
|
-
the debug function will only log the message on the server if sessionStorage "debug" is set to a string which is a colon separated list of topics
|
|
43
|
-
and that list has the topic for this debug call in it.
|
|
44
|
-
|
|
45
|
-
NOTE: It is normally expected for the server to provide a mechanism to update
|
|
46
|
-
the confgi before it is returned, The main app then overwrites sessionStorage 'debug' item with a new list of topics when you want
|
|
47
|
-
debug to switch on and off dynamically.
|
|
48
|
-
|
|
49
|
-
regardless of whether the message is logged on the server, it is also added to the performance.mark buffer
|
|
50
|
-
so that it can be sent to the server on a crash.
|
|
51
|
-
|
|
52
|
-
Although Debug is the default export this module also provides the following named exports
|
|
32
|
+
immediate - if set, the message is output (formatted) to the console.
|
|
53
33
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
Returns a function that will send a row to the server (for writing into the log there), using the parameters above and
|
|
35
|
+
some optional extra values these extra parameters are
|
|
36
|
+
|
|
37
|
+
crash - the literal word "crash". if set, then this will be highlighted in the output. Don't provide this as
|
|
38
|
+
the first parameter if a normal call
|
|
39
|
+
logtime - a unix millisecond timestamp. If provided if must be for today, otherwise it will be as
|
|
40
|
+
though it were not provided. If provided it will be the logtime, otherwise "Now" will be used.
|
|
41
|
+
ipaddress - an optional parameter container a string representation of an ip address. Ignored if not
|
|
42
|
+
a valid adddress. If provided its value will be highlighted and surrounded in "[]"
|
|
43
|
+
...messages - As many parameters containing parts of the message. The message will be joined together
|
|
44
|
+
with a space separation and displayed with the colourspec parameter.
|
|
61
45
|
*/
|
|
62
46
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let buffer = []; //buffer of up to 50 topic/message pairs to allow us to hold some if resource buffer becomes full;
|
|
66
|
-
|
|
67
|
-
const topicMap = new Map();
|
|
68
|
-
|
|
69
|
-
let initialised = false;
|
|
70
|
-
|
|
47
|
+
/*
|
|
48
|
+
Logger is like Debug (indeed its a wrapper for it) except
|
|
71
49
|
|
|
72
|
-
|
|
73
|
-
const entries = performance.getEntriesByType('mark');
|
|
74
|
-
performance.clearMarks();
|
|
75
|
-
const startPoint = entries.length - BUFFER_SIZE;
|
|
76
|
-
buffer.splice(0, buffer.length + startPoint);
|
|
77
|
-
for (let i = 0; i < entries.length; i++) {
|
|
78
|
-
if (entries[i].name === KEY_TOPIC || i >= startPoint) {
|
|
79
|
-
buffer.push({ topic: entries[i].name, message: entries[i].detail, time: Math.round(entries[i].startTime) });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
50
|
+
It doesn't need short date, or immediate parameters as thats whats assumed
|
|
83
51
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
buffer = [];
|
|
87
|
-
performance.setResourceTimingBufferSize(150)
|
|
88
|
-
window.addEventListener('resourcetimingbufferfull', bufferFull)
|
|
89
|
-
}
|
|
52
|
+
*/
|
|
53
|
+
import {DebugHelper, messageFormatter} from '@akc42/server-utils';
|
|
90
54
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
buffer.reverse();
|
|
97
|
-
let message = '';
|
|
98
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
99
|
-
message += `(${buffer[i].topic}) ${buffer[i].message} :gap ${i < buffer.length - 1 ? buffer[i].time - buffer[i + 1].time : 0}ms\n`;
|
|
55
|
+
export function Logger(topic, colourspec) {
|
|
56
|
+
const debug = Debug(topic, colourspec, 1,1);
|
|
57
|
+
return function(c, ip, ...args) {
|
|
58
|
+
const output = debug(c,ip,args);
|
|
59
|
+
console.log(output.message);
|
|
100
60
|
}
|
|
101
|
-
const blob = new Blob([JSON.stringify({
|
|
102
|
-
message: message
|
|
103
|
-
})], { type: 'application/json' });
|
|
104
|
-
navigator.sendBeacon(`/api/debuglog/clientpath`, blob);
|
|
105
|
-
|
|
106
|
-
buffer = []; //we will start our buffer from scratch again
|
|
107
|
-
|
|
108
61
|
}
|
|
109
62
|
|
|
63
|
+
export function Debug(topic, colourspec, shortdate, immediate = false) {
|
|
64
|
+
return DebugHelper(topic, colourspec, shortdate, immediate , writer)
|
|
110
65
|
|
|
111
|
-
function Debug(t) {
|
|
112
|
-
if (typeof t !== 'string' || t.length === 0 || !/^[a-zA-Z]+$/.test(t)) {
|
|
113
|
-
console.error('Debug requires topic which is a non zero length string of only letters', t, 'Received');
|
|
114
|
-
throw new Error('Invalid Debug Topic');
|
|
115
|
-
}
|
|
116
|
-
const tl = t.toLowerCase();
|
|
117
|
-
if (topicMap.has(tl)) {
|
|
118
|
-
const topic = topicMap.get(tl);
|
|
119
|
-
return topic.debug;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const topicHandler = {
|
|
123
|
-
topic: tl,
|
|
124
|
-
timestamp: new Date().getTime(),
|
|
125
|
-
defined: false, //has the config been defined yet
|
|
126
|
-
debug: function (...args) {
|
|
127
|
-
//do time calc before potential delay to see if we are enabled
|
|
128
|
-
const now = new Date().getTime();
|
|
129
|
-
const gap = now - this.timestamp;
|
|
130
|
-
this.timestamp = now;
|
|
131
|
-
const message = args.reduce((cum, arg) => {
|
|
132
|
-
return `${cum} ${arg}`.trim();
|
|
133
|
-
}, '');
|
|
134
|
-
if (initialised) performance.mark(this.topic, { detail: message }); //save our message locally regardless of if enabled
|
|
135
|
-
let enabled = false;
|
|
136
|
-
const debugConf = sessionStorage.getItem('debug');
|
|
137
|
-
if (debugConf) {
|
|
138
|
-
const topics = debugConf.split(':');
|
|
139
|
-
if (topics.includes(this.topic)) enabled = true;
|
|
140
|
-
}
|
|
141
|
-
if (enabled) {
|
|
142
|
-
const blob = new Blob([JSON.stringify({
|
|
143
|
-
message: message,
|
|
144
|
-
gap: gap
|
|
145
|
-
})], { type: 'application/json' });
|
|
146
|
-
|
|
147
|
-
navigator.sendBeacon(`/api/debuglog/${this.topic}`, blob);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
topicHandler.debug = topicHandler.debug.bind(topicHandler);
|
|
152
|
-
topicMap.set(topicHandler.topic, topicHandler);
|
|
153
|
-
return topicHandler.debug
|
|
154
66
|
}
|
|
155
67
|
|
|
156
|
-
|
|
68
|
+
function writer (logtime, crash, shortdate,ip, topic, message, colourspec,gap, i) {
|
|
69
|
+
const blob = new Blob([JSON.stringify({logtime,crash, shortdate, topic,message,colourspec,gap})], { type: 'application/json' });
|
|
70
|
+
navigator.sendBeacon(`/api/debuglog/${i ? 1: 0}`, blob);
|
|
71
|
+
return messageFormatter('no id', logtime,crash,shortdate,ip,topic,message,colourspec,gap)
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akc42/app-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "General Utilities for SPAs",
|
|
5
|
-
"
|
|
6
|
-
".": "./*.js"
|
|
7
|
-
},
|
|
5
|
+
"mains": "app-utils.js",
|
|
8
6
|
"scripts": {
|
|
9
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
8
|
},
|
|
@@ -20,5 +18,9 @@
|
|
|
20
18
|
"bugs": {
|
|
21
19
|
"url": "https://github.com/akc42/app-utils/issues"
|
|
22
20
|
},
|
|
23
|
-
"homepage": "https://github.com/akc42/app-utils#readme"
|
|
21
|
+
"homepage": "https://github.com/akc42/app-utils#readme",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@akc42/server-utils": "^3.4.2",
|
|
24
|
+
"lit": "^3.3.1"
|
|
25
|
+
}
|
|
24
26
|
}
|
package/partMap.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@licence
|
|
3
|
+
Copyright (c) 2021 Alan Chandler, all rights reserved
|
|
4
|
+
|
|
5
|
+
This file is part of @akc42/app-utils.
|
|
6
|
+
|
|
7
|
+
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Much of this code is copied (as a model of what to do) from the classMap directive in Googles lit package.
|
|
22
|
+
* Copyright 2018 Google LLC
|
|
23
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {noChange} from 'lit'
|
|
27
|
+
import {directive,Directive,PartType} from 'lit/directive';
|
|
28
|
+
|
|
29
|
+
class PartMapDirective extends Directive {
|
|
30
|
+
|
|
31
|
+
constructor(partInfo) {
|
|
32
|
+
super(partInfo);
|
|
33
|
+
|
|
34
|
+
if (partInfo.type !== PartType.ATTRIBUTE || partInfo.name !== 'part' || partInfo.strings?.length > 2) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'`partMap()` can only be used in the `part` attribute ' +
|
|
37
|
+
'and must be the only part in the attribute.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render(partInfo) {
|
|
43
|
+
// Add spaces to ensure separation from static parts
|
|
44
|
+
return (
|
|
45
|
+
' ' +
|
|
46
|
+
Object.keys(partInfo)
|
|
47
|
+
.filter((key) => partInfo[key])
|
|
48
|
+
.join(' ') +
|
|
49
|
+
' '
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
update(part, [partInfo]) {
|
|
54
|
+
// Remember dynamic parts on the first render
|
|
55
|
+
if (this._previousParts === undefined) {
|
|
56
|
+
this._previousParts = new Set();
|
|
57
|
+
if (part.strings !== undefined) {
|
|
58
|
+
this._staticParts = new Set(
|
|
59
|
+
part.strings
|
|
60
|
+
.join(' ')
|
|
61
|
+
.split(/\s/)
|
|
62
|
+
.filter((s) => s !== '')
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
for (const name in partInfo) {
|
|
66
|
+
if (partInfo[name] && !this._staticParts?.has(name)) {
|
|
67
|
+
this._previousParts.add(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return this.render(partInfo);
|
|
71
|
+
}
|
|
72
|
+
let changed = false;
|
|
73
|
+
// Remove old classes that no longer apply
|
|
74
|
+
for (const name of this._previousParts) {
|
|
75
|
+
if (!(name in partInfo)) {
|
|
76
|
+
this._previousParts.delete(name);
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add or remove classes based on their classMap value
|
|
82
|
+
for (const name in partInfo) {
|
|
83
|
+
// We explicitly want a loose truthy check of `value` because it seems
|
|
84
|
+
// more convenient that '' and 0 are skipped.
|
|
85
|
+
const value = !!partInfo[name];
|
|
86
|
+
if (
|
|
87
|
+
value !== this._previousParts.has(name) &&
|
|
88
|
+
!this._staticParts?.has(name)
|
|
89
|
+
) {
|
|
90
|
+
changed = true;
|
|
91
|
+
if (value) {
|
|
92
|
+
this._previousParts.add(name);
|
|
93
|
+
} else {
|
|
94
|
+
this._previousParts.delete(name);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (changed) return this.render(partInfo);
|
|
99
|
+
return noChange;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A directive that applies dynamic parts
|
|
105
|
+
*
|
|
106
|
+
* This must be used in the `part` attribute and must be the only part used in
|
|
107
|
+
* the attribute. It takes each property in the `partInfo` argument and adds
|
|
108
|
+
* the property name to the element's `part` if the property value is
|
|
109
|
+
* truthy; if the property value is falsy, the property name is removed from
|
|
110
|
+
* the element's `part`.
|
|
111
|
+
*
|
|
112
|
+
* For example `{foo: bar}` applies the part name `foo` if the value of `bar` is
|
|
113
|
+
* truthy.
|
|
114
|
+
*
|
|
115
|
+
* @param partInfo
|
|
116
|
+
*/
|
|
117
|
+
export const partMap = directive(PartMapDirective);
|
package/switch-path.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
export function generateUri(path, params) {
|
|
22
22
|
var str = [];
|
|
23
23
|
if (params) {
|
|
24
|
-
for (
|
|
24
|
+
for (const param in params) {
|
|
25
25
|
//eslint-disable-next-line no-prototype-builtins
|
|
26
26
|
if (params.hasOwnProperty(param)) {
|
|
27
27
|
str.push(encodeURIComponent(param) + '=' + encodeURIComponent(params[param]));
|
|
@@ -38,5 +38,10 @@ export function switchPath(path, params) {
|
|
|
38
38
|
history.pushState({}, null, generateUri(path, params));
|
|
39
39
|
window.dispatchEvent(new CustomEvent('location-altered', { composed: true, bubbles: true }));
|
|
40
40
|
}
|
|
41
|
-
export default switchPath;
|
|
42
41
|
|
|
42
|
+
export function navigate (e) {
|
|
43
|
+
const link = e.currentTarget.getAttribute('path');
|
|
44
|
+
if (typeof link !== 'undefined') {
|
|
45
|
+
switchPath(link);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/post-api.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
@licence
|
|
3
|
-
Copyright (c) 2020 Alan Chandler, all rights reserved
|
|
4
|
-
|
|
5
|
-
This file is part of @akc42/app-utils.
|
|
6
|
-
|
|
7
|
-
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
-
it under the terms of the GNU General Public License as published by
|
|
9
|
-
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
-
(at your option) any later version.
|
|
11
|
-
|
|
12
|
-
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
-
GNU General Public License for more details.
|
|
16
|
-
|
|
17
|
-
You should have received a copy of the GNU General Public License
|
|
18
|
-
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export default async function api(url, params, blob, signal) {
|
|
23
|
-
const address = '/api/' + url;
|
|
24
|
-
const options = {
|
|
25
|
-
credentials: 'same-origin',
|
|
26
|
-
method: 'post',
|
|
27
|
-
headers: new Headers({
|
|
28
|
-
'content-type': 'application/json'
|
|
29
|
-
}),
|
|
30
|
-
body: JSON.stringify(params ?? {})
|
|
31
|
-
};
|
|
32
|
-
performance.mark('fetchapi', { detail: `${address} params: ${options.body}` })
|
|
33
|
-
if (signal) options.signal = signal;
|
|
34
|
-
let text;
|
|
35
|
-
try {
|
|
36
|
-
const response = await window.fetch(address, options);
|
|
37
|
-
if (!response.ok) throw new CustomEvent('api-error', {composed: true, bubbles: true , detail:{status:response.status, url:address}});
|
|
38
|
-
performance.mark('fetchdone', {detail: address});
|
|
39
|
-
performance.measure('apicalltime',{start: 'fetchapi', end:'fetchdone', detail: address});
|
|
40
|
-
if (blob) {
|
|
41
|
-
text = '---502---'; //Simulate a 502 (bad gateway) incase there is an error in following.
|
|
42
|
-
const b = await response.blob();
|
|
43
|
-
window.open(
|
|
44
|
-
URL.createObjectURL(b),
|
|
45
|
-
'_blank',
|
|
46
|
-
'chrome=yes,centerscreen,resizable,scrollbars,status,height=800,width=800');
|
|
47
|
-
return {};
|
|
48
|
-
} else {
|
|
49
|
-
text = await response.text();
|
|
50
|
-
if (text.length > 0) return JSON.parse(text);
|
|
51
|
-
return {};
|
|
52
|
-
}
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (!options.signal || !options.signal.aborted) {
|
|
55
|
-
if (err instanceof TypeError) throw new CustomEvent('api-network', { bubbles:true, composed:true, detail: address})
|
|
56
|
-
if (err.type === 'api-error') throw err; //just throw whatever error we had
|
|
57
|
-
//we failed to parse the json - the actual code should be in the text near the end;
|
|
58
|
-
throw new CustomEvent('api-error', { composed: true, bubbles: true, detail: {status:parseInt((text?? '---502---').slice(-6, -3), 10), url:address }});
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|