saasufy-components 1.7.2 → 1.9.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 +22 -9
- package/app-router.js +90 -8
- package/log-in-form.js +104 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -7,6 +7,11 @@ Coming soon.
|
|
|
7
7
|
|
|
8
8
|
## Components
|
|
9
9
|
|
|
10
|
+
### socket-provider
|
|
11
|
+
|
|
12
|
+
A top level component which connects to your Saasufy service and inside which you can place other components which depends on Saasufy data.
|
|
13
|
+
A Saasufy component which integrates with data from Saasufy is known as a `socket-consumer` and must always be placed inside a `socket-provider` element (although it does not have to be a direct child).
|
|
14
|
+
|
|
10
15
|
### collection-adder
|
|
11
16
|
|
|
12
17
|
A form component for inserting data into collections.
|
|
@@ -15,35 +20,43 @@ A form component for inserting data into collections.
|
|
|
15
20
|
|
|
16
21
|
Used for rendering collections as lists, tables or other sequences based on a specific view using a template.
|
|
17
22
|
Supports pagination by allowing you to specify custom buttons or links to navigate between pages.
|
|
18
|
-
Can also perform basic CRUD operations such as deleting or creating records.
|
|
23
|
+
Can also perform basic CRUD operations such as deleting or creating records by listening for events from child components.
|
|
19
24
|
|
|
20
25
|
### collection-reducer
|
|
21
26
|
|
|
22
27
|
Similar to the `collection-browser` component but designed to render collections in combined format. For example, to combine values from multiple records into a single item.
|
|
23
|
-
A common use case is to extract and
|
|
28
|
+
A common use case is to extract and join values to pass to other child components via their attributes.
|
|
24
29
|
|
|
25
30
|
### model-input
|
|
26
31
|
|
|
27
|
-
Used for displaying and editing a field of a model instance in real time.
|
|
32
|
+
Used for displaying and editing a single field of a model instance in real time.
|
|
33
|
+
|
|
34
|
+
### model-text
|
|
35
|
+
|
|
36
|
+
Used to displaying a field of a model instance in real time.
|
|
28
37
|
|
|
29
38
|
### model-viewer
|
|
30
39
|
|
|
31
|
-
Used for rendering
|
|
40
|
+
Used for rendering a single model resource using a template. This is the single-model alternative to the `collection-browser` which works with collections of models.
|
|
32
41
|
|
|
33
42
|
### input-provider
|
|
34
43
|
|
|
35
44
|
An input component which can pass data to other components (or HTML elements) via custom attributes.
|
|
36
|
-
A common use case for it is to pass user input to `collection-browser` or `model-viewer` to
|
|
45
|
+
A common use case for it is to pass user input to `collection-browser` or `model-viewer` components to then fetch data from Saasufy.
|
|
37
46
|
This component can be configured to provide its data to multiple components (consumers) via custom attributes.
|
|
38
47
|
Note that an `input-provider` only targets elements which are downstream from its parent element in the HTML hierarchy.
|
|
39
48
|
|
|
40
|
-
###
|
|
49
|
+
### log-in-form
|
|
41
50
|
|
|
42
|
-
A
|
|
51
|
+
A form component which allows end-users to authenticate themselves into your app using the Saasufy blockchain or any Capitalisk-based blockchain.
|
|
52
|
+
On success, the socket of the form's `socket-provider` will be set to the authenticated state; this will then cause appropriate Saasufy components which share this `socket-provider` to reload themselves.
|
|
53
|
+
Once a user is authenticated, they will be able to access restricted data (as specified on the Saasufy `Model -> Access` page).
|
|
54
|
+
It's also possible to specify a `success-location-hash` attribute to trigger a client-side redirect upon successful authentication.
|
|
55
|
+
The change in the location hash can then be detected by an `app-router` to switch to a different page upon successful login.
|
|
43
56
|
|
|
44
|
-
###
|
|
57
|
+
### confirm-modal
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
A modal component to prompt the user for confirmation before performing sensitive operations.
|
|
47
60
|
|
|
48
61
|
### overlay-modal
|
|
49
62
|
|
package/app-router.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import { toSafeHTML } from './utils.js';
|
|
2
|
-
import {
|
|
1
|
+
import { toSafeHTML, renderTemplate, debouncer } from './utils.js';
|
|
2
|
+
import { SocketConsumer } from './socket.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const DEFAULT_DEBOUNCE_DELAY = 100;
|
|
5
|
+
|
|
6
|
+
export class AppRouter extends SocketConsumer {
|
|
5
7
|
constructor() {
|
|
6
8
|
super();
|
|
7
9
|
this.attachShadow({ mode: 'open' });
|
|
8
10
|
|
|
9
11
|
this.pages = {};
|
|
10
12
|
this.pageRouteInfos = [];
|
|
13
|
+
this.debounce = debouncer();
|
|
14
|
+
this.debounceDelay = DEFAULT_DEBOUNCE_DELAY;
|
|
11
15
|
|
|
12
16
|
this.hashStartRegex = /^#/;
|
|
13
17
|
|
|
14
18
|
this.handleHashChangeEvent = () => {
|
|
15
|
-
this.renderCurrentPage
|
|
19
|
+
this.debounce(this.renderCurrentPage, this.debounceDelay);
|
|
16
20
|
};
|
|
17
21
|
|
|
18
|
-
this.handleSlotChangeEvent = () => {
|
|
22
|
+
this.handleSlotChangeEvent = (event) => {
|
|
19
23
|
let pageTemplates = this.shadowRoot.querySelector('slot[name="page"]').assignedNodes();
|
|
20
24
|
|
|
21
25
|
for (let template of pageTemplates) {
|
|
@@ -30,8 +34,9 @@ export class AppRouter extends HTMLElement {
|
|
|
30
34
|
params: [ ...route.matchAll(/:[^\/]*/g) ].map(paramMatch => paramMatch[0].replace(':', ''))
|
|
31
35
|
};
|
|
32
36
|
});
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
if (!this.socket || this.socket.state === this.socket.OPEN) {
|
|
38
|
+
this.debounce(this.renderCurrentPage, this.debounceDelay);
|
|
39
|
+
}
|
|
35
40
|
};
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -43,15 +48,61 @@ export class AppRouter extends HTMLElement {
|
|
|
43
48
|
return Object.fromEntries(argEntries);
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
static get observedAttributes() {
|
|
52
|
+
return [
|
|
53
|
+
'default-page',
|
|
54
|
+
'debounce-delay'
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
59
|
+
if (name === 'debounce-delay') {
|
|
60
|
+
if (newValue) {
|
|
61
|
+
this.debounceDelay = Number(newValue);
|
|
62
|
+
} else {
|
|
63
|
+
this.debounceDelay = DEFAULT_DEBOUNCE_DELAY;
|
|
64
|
+
}
|
|
65
|
+
} else if (this.isReady) {
|
|
66
|
+
this.renderCurrentPage();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
connectedCallback() {
|
|
47
71
|
window.addEventListener('hashchange', this.handleHashChangeEvent);
|
|
48
72
|
this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Socket is optional as not all routing use cases require authentication.
|
|
76
|
+
this.socket = this.getSocket();
|
|
77
|
+
} catch (error) {}
|
|
78
|
+
if (this.socket) {
|
|
79
|
+
let hasProcessedSocketConnect = this.socket.state === this.socket.OPEN;
|
|
80
|
+
this.authStateChangeConsumer = this.socket.listener('authStateChange').createConsumer();
|
|
81
|
+
(async () => {
|
|
82
|
+
for await (let event of this.authStateChangeConsumer) {
|
|
83
|
+
if (hasProcessedSocketConnect) {
|
|
84
|
+
this.debounce(this.renderCurrentPage, this.debounceDelay);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
|
|
89
|
+
this.connectConsumer = this.socket.listener('connect').createConsumer();
|
|
90
|
+
(async () => {
|
|
91
|
+
for await (let event of this.connectConsumer) {
|
|
92
|
+
hasProcessedSocketConnect = true;
|
|
93
|
+
this.debounce(this.renderCurrentPage, this.debounceDelay);
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
}
|
|
49
97
|
this.render();
|
|
98
|
+
this.isReady = true;
|
|
50
99
|
}
|
|
51
100
|
|
|
52
101
|
disconnectedCallback() {
|
|
53
102
|
window.removeEventListener('hashchange', this.handleHashChangeEvent);
|
|
54
103
|
this.shadowRoot.removeEventListener('slotchange', this.handleSlotChangeEvent);
|
|
104
|
+
this.authStateChangeConsumer.kill();
|
|
105
|
+
this.connectConsumer.kill();
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
renderCurrentPage() {
|
|
@@ -66,7 +117,38 @@ export class AppRouter extends HTMLElement {
|
|
|
66
117
|
) || {};
|
|
67
118
|
|
|
68
119
|
let pageTemplate = this.pages[route];
|
|
69
|
-
if (!pageTemplate)
|
|
120
|
+
if (!pageTemplate) {
|
|
121
|
+
let defaultPage = this.getAttribute('default-page');
|
|
122
|
+
if (!defaultPage) return;
|
|
123
|
+
pageTemplate = this.pages[defaultPage];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let noAuthRedirect = pageTemplate.getAttribute('no-auth-redirect');
|
|
127
|
+
let authRedirect = pageTemplate.getAttribute('auth-redirect');
|
|
128
|
+
let hardRedirect = pageTemplate.hasAttribute('hard-redirect');
|
|
129
|
+
|
|
130
|
+
if (this.socket && (authRedirect || noAuthRedirect)) {
|
|
131
|
+
if (this.socket.authState === 'authenticated') {
|
|
132
|
+
if (authRedirect) {
|
|
133
|
+
if (hardRedirect) {
|
|
134
|
+
location.hash = authRedirect;
|
|
135
|
+
return;
|
|
136
|
+
} else {
|
|
137
|
+
pageTemplate = this.pages[authRedirect];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
if (this.socket.state !== this.socket.OPEN) return;
|
|
142
|
+
if (noAuthRedirect) {
|
|
143
|
+
if (hardRedirect) {
|
|
144
|
+
location.hash = noAuthRedirect;
|
|
145
|
+
return;
|
|
146
|
+
} else {
|
|
147
|
+
pageTemplate = this.pages[noAuthRedirect];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
70
152
|
|
|
71
153
|
let routeArgs;
|
|
72
154
|
if (route) {
|
package/log-in-form.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import '/node_modules/capitalisk-auth-client/dist/capitalisk-log-in.js';
|
|
2
|
+
import { SocketConsumer } from './socket.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_AUTH_TIMEOUT = 10000;
|
|
5
|
+
|
|
6
|
+
class LogInForm extends SocketConsumer {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connectedCallback() {
|
|
12
|
+
this.socket = this.getSocket();
|
|
13
|
+
this.isReady = true;
|
|
14
|
+
this.render();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static get observedAttributes() {
|
|
18
|
+
return [
|
|
19
|
+
'hostname',
|
|
20
|
+
'port',
|
|
21
|
+
'network-symbol',
|
|
22
|
+
'chain-module-name',
|
|
23
|
+
'secure',
|
|
24
|
+
'auth-timeout',
|
|
25
|
+
'success-location-hash'
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
30
|
+
if (!this.isReady) return;
|
|
31
|
+
this.render();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
let hostname = this.getAttribute('hostname');
|
|
36
|
+
let port = this.getAttribute('port');
|
|
37
|
+
let networkSymbol = this.getAttribute('network-symbol');
|
|
38
|
+
let chainModuleName = this.getAttribute('chain-module-name');
|
|
39
|
+
let secure = this.getAttribute('secure');
|
|
40
|
+
let authTimeout = Number(this.getAttribute('auth-timeout') || DEFAULT_AUTH_TIMEOUT);
|
|
41
|
+
let successLocationHash = this.getAttribute('success-location-hash');
|
|
42
|
+
|
|
43
|
+
this.innerHTML = `
|
|
44
|
+
<capitalisk-log-in
|
|
45
|
+
hostname="${hostname}"
|
|
46
|
+
port="${port}"
|
|
47
|
+
network-symbol="${networkSymbol}"
|
|
48
|
+
chain-module-name="${chainModuleName}"
|
|
49
|
+
secure="${secure}"
|
|
50
|
+
disabled="true"
|
|
51
|
+
></capitalisk-log-in>
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
let capitaliskLogIn = this.querySelector('capitalisk-log-in');
|
|
55
|
+
capitaliskLogIn.addEventListener('accountReadyStateChange', (event) => {
|
|
56
|
+
if (event.detail.accountReady) {
|
|
57
|
+
capitaliskLogIn.removeAttribute('disabled');
|
|
58
|
+
} else {
|
|
59
|
+
capitaliskLogIn.setAttribute('disabled', '');
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
capitaliskLogIn.addEventListener('error', (event) => {
|
|
65
|
+
console.error(`CapitaliskLogIn error: ${event.detail.error.message}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
capitaliskLogIn.addEventListener('submitCredentials', async (event) => {
|
|
69
|
+
capitaliskLogIn.setAttribute('disabled', '');
|
|
70
|
+
capitaliskLogIn.removeAttribute('error');
|
|
71
|
+
capitaliskLogIn.setAttribute('submit-button-text', 'Loading...');
|
|
72
|
+
try {
|
|
73
|
+
let logInResponse = await this.socket.invoke('log-in', event.detail);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
capitaliskLogIn.setAttribute('error', `${error.message}.`);
|
|
76
|
+
capitaliskLogIn.removeAttribute('disabled');
|
|
77
|
+
capitaliskLogIn.setAttribute('submit-button-text', 'Log in');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (this.socket.authState !== 'authenticated') {
|
|
81
|
+
try {
|
|
82
|
+
await this.socket.listener('authenticate').once(authTimeout);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
capitaliskLogIn.setAttribute(
|
|
85
|
+
'error',
|
|
86
|
+
'Authentication timed out, please try again.'
|
|
87
|
+
);
|
|
88
|
+
capitaliskLogIn.removeAttribute('disabled');
|
|
89
|
+
capitaliskLogIn.setAttribute('submit-button-text', 'Log in');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
capitaliskLogIn.setAttribute('disabled', '');
|
|
95
|
+
capitaliskLogIn.setAttribute('submit-button-text', 'Success!');
|
|
96
|
+
|
|
97
|
+
if (successLocationHash) {
|
|
98
|
+
location.hash = successLocationHash;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
window.customElements.define('log-in-form', LogInForm);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saasufy-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Components for Saasufy.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"ag-collection": "^3.3.0",
|
|
21
21
|
"ag-model": "^1.4.1",
|
|
22
|
+
"capitalisk-auth-client": "^1.5.1",
|
|
22
23
|
"expr-eval": "^2.0.2",
|
|
23
24
|
"socketcluster-client": "^19.1.0"
|
|
24
25
|
},
|