@wral/studio.mods.auth 1.0.0 → 2.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 +3 -3
- package/bitbucket-pipelines.yml +3 -3
- package/dist/auth.cjs.js +119 -205
- package/dist/auth.es.js +387 -606
- package/dist/lib.cjs.js +1 -1
- package/dist/lib.es.js +4 -6
- package/example-app.mjs +34 -0
- package/index.html +19 -72
- package/package.json +3 -2
- package/src/{components/forgot-password-form.mjs → forgot-password-form.mjs} +69 -69
- package/src/helper.mjs +5 -16
- package/src/index.mjs +108 -12
- package/src/layout.mjs +38 -0
- package/src/{components/login-form.mjs → login-form.mjs} +125 -122
- package/src/token.mjs +34 -30
- package/vellum/README.md +1 -0
- package/vellum/app-manager.mjs +126 -0
- package/vellum/example-app.mjs +34 -0
- package/vellum/index.mjs +26 -0
- package/vellum/layout.mjs +85 -0
- package/vellum/mod-setup.mjs +22 -0
- package/vellum/render.mjs +21 -0
- package/vellum/themes/default/index.css +89 -0
- package/vite.config.mjs +1 -1
- package/src/auth.mjs +0 -208
- package/src/auth.test.mjs +0 -97
- package/src/components/auth-app.mjs +0 -26
- package/src/config.mjs +0 -27
- package/src/login-layout.mjs +0 -32
- package/src/login.mjs +0 -20
- package/src/routes/change-password.mjs +0 -158
- package/src/routes/dashboard.mjs +0 -17
- package/src/routes/index.mjs +0 -15
- package/src/state.mjs +0 -61
- package/src/state.test.mjs +0 -58
- package/src/styles.mjs +0 -9
- package/src/utils.mjs +0 -3
- package/vellum-fixture.mjs +0 -86
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This login form is not part of any application route, because it represents
|
|
3
|
-
* the public facing unauthenticated side of the application.
|
|
4
|
-
*
|
|
5
|
-
* Anything regarding login that is handled by an unauthenticated user should
|
|
6
|
-
* be handled through here.
|
|
7
|
-
*
|
|
8
|
-
* Other routes assume the user is authenticated.
|
|
9
|
-
*/
|
|
10
1
|
import { LitElement, html, css } from 'lit';
|
|
11
|
-
import login from '../login.mjs';
|
|
12
|
-
import { ForgotPasswordForm } from './forgot-password-form.mjs';
|
|
13
2
|
import { createClient } from '@wral/sdk-auth';
|
|
3
|
+
import './forgot-password-form.mjs';
|
|
14
4
|
|
|
15
5
|
|
|
16
|
-
class
|
|
6
|
+
class AuthLoginForm extends LitElement {
|
|
17
7
|
|
|
18
8
|
static get properties() {
|
|
19
9
|
return {
|
|
@@ -25,10 +15,9 @@ class LoginForm extends LitElement {
|
|
|
25
15
|
return css`
|
|
26
16
|
:host {
|
|
27
17
|
display: block;
|
|
28
|
-
font-family: Arial, sans-serif;
|
|
29
18
|
}
|
|
30
19
|
.login-form {
|
|
31
|
-
background-color: var(--color-gray-
|
|
20
|
+
background-color: var(--color-gray-50);
|
|
32
21
|
padding: var(--spacing-xl, 3rem);
|
|
33
22
|
border-radius: var(--radius-md, 5px);
|
|
34
23
|
box-shadow: var(--drop-shadow-md, none);
|
|
@@ -39,14 +28,14 @@ class LoginForm extends LitElement {
|
|
|
39
28
|
margin: 5px 0 15px 0;
|
|
40
29
|
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
41
30
|
box-sizing: border-box;
|
|
42
|
-
border: 1px solid var(--color-gray-300, #
|
|
43
|
-
border-radius: var(--radius-sm,
|
|
44
|
-
color: var(--color-gray-
|
|
45
|
-
background-color: var(--color-gray-
|
|
31
|
+
border: 1px solid var(--color-gray-300, #CCC);
|
|
32
|
+
border-radius: var(--radius-sm, 3px);
|
|
33
|
+
color: var(--color-gray-700, #333);
|
|
34
|
+
background-color: var(--color-gray-100, #FFF);
|
|
46
35
|
}
|
|
47
36
|
form input[type="submit"] {
|
|
48
37
|
background-color: var(--color-primary, inherit);
|
|
49
|
-
color: var(--color-gray-
|
|
38
|
+
color: var(--color-gray-50, #fff);
|
|
50
39
|
border: none;
|
|
51
40
|
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
52
41
|
border-radius: var(--radius-dynamic, 5px);
|
|
@@ -57,15 +46,15 @@ class LoginForm extends LitElement {
|
|
|
57
46
|
}
|
|
58
47
|
form label {
|
|
59
48
|
font-size: 14px;
|
|
60
|
-
color: var(--color-gray-
|
|
49
|
+
color: var(--color-gray-900, #333);
|
|
61
50
|
font-weight: bold;
|
|
62
51
|
margin-bottom: var(--spacing-sm, 0.5rem);
|
|
63
52
|
}
|
|
64
53
|
a.forgot {
|
|
65
54
|
color: var(--color-primary, inherit);
|
|
66
55
|
text-decoration: none;
|
|
67
|
-
font-size: 12px;
|
|
68
56
|
font-weight: bold;
|
|
57
|
+
font-size: 12px;
|
|
69
58
|
}
|
|
70
59
|
.error {
|
|
71
60
|
color: var(--color-error, red);
|
|
@@ -80,7 +69,7 @@ class LoginForm extends LitElement {
|
|
|
80
69
|
.header .intro {
|
|
81
70
|
width: 200px;
|
|
82
71
|
margin-bottom: 0;
|
|
83
|
-
color: var(--color-gray-
|
|
72
|
+
color: var(--color-gray-300, #333);
|
|
84
73
|
}
|
|
85
74
|
.header .brand {
|
|
86
75
|
font-size: 32px;
|
|
@@ -105,42 +94,55 @@ class LoginForm extends LitElement {
|
|
|
105
94
|
|
|
106
95
|
constructor() {
|
|
107
96
|
super();
|
|
108
|
-
this.formState =
|
|
109
|
-
this.username = '';
|
|
110
|
-
this.password = '';
|
|
111
|
-
this.token = undefined;
|
|
97
|
+
this.formState = this.constructor.states.DEFAULT;
|
|
112
98
|
this.errors = [];
|
|
99
|
+
this.isBusy = false; // indicates some operation is in progress
|
|
113
100
|
}
|
|
114
101
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
102
|
+
login({ username, password }) {
|
|
103
|
+
const authClient = createClient({
|
|
104
|
+
baseUrl: this.api,
|
|
105
|
+
});
|
|
106
|
+
return authClient.mintToken({ username, password });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
emitLoginSuccess(token) {
|
|
110
|
+
this.dispatchEvent(new CustomEvent('login-success', {
|
|
111
|
+
detail: { token },
|
|
112
|
+
bubbles: true,
|
|
113
|
+
composed: true,
|
|
114
|
+
cancelable: true,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
handleSubmitLogin(event) {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
const formElem = event.explicitOriginalTarget.parentElement;
|
|
119
121
|
const formData = new FormData(formElem).entries().reduce((acc, [key, value]) => {
|
|
120
122
|
acc[key] = value;
|
|
121
123
|
return acc;
|
|
122
124
|
}, {});
|
|
123
125
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
this.isBusy = true;
|
|
127
|
+
this.errors = [];
|
|
128
|
+
this.requestUpdate();
|
|
129
|
+
this.login(formData).then(({ token, challenge }) => {
|
|
130
|
+
if (challenge?.name === 'NEW_PASSWORD_REQUIRED') {
|
|
127
131
|
// store these to use in later forms
|
|
128
132
|
this.token = token;
|
|
129
133
|
this.username = formData['username'];
|
|
130
134
|
this.password = formData['password'];
|
|
131
135
|
|
|
132
|
-
this.setState(
|
|
133
|
-
// don't dispatch a success event
|
|
136
|
+
this.setState(this.constructor.states.CHALLENGE_CHANGE_PW);
|
|
137
|
+
// don't dispatch a success event yet
|
|
134
138
|
// The user should change their password first
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
this.
|
|
140
|
-
detail: { token },
|
|
141
|
-
}));
|
|
141
|
+
|
|
142
|
+
// Emit a success event
|
|
143
|
+
this.emitLoginSuccess(token);
|
|
142
144
|
}).catch((error) => {
|
|
143
|
-
console.
|
|
145
|
+
console.error(error);
|
|
144
146
|
this.errors = [error.message];
|
|
145
147
|
this.requestUpdate();
|
|
146
148
|
});
|
|
@@ -148,17 +150,17 @@ class LoginForm extends LitElement {
|
|
|
148
150
|
|
|
149
151
|
onSubmitChangePasswordChallenge(e) {
|
|
150
152
|
e.preventDefault();
|
|
151
|
-
const formElem = e.explicitOriginalTarget;
|
|
153
|
+
const formElem = e.explicitOriginalTarget.parentElement;
|
|
152
154
|
const formData = new FormData(formElem).entries().reduce((acc, [key, value]) => {
|
|
153
155
|
acc[key] = value;
|
|
154
156
|
return acc;
|
|
155
157
|
}, {});
|
|
156
|
-
|
|
157
|
-
// validate password
|
|
158
|
+
|
|
159
|
+
// validate password, client-side
|
|
158
160
|
const password = formData['password'];
|
|
159
161
|
const confirmPassword = formData['confirm-password'];
|
|
160
162
|
if (password !== confirmPassword) {
|
|
161
|
-
this.errors = ['Passwords do not match
|
|
163
|
+
this.errors = ['Passwords do not match'];
|
|
162
164
|
this.requestUpdate();
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
@@ -171,81 +173,72 @@ class LoginForm extends LitElement {
|
|
|
171
173
|
currentPassword: this.password,
|
|
172
174
|
newPassword: password,
|
|
173
175
|
}).then(() => {
|
|
174
|
-
|
|
175
|
-
this.dispatchEvent(new CustomEvent('login-success', {
|
|
176
|
-
detail: { token: this.token },
|
|
177
|
-
}));
|
|
176
|
+
this.emitLoginSuccess(this.token);
|
|
178
177
|
}).catch((error) => {
|
|
179
|
-
console.
|
|
178
|
+
console.error(error);
|
|
180
179
|
this.errors = [error.message];
|
|
181
180
|
this.requestUpdate();
|
|
182
181
|
});
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
setState(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
184
|
+
setState(state) {
|
|
185
|
+
this.formState = state;
|
|
186
|
+
this.isBusy = false;
|
|
187
|
+
this.errors = [];
|
|
188
|
+
this.requestUpdate();
|
|
191
189
|
}
|
|
192
190
|
|
|
193
191
|
render() {
|
|
194
192
|
return html`<div class="login-form">
|
|
195
|
-
${this.renderHeader()}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
${ this.renderHeader() }
|
|
194
|
+
${ this.formState === this.constructor.states.DEFAULT ?
|
|
195
|
+
this.renderLoginForm() : '' }
|
|
196
|
+
${ this.formState === this.constructor.states.CHALLENGE_CHANGE_PW ?
|
|
197
|
+
this.renderChangePasswordForm() : '' }
|
|
198
|
+
${ this.formState === this.constructor.states.FORGOT ?
|
|
199
|
+
this.renderForgotPasswordForm() : '' }
|
|
202
200
|
</div>`;
|
|
203
201
|
}
|
|
204
202
|
|
|
205
|
-
/**
|
|
206
|
-
* TODO: accept the header as a slot
|
|
207
|
-
*/
|
|
208
203
|
renderHeader() {
|
|
209
204
|
return html`<div class="header">
|
|
210
|
-
<div class="intro">
|
|
211
|
-
|
|
212
|
-
</div>
|
|
213
|
-
<div class="brand">
|
|
214
|
-
<em>WRAL</em>.studio
|
|
215
|
-
</div>
|
|
205
|
+
<div class="intro">Welcome to</div>
|
|
206
|
+
<div class="brand"><em>WRAL</em>.studio</div>
|
|
216
207
|
</div>`;
|
|
217
208
|
}
|
|
218
209
|
|
|
219
210
|
renderLoginForm() {
|
|
220
211
|
return html`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<label for="password">Password</label>
|
|
231
|
-
<input
|
|
232
|
-
type="password"
|
|
233
|
-
name="password"
|
|
234
|
-
placeholder="Password"
|
|
235
|
-
required
|
|
236
|
-
/>
|
|
237
|
-
<div>
|
|
238
|
-
<a href="#" class="forgot"
|
|
239
|
-
@click=${() => {this.setState(LoginForm.states.FORGOT);}}
|
|
240
|
-
>Forgot password</a>
|
|
241
|
-
</div>
|
|
212
|
+
<form>
|
|
213
|
+
<label for="username">User</label>
|
|
214
|
+
<input
|
|
215
|
+
type="text"
|
|
216
|
+
name="username"
|
|
217
|
+
placeholder="Email or Username"
|
|
218
|
+
required
|
|
219
|
+
/>
|
|
242
220
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
221
|
+
<label for="password">Password</label>
|
|
222
|
+
<input
|
|
223
|
+
type="password"
|
|
224
|
+
name="password"
|
|
225
|
+
placeholder="Password"
|
|
226
|
+
required
|
|
227
|
+
/>
|
|
228
|
+
<div>
|
|
229
|
+
<a href="#" class="forgot"
|
|
230
|
+
@click=${() => {
|
|
231
|
+
this.setState(this.constructor.states.FORGOT);
|
|
232
|
+
}}>Forgot password</a>
|
|
233
|
+
</div>
|
|
234
|
+
${this.errors
|
|
235
|
+
? this.errors.map(error => html`<p class="error">${error}</p>`) : ''}
|
|
247
236
|
|
|
248
|
-
|
|
237
|
+
<input type="submit" value="Log in" @click=${this.handleSubmitLogin}
|
|
238
|
+
?disabled=${this.isBusy}
|
|
239
|
+
/>
|
|
240
|
+
</form>
|
|
241
|
+
`;
|
|
249
242
|
}
|
|
250
243
|
|
|
251
244
|
renderForgotPasswordForm() {
|
|
@@ -258,31 +251,41 @@ class LoginForm extends LitElement {
|
|
|
258
251
|
|
|
259
252
|
renderChangePasswordForm() {
|
|
260
253
|
return html`
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
? this.errors.map(error => html`<p class="error">${error}</p>`)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
</form>
|
|
254
|
+
<p>You have logged in successfully, but you must change your password.</p>
|
|
255
|
+
<form @submit="${this.onSubmitChangePasswordChallenge}">
|
|
256
|
+
<input type="hidden" name="username" value="${this.username}" />
|
|
257
|
+
<label for="password">New Password</label>
|
|
258
|
+
<input
|
|
259
|
+
type="password"
|
|
260
|
+
name="password"
|
|
261
|
+
placeholder="Password"
|
|
262
|
+
required
|
|
263
|
+
/>
|
|
264
|
+
<label for="confirm-password">Confirm Password</label>
|
|
265
|
+
<input
|
|
266
|
+
type="password"
|
|
267
|
+
name="confirm-password"
|
|
268
|
+
placeholder="Confirm Password"
|
|
269
|
+
required
|
|
270
|
+
/>
|
|
271
|
+
${this.errors
|
|
272
|
+
? this.errors.map(error => html`<p class="error">${error}</p>`) : ''}
|
|
273
|
+
<input type="submit" value="Change Password" />
|
|
274
|
+
</form>
|
|
283
275
|
`;
|
|
284
276
|
}
|
|
277
|
+
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!customElements.get('auth-login-form')) {
|
|
281
|
+
customElements.define('auth-login-form', AuthLoginForm);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function render(state, onLoginSuccess) {
|
|
285
|
+
return html`<auth-login-form slot="main"
|
|
286
|
+
api=${state?.apiBaseUrl}
|
|
287
|
+
@login-success=${onLoginSuccess}>
|
|
288
|
+
</auth-login-form>`;
|
|
285
289
|
}
|
|
286
290
|
|
|
287
|
-
|
|
288
|
-
customElements.define('auth-forgot-password-form', ForgotPasswordForm);
|
|
291
|
+
export default render;
|
package/src/token.mjs
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
|
+
import { createClient } from '@wral/sdk-auth';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* @example
|
|
9
|
-
* // Decode a sample JWT
|
|
10
|
-
* const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
|
|
11
|
-
* + '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2M'
|
|
12
|
-
* + 'jM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
13
|
-
* const payload = decodeToken(token);
|
|
14
|
-
* console.log('Decoded Payload:', payload);
|
|
15
|
-
*/
|
|
16
|
-
export function decodeToken(token) {
|
|
17
|
-
return JSON.parse(window.atob(token.split('.')[1]
|
|
18
|
-
.replace(/-/g, '+').replace(/_/g, '/')));
|
|
3
|
+
export function getToken({ tokenKey }) {
|
|
4
|
+
return window.localStorage.getItem(tokenKey);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function setToken({ tokenKey }, token) {
|
|
8
|
+
window.localStorage.setItem(tokenKey, token);
|
|
19
9
|
}
|
|
20
10
|
|
|
11
|
+
export function destroyToken({ tokenKey }) {
|
|
12
|
+
window.localStorage.removeItem(tokenKey);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getFreshToken(state) {
|
|
16
|
+
const token = getToken(state);
|
|
17
|
+
if (!token || token.length < 1) {
|
|
18
|
+
throw new Error('No token found');
|
|
19
|
+
}
|
|
20
|
+
if (shouldRefreshToken(token)) {
|
|
21
|
+
const authClient = createClient({
|
|
22
|
+
baseUrl: state.apiBaseUrl,
|
|
23
|
+
});
|
|
24
|
+
return await authClient.refreshToken({ token }).then(({ token }) => token);
|
|
25
|
+
}
|
|
26
|
+
return token;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {number}
|
|
26
|
-
*
|
|
27
|
-
* @param {number} [now=Date.now()] - The current timestamp, primarily used
|
|
28
|
-
* for testing.
|
|
29
|
-
* @returns {boolean} True if the token needs to be refreshed, false otherwise.
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* // Check if token needs to be refreshed
|
|
33
|
-
* const tokenNeedsRefresh = shouldRefreshToken('someToken', 300);
|
|
34
|
-
* console.log('Token needs refresh:', tokenNeedsRefresh);
|
|
30
|
+
* Determine if a token needs to be refreshed, based on its expiration date
|
|
31
|
+
* @param {string} token
|
|
32
|
+
* @param {number} offset - milliseconds early to consider refreshing
|
|
33
|
+
* @param {number} now - current time
|
|
34
|
+
* @returns {boolean}
|
|
35
35
|
*/
|
|
36
36
|
export function shouldRefreshToken(token, offset = 300, now = Date.now()) {
|
|
37
37
|
const decoded = decodeToken(token);
|
|
38
38
|
return decoded.exp * 1000 - offset < now;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function decodeToken(token) {
|
|
42
|
+
return JSON.parse(window.atob(token.split('.')[1]
|
|
43
|
+
.replace(/-/g, '+').replace(/_/g, '/')));
|
|
44
|
+
}
|
package/vellum/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is an example Vellum app that consumes this auth mod
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* The App Manager has the responsibility of handling requests related to Apps
|
|
4
|
+
* It handles app registration (adding an app to the main application),
|
|
5
|
+
* and activation/deactivation of apps.
|
|
6
|
+
* @param toolkit
|
|
7
|
+
* @return void - Bootstraps app management within the toolkit as a side-effect
|
|
8
|
+
*/
|
|
9
|
+
export function init(toolkit) {
|
|
10
|
+
|
|
11
|
+
const modName = 'mod-app-manager';
|
|
12
|
+
const debug = true; // TODO: use data-debug attribute
|
|
13
|
+
|
|
14
|
+
const state = {
|
|
15
|
+
apps: new Map(), // { id: { id, name, content, fragment } }
|
|
16
|
+
activeAppId: null,
|
|
17
|
+
toolkit,
|
|
18
|
+
debug: (...args) => debug && console.log('[app-manager]',...args),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
Object.entries({
|
|
22
|
+
'app:register': registerApp,
|
|
23
|
+
'app:activate': activateApp,
|
|
24
|
+
'app:close': closeApp,
|
|
25
|
+
}).forEach(([actionType, fn]) => toolkit.dispatchAction({
|
|
26
|
+
type: 'action:register',
|
|
27
|
+
detail: {
|
|
28
|
+
actionType,
|
|
29
|
+
modName,
|
|
30
|
+
handler: (...args) => fn(state, ...args),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function registerApp(state, detail) {
|
|
37
|
+
state.debug('registerApp', detail);
|
|
38
|
+
const { id, name, content } = detail;
|
|
39
|
+
if (!id || !name || !content) {
|
|
40
|
+
console.error('Invalid app registration', detail);
|
|
41
|
+
return; // TODO: consider throwing
|
|
42
|
+
}
|
|
43
|
+
state.apps.set(id, { id, name, content, fragment: null });
|
|
44
|
+
|
|
45
|
+
state.toolkit.dispatchAction({
|
|
46
|
+
type: 'layout:content:append',
|
|
47
|
+
detail: {
|
|
48
|
+
content: appToolbarButton(state, detail),
|
|
49
|
+
id: `app-btn-${id}`, // must match id in appToolbarButton
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function activateApp(state, detail) {
|
|
56
|
+
const { id } = detail;
|
|
57
|
+
const { apps, activeAppId, toolkit } = state;
|
|
58
|
+
|
|
59
|
+
// no-op if already active
|
|
60
|
+
if (activeAppId === id) {
|
|
61
|
+
state.debug('activateApp - already active', id);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!apps.has(id)) {
|
|
66
|
+
state.debug('activateApp - app not found', id);
|
|
67
|
+
console.error('App not found: ', id);
|
|
68
|
+
return; // TODO: consider throwing
|
|
69
|
+
}
|
|
70
|
+
const app = apps.get(id);
|
|
71
|
+
|
|
72
|
+
// remove existing app, save fragment for later
|
|
73
|
+
if (activeAppId) {
|
|
74
|
+
state.debug('activateApp - closing', activeAppId);
|
|
75
|
+
toolkit.dispatchAction({
|
|
76
|
+
type: 'layout:content:remove',
|
|
77
|
+
detail: {
|
|
78
|
+
id: `app-content-${activeAppId}`,
|
|
79
|
+
callback: (fragment) => {
|
|
80
|
+
const currentApp = apps.get(activeAppId);
|
|
81
|
+
state.debug('activateApp - closing callback', { activeAppId, fragment });
|
|
82
|
+
if (currentApp) currentApp.fragment = fragment;
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// present the app, using prior fragment if available
|
|
89
|
+
const content = app.fragment || app.content();
|
|
90
|
+
state.debug('activateApp - presenting', { id, content });
|
|
91
|
+
toolkit.dispatchAction({
|
|
92
|
+
type: 'layout:content:append',
|
|
93
|
+
detail: { content, id: `app-content-${id}` },
|
|
94
|
+
});
|
|
95
|
+
state.activeAppId = id;
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function closeApp(state) {
|
|
100
|
+
const { activeAppId, apps, toolkit } = state;
|
|
101
|
+
|
|
102
|
+
// no-op if no active app
|
|
103
|
+
if (!activeAppId) return;
|
|
104
|
+
toolkit.dispatchAction({
|
|
105
|
+
type: 'layout:content:remove',
|
|
106
|
+
detail: { id: `app-content-${activeAppId}` },
|
|
107
|
+
});
|
|
108
|
+
const app = apps.get(activeAppId);
|
|
109
|
+
if (app) app.fragment = null;
|
|
110
|
+
state.activeAppId = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function appToolbarButton(state, detail) {
|
|
114
|
+
console.log('apToolbarButton', { state, detail });
|
|
115
|
+
return html`
|
|
116
|
+
<button slot="menu"
|
|
117
|
+
id="app-btn-${detail.id}"
|
|
118
|
+
@click=${() => state.toolkit.dispatchAction({
|
|
119
|
+
type: 'app:activate',
|
|
120
|
+
detail,
|
|
121
|
+
})}
|
|
122
|
+
>
|
|
123
|
+
${detail.name}
|
|
124
|
+
</button>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
|
|
3
|
+
export async function init(toolkit, mod) {
|
|
4
|
+
|
|
5
|
+
let token = null;
|
|
6
|
+
|
|
7
|
+
const content = html`
|
|
8
|
+
<div slot="main" id="example-app">
|
|
9
|
+
<h1>Example</h1>
|
|
10
|
+
<p>This is an example app.</p>
|
|
11
|
+
<pre><code>${JSON.stringify(mod, null, 2)}</code></pre>
|
|
12
|
+
</div>
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
toolkit.dispatchAction({
|
|
16
|
+
type: 'app:register',
|
|
17
|
+
detail: {
|
|
18
|
+
id: 'example',
|
|
19
|
+
name: 'Example App',
|
|
20
|
+
content: () => content,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
toolkit.dispatchAction({
|
|
25
|
+
type: 'auth:requestToken',
|
|
26
|
+
detail: {
|
|
27
|
+
callback: (t) => {
|
|
28
|
+
token = t;
|
|
29
|
+
console.log("got a token!", token);
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
}
|
package/vellum/index.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import
|
|
2
|
+
VellumHarness
|
|
3
|
+
from '@thefarce/vellum';
|
|
4
|
+
import { render } from './render.mjs';
|
|
5
|
+
import { init as _init } from './mod-setup.mjs';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Define the web-components that will make up the Studio App
|
|
9
|
+
*/
|
|
10
|
+
if (!customElements.get('wral-studio')) {
|
|
11
|
+
customElements.define('wral-studio', VellumHarness);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Add our custom renderer to the wral-studio App element
|
|
16
|
+
*/
|
|
17
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
18
|
+
const app = document.querySelector('wral-studio');
|
|
19
|
+
if (app) {
|
|
20
|
+
app.setRenderer(render);
|
|
21
|
+
} else {
|
|
22
|
+
console.error('Missing \'wral-studio\' element');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const init = _init;
|