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 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 combine values from a specific field to pass to other child components via their attributes.
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 models using a template.
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 display relevant data loaded from Saasufy (e.g. based on a user query).
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
- ### confirm-modal
49
+ ### log-in-form
41
50
 
42
- A modal component to prompt the user for confirmation before performing sensitive operations.
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
- ### model-text
57
+ ### confirm-modal
45
58
 
46
- Used to displaying a field of a model instance in real time.
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 { renderTemplate } from './utils.js';
1
+ import { toSafeHTML, renderTemplate, debouncer } from './utils.js';
2
+ import { SocketConsumer } from './socket.js';
3
3
 
4
- export class AppRouter extends HTMLElement {
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
- this.renderCurrentPage();
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) return;
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.7.2",
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
  },