@iebh/tera-fy 1.0.17 → 1.0.19
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/api.md +320 -200
- package/dist/plugin.vue2.es2019.js +1 -2
- package/dist/terafy.es2019.js +2 -3
- package/dist/terafy.js +2 -3
- package/lib/terafy.client.js +76 -9
- package/lib/terafy.server.js +266 -18
- package/package.json +16 -16
- package/plugins/vue2.js +68 -0
- package/utils/pDefer.js +15 -0
- package/widgets/tera-library-select.vue +8 -0
- package/dist/plugin.vue2.es2019.js.map +0 -7
- package/dist/terafy.es2019.js.map +0 -7
- package/dist/terafy.js.map +0 -7
package/lib/terafy.server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {cloneDeep, has as pathExists, set as pathSet} from 'lodash-es';
|
|
1
|
+
import {cloneDeep, has as pathExists, get as pathGet, set as pathSet} from 'lodash-es';
|
|
2
2
|
import {diffApply, jsonPatchPathConverter as jsPatchConverter} from 'just-diff-apply';
|
|
3
|
-
import {nanoid} from 'nanoid';
|
|
4
3
|
import mixin from '#utils/mixin';
|
|
4
|
+
import {nanoid} from 'nanoid';
|
|
5
|
+
import promiseDefer from '#utils/pDefer';
|
|
5
6
|
import Reflib from '@iebh/reflib';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -21,7 +22,9 @@ export default class TeraFyServer {
|
|
|
21
22
|
* @property {Number} subscribeTimeout Acceptable timeout period for subscribers to acklowledge a project change event, failing to respond will result in the subscriber being removed from the available subscriber list
|
|
22
23
|
* @property {String} restrictOrigin URL to restrict communications to
|
|
23
24
|
* @property {String} projectId The project to use as the default reference when calling various APIs
|
|
24
|
-
* @property {Number} The current server mode matching `SERVERMODE_*`
|
|
25
|
+
* @property {Number} serverMode The current server mode matching `SERVERMODE_*`
|
|
26
|
+
* @property {String} siteUrl The main site absolute URL
|
|
27
|
+
* @property {String} sitePathLogin Either an absolute URL or the relative path (taken from `siteUrl`) when trying to log in the user
|
|
25
28
|
*/
|
|
26
29
|
settings = {
|
|
27
30
|
devMode: false,
|
|
@@ -29,12 +32,15 @@ export default class TeraFyServer {
|
|
|
29
32
|
subscribeTimeout: 2000,
|
|
30
33
|
projectId: null,
|
|
31
34
|
serverMode: 0,
|
|
35
|
+
siteUrl: window.location.href,
|
|
36
|
+
sitePathLogin: '/login',
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
static SERVERMODE_NONE = 0;
|
|
35
40
|
static SERVERMODE_EMBEDDED = 1;
|
|
36
|
-
static
|
|
37
|
-
static
|
|
41
|
+
static SERVERMODE_FRAME = 2;
|
|
42
|
+
static SERVERMODE_POPUP = 3;
|
|
43
|
+
static SERVERMODE_TERA = 4; // Terafy is running as the main TERA site
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
// Contexts - createContext(), getClientContext(), messageEvent, senderRpc() {{{
|
|
@@ -94,7 +100,7 @@ export default class TeraFyServer {
|
|
|
94
100
|
}
|
|
95
101
|
},
|
|
96
102
|
});
|
|
97
|
-
case TeraFyServer.
|
|
103
|
+
case TeraFyServer.SERVERMODE_FRAME:
|
|
98
104
|
// Server is the top-level window so we need to send messages to an embedded iFrame
|
|
99
105
|
var iFrame = document.querySelector('iframe#external');
|
|
100
106
|
if (!iFrame) throw new Error('Cannot locate TERA-FY top-level->iFrame#external');
|
|
@@ -114,6 +120,7 @@ export default class TeraFyServer {
|
|
|
114
120
|
}
|
|
115
121
|
},
|
|
116
122
|
});
|
|
123
|
+
case TeraFyServer.SERVERMODE_POPUP:
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
@@ -202,10 +209,10 @@ export default class TeraFyServer {
|
|
|
202
209
|
TERA: 1,
|
|
203
210
|
...cloneDeep(message), // Need to clone to resolve promise nasties
|
|
204
211
|
};
|
|
205
|
-
this.debug('INFO', '
|
|
212
|
+
this.debug('INFO', 'Dispatch response', message, '<=>', payload);
|
|
206
213
|
(sendVia || globalThis.parent).postMessage(payload, this.settings.restrictOrigin);
|
|
207
214
|
} catch (e) {
|
|
208
|
-
this.debug('ERROR', 'Attempted to dispatch
|
|
215
|
+
this.debug('ERROR', 'Attempted to dispatch response server->client', payload);
|
|
209
216
|
this.debug('ERROR', 'Message compose server->client:', e);
|
|
210
217
|
}
|
|
211
218
|
}
|
|
@@ -221,8 +228,11 @@ export default class TeraFyServer {
|
|
|
221
228
|
case 'embedded':
|
|
222
229
|
this.settings.serverMode = TeraFyServer.SERVERMODE_EMBEDDED;
|
|
223
230
|
break;
|
|
224
|
-
case '
|
|
225
|
-
this.settings.serverMode = TeraFyServer.
|
|
231
|
+
case 'frame':
|
|
232
|
+
this.settings.serverMode = TeraFyServer.SERVERMODE_FRAME;
|
|
233
|
+
break;
|
|
234
|
+
case 'popup':
|
|
235
|
+
this.settings.serverMode = TeraFyServer.SERVERMODE_POPUP;
|
|
226
236
|
break;
|
|
227
237
|
default:
|
|
228
238
|
throw new Error(`Unsupported server mode "${mode}"`);
|
|
@@ -330,9 +340,17 @@ export default class TeraFyServer {
|
|
|
330
340
|
/**
|
|
331
341
|
* Fetch the current session user
|
|
332
342
|
*
|
|
343
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
344
|
+
* @param {Boolean} [options.forceRetry=false] Forcabily try to refresh the user state
|
|
345
|
+
*
|
|
333
346
|
* @returns {Promise<User>} The current logged in user or null if none
|
|
334
347
|
*/
|
|
335
|
-
getUser() {
|
|
348
|
+
getUser(options) {
|
|
349
|
+
let settings = {
|
|
350
|
+
forceRetry: false,
|
|
351
|
+
...options,
|
|
352
|
+
};
|
|
353
|
+
|
|
336
354
|
let $auth = app.service('$auth');
|
|
337
355
|
let $subscriptions = app.service('$subscriptions');
|
|
338
356
|
|
|
@@ -340,7 +358,11 @@ export default class TeraFyServer {
|
|
|
340
358
|
$auth.promise(),
|
|
341
359
|
$subscriptions.promise(),
|
|
342
360
|
])
|
|
343
|
-
.then(()=>
|
|
361
|
+
.then(()=> {
|
|
362
|
+
if (!$auth.isLoggedIn && settings.forceRetry)
|
|
363
|
+
return $auth.restoreLogin();
|
|
364
|
+
})
|
|
365
|
+
.then(()=> $auth.user?.id ? {
|
|
344
366
|
id: $auth.user.id,
|
|
345
367
|
email: $auth.user.email,
|
|
346
368
|
name: [
|
|
@@ -349,6 +371,10 @@ export default class TeraFyServer {
|
|
|
349
371
|
].filter(Boolean).join(' '),
|
|
350
372
|
isSubscribed: $subscriptions.isSubscribed,
|
|
351
373
|
} : null)
|
|
374
|
+
.catch(e => {
|
|
375
|
+
console.warn('Catch', e);
|
|
376
|
+
debugger;
|
|
377
|
+
})
|
|
352
378
|
}
|
|
353
379
|
|
|
354
380
|
|
|
@@ -361,10 +387,97 @@ export default class TeraFyServer {
|
|
|
361
387
|
*/
|
|
362
388
|
requireUser() {
|
|
363
389
|
return this.getUser()
|
|
364
|
-
.then(user =>
|
|
390
|
+
.then(user => {
|
|
391
|
+
this.debug('Current user is', user ? user : 'Not valid');
|
|
392
|
+
if (user) throw 'EXIT'; // Valid user? Escape promise chain
|
|
393
|
+
})
|
|
394
|
+
.then(async ()=> { // No user present - try to validate with other methods
|
|
395
|
+
switch (this.settings.serverMode) {
|
|
396
|
+
case TeraFyServer.SERVERMODE_EMBEDDED:
|
|
397
|
+
/* - Doesn't work because Kinde sets the CSP header `frame-ancestors 'self'` which prevents usage within an iFrame
|
|
398
|
+
const $auth = app.service('$auth');
|
|
399
|
+
return this.requestFocus(()=> $auth.login()
|
|
400
|
+
.then(()=> {
|
|
401
|
+
console.log('New user state', $auth.isLoggedIn);
|
|
402
|
+
})
|
|
403
|
+
);
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
var focusContent = document.createElement('div');
|
|
407
|
+
focusContent.innerHTML = '<div>Authenticate with <a href="https://tera-tools.com" target="_blank">TERA-tools.com</a></div>'
|
|
408
|
+
+ '<div class="mt-2"><a class="btn btn-light">Open Popup...</a></div>';
|
|
409
|
+
|
|
410
|
+
// Attach click listner to internal button to re-popup the auth window (in case popups are blocked)
|
|
411
|
+
focusContent.querySelector('a.btn').addEventListener('click', ()=>
|
|
412
|
+
this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl))
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Create a deferred promise which will (eventually) resolve when the downstream window signals its ready
|
|
416
|
+
let waitOnWindowAuth = promiseDefer();
|
|
417
|
+
|
|
418
|
+
// Create a listener for the message from the downstream window to resolve the promise
|
|
419
|
+
let listenMessages = ({data}) => {
|
|
420
|
+
if (data.TERA && data.action == 'noop' && data.isLoggedIn) { // Signal sent from landing page - we're logged in, yey!
|
|
421
|
+
this.debug('Received ok message from popup window');
|
|
422
|
+
waitOnWindowAuth.resolve();
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
window.addEventListener('message', listenMessages);
|
|
426
|
+
|
|
427
|
+
// Go fullscreen, try to open the auth window + prompt the user to retry (if popups are blocked) and wait for resolution
|
|
428
|
+
await this.requestFocus(()=> {
|
|
429
|
+
// Try opening the popup automatically - this will likely fail if the user has popup blocking enabled
|
|
430
|
+
this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl));
|
|
431
|
+
|
|
432
|
+
// Display a message to the user, offering the ability to re-open the popup if it was originally denied
|
|
433
|
+
this.uiSplat(focusContent, {logo: true});
|
|
434
|
+
|
|
435
|
+
this.debug('Begin auth-check deferred wait...');
|
|
436
|
+
return waitOnWindowAuth.promise;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
this.debug('Cleaning up popup auth');
|
|
440
|
+
|
|
441
|
+
// Remove message subscription
|
|
442
|
+
window.removeEventListener('message', listenMessages);
|
|
443
|
+
|
|
444
|
+
// Disable overlay content
|
|
445
|
+
this.uiSplat(false);
|
|
446
|
+
|
|
447
|
+
// Tell $auth to forcibly refresh its user data
|
|
448
|
+
await app.service('$auth').restoreLogin();
|
|
449
|
+
|
|
450
|
+
// Sanity Check against the login that should have been restored
|
|
451
|
+
if (!app.service('$auth').isLoggedIn) {
|
|
452
|
+
console.warn('Expecting tera-server to have restored $auth.user - but its still not logged in!');
|
|
453
|
+
debugger;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ... then refresh the project list as we're likely going to need it
|
|
457
|
+
await app.service('$projects').refresh();
|
|
458
|
+
|
|
459
|
+
// Go back to start of auth checking loop and repull the user data
|
|
460
|
+
throw 'REDO';
|
|
461
|
+
|
|
462
|
+
break;
|
|
463
|
+
default:
|
|
464
|
+
// Pass - Implied - Cannot authenticate via other method so just fall through to scalding the user
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
.then(()=> this.uiAlert('You must be logged in to <a href="https://tera-tools.com" target="_blank">TERA-tools.com</a> to use this tool', {
|
|
365
468
|
title: 'TERA-tools account needed',
|
|
366
469
|
isHtml: true,
|
|
470
|
+
buttons: false,
|
|
367
471
|
}))
|
|
472
|
+
.then(()=> { throw 'REDO' }) // Go into loop to keep requesting user data
|
|
473
|
+
.catch(e => {
|
|
474
|
+
if (e === 'EXIT') {
|
|
475
|
+
return; // Exit with a valid user
|
|
476
|
+
} else if (e == 'REDO') {
|
|
477
|
+
return this.requireUser();
|
|
478
|
+
}
|
|
479
|
+
throw e;
|
|
480
|
+
})
|
|
368
481
|
}
|
|
369
482
|
|
|
370
483
|
// }}}
|
|
@@ -576,16 +689,61 @@ export default class TeraFyServer {
|
|
|
576
689
|
if (!app.service('$projects').active) throw new Error('No active project');
|
|
577
690
|
if (typeof path != 'string' && !Array.isArray(path)) throw new Error('setProjectStateDefaults(path, value) - path must be a dotted string or array of path segments');
|
|
578
691
|
|
|
579
|
-
|
|
692
|
+
this._pathSet(app.service('$projects').active, path, value);
|
|
580
693
|
|
|
581
694
|
return (
|
|
582
|
-
|
|
583
|
-
:
|
|
695
|
+
settings.save && settings.sync ? this.saveProjectState()
|
|
696
|
+
: settings.save ? void this.saveProjectState()
|
|
584
697
|
: (()=> { throw new Error('setProjectState({sync: true, save: false}) makes no sense') })()
|
|
585
698
|
);
|
|
586
699
|
}
|
|
587
700
|
|
|
588
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Internal recursive path setter used by setProjectState() / setProjectStateDefaults()
|
|
704
|
+
* The implementation defaults to _.set() unless overriden by a plugin
|
|
705
|
+
*
|
|
706
|
+
* @private
|
|
707
|
+
* @param {Object} subject The base subject to operate on
|
|
708
|
+
* @param {String|Array} path The path to set in dotted or array notation
|
|
709
|
+
* @param {*} value The value to set
|
|
710
|
+
*
|
|
711
|
+
* @returns {*} The set value
|
|
712
|
+
*/
|
|
713
|
+
_pathSet(subject, path, value) {
|
|
714
|
+
return pathSet(subject, path, value);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Internal recursive path checker used by setProjectStateDefaults()
|
|
720
|
+
* The implementation defaults to _.has() unless overriden by a plugin
|
|
721
|
+
*
|
|
722
|
+
* @private
|
|
723
|
+
* @param {Object} subject The base subject to examine
|
|
724
|
+
* @param {String|Array} path The path to fetch in dotted or array notation
|
|
725
|
+
* @returns {Boolean} True if the given path already exists within the subject
|
|
726
|
+
*/
|
|
727
|
+
_pathHas(subject, path) {
|
|
728
|
+
return pathExists(subject, path);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Internal recursive path fetcher
|
|
734
|
+
* The implementation defaults to _.get() unless overriden by a plugin
|
|
735
|
+
*
|
|
736
|
+
* @private
|
|
737
|
+
* @param {Object} subject The base subject to examine
|
|
738
|
+
* @param {String|Array} path The path to fetch in dotted or array notation
|
|
739
|
+
* @param {*} [fallback] Optional fallback to return if the end point does not exist
|
|
740
|
+
* @returns {*} True if the given path already exists within the subject
|
|
741
|
+
*/
|
|
742
|
+
_pathGet(subject, path, fallback) {
|
|
743
|
+
return pathGet(subject, path, fallback);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
|
|
589
747
|
/**
|
|
590
748
|
* Set a nested value within the project state - just like `setProjectState()` - but only if no value for that path exists
|
|
591
749
|
*
|
|
@@ -599,7 +757,7 @@ export default class TeraFyServer {
|
|
|
599
757
|
setProjectStateDefaults(path, value, options) {
|
|
600
758
|
if (!app.service('$projects').active) throw new Error('No active project');
|
|
601
759
|
|
|
602
|
-
if (!
|
|
760
|
+
if (!this._pathHas(app.service('$projects').active, path)) {
|
|
603
761
|
return this.setProjectState(path, value, options)
|
|
604
762
|
.then(()=> true)
|
|
605
763
|
} else {
|
|
@@ -944,6 +1102,7 @@ export default class TeraFyServer {
|
|
|
944
1102
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
945
1103
|
* @param {String} [options.title='TERA'] The title of the alert box
|
|
946
1104
|
* @param {Boolean} [options.isHtml=false] If falsy the text is rendered as plain-text otherwise it will be assumed as HTML content
|
|
1105
|
+
* @param {'ok'|false} [options.buttons='ok'] Button set to use or falsy to disable
|
|
947
1106
|
*
|
|
948
1107
|
* @returns {Promise} A promise which resolves when the alert has been dismissed
|
|
949
1108
|
*/
|
|
@@ -951,6 +1110,7 @@ export default class TeraFyServer {
|
|
|
951
1110
|
let settings = {
|
|
952
1111
|
title: 'TERA',
|
|
953
1112
|
isHtml: false,
|
|
1113
|
+
buttons: 'ok',
|
|
954
1114
|
...options,
|
|
955
1115
|
};
|
|
956
1116
|
|
|
@@ -958,12 +1118,100 @@ export default class TeraFyServer {
|
|
|
958
1118
|
app.service('$prompt').dialog({
|
|
959
1119
|
title: settings.title,
|
|
960
1120
|
body: text,
|
|
961
|
-
buttons:
|
|
1121
|
+
buttons:
|
|
1122
|
+
settings.buttons == 'ok' ? ['ok']
|
|
1123
|
+
: false,
|
|
962
1124
|
isHtml: settings.isHtml,
|
|
963
1125
|
dialogClose: 'resolve',
|
|
964
1126
|
})
|
|
965
1127
|
);
|
|
966
1128
|
}
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Open a popup window containing a new site
|
|
1133
|
+
*
|
|
1134
|
+
* @param {String} url The URL to open
|
|
1135
|
+
*
|
|
1136
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
1137
|
+
* @param {Number} [options.width=500] The desired width of the window
|
|
1138
|
+
* @param {Number} [options.height=600] The desired height of the window
|
|
1139
|
+
* @param {Boolean} [options.center=true] Attempt to center the window on the screen
|
|
1140
|
+
* @param {Object} [options.permissions] Additional permissions to set on opening, defaults to a suitable set of permission for popups (see code)
|
|
1141
|
+
*
|
|
1142
|
+
* @returns {WindowProxy} The opened window object (if `noopener` is not set in permissions)
|
|
1143
|
+
*/
|
|
1144
|
+
uiWindow(url, options) {
|
|
1145
|
+
let settings = {
|
|
1146
|
+
width: 500,
|
|
1147
|
+
height: 600,
|
|
1148
|
+
center: true,
|
|
1149
|
+
permissions: {
|
|
1150
|
+
popup: true,
|
|
1151
|
+
location: false,
|
|
1152
|
+
menubar: false,
|
|
1153
|
+
status: false,
|
|
1154
|
+
scrolbars: false,
|
|
1155
|
+
},
|
|
1156
|
+
...options,
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
return window.open(url, '_blank', Object.entries({
|
|
1160
|
+
...settings.permissions,
|
|
1161
|
+
width: settings.width,
|
|
1162
|
+
height: settings.height,
|
|
1163
|
+
...(settings.center && {
|
|
1164
|
+
left: screen.width/2 - settings.width/2,
|
|
1165
|
+
top: screen.height/2 - settings.height/2,
|
|
1166
|
+
}),
|
|
1167
|
+
})
|
|
1168
|
+
.map(([key, val]) => key + '=' + (
|
|
1169
|
+
typeof val == 'boolean' ? val ? '1' : '0' // Map booleans to 1/0
|
|
1170
|
+
: val
|
|
1171
|
+
))
|
|
1172
|
+
.join(', ')
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Display HTML content full-screen within TERA
|
|
1179
|
+
* This function is ideally called within a requestFocus() wrapper
|
|
1180
|
+
*
|
|
1181
|
+
* @param {DOMElement|String|false} content Either a prepared DOM element or string to compile, set to falsy to remove existing content
|
|
1182
|
+
*
|
|
1183
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
1184
|
+
* @param {Boolean|String} [options.logo=false] Add a logo to the output, if boolean true the Tera-tools logo is used otherwise specify a path or URL
|
|
1185
|
+
*/
|
|
1186
|
+
uiSplat(content, options) {
|
|
1187
|
+
let settings = {
|
|
1188
|
+
logo: false,
|
|
1189
|
+
...options,
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
if (!content) { // Remove content
|
|
1193
|
+
globalThis.document.body.querySelector('.tera-fy-uiSplat').remove();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
let compiledContent = typeof content == 'string'
|
|
1198
|
+
? (()=> {
|
|
1199
|
+
let el = document.createElement('div')
|
|
1200
|
+
el.innerHTML = content;
|
|
1201
|
+
return el;
|
|
1202
|
+
})()
|
|
1203
|
+
: content;
|
|
1204
|
+
|
|
1205
|
+
compiledContent.classList.add('tera-fy-uiSplat');
|
|
1206
|
+
|
|
1207
|
+
if (settings.logo) {
|
|
1208
|
+
let logoEl = document.createElement('div');
|
|
1209
|
+
logoEl.innerHTML = `<img src="${typeof settings.logo == 'string' ? settings.logo : '/assets/logo/logo.svg'}" class="img-logo"/>`;
|
|
1210
|
+
compiledContent.prepend(logoEl);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
globalThis.document.body.append(compiledContent);
|
|
1214
|
+
}
|
|
967
1215
|
// }}}
|
|
968
1216
|
|
|
969
1217
|
// Utility - debug() {{{
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iebh/tera-fy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "TERA website worker",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --
|
|
6
|
+
"dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --serve --servedir=.",
|
|
7
7
|
"build": "concurrently 'npm:build:*'",
|
|
8
|
-
"build:client": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify
|
|
9
|
-
"build:client:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle lib/terafy.client.js --outfile=dist/terafy.es2019.js --minify
|
|
10
|
-
"build:plugins-vue2:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle plugins/vue2.js --outfile=dist/plugin.vue2.es2019.js --minify
|
|
8
|
+
"build:client": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify",
|
|
9
|
+
"build:client:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle lib/terafy.client.js --outfile=dist/terafy.es2019.js --minify",
|
|
10
|
+
"build:plugins-vue2:es2019": "esbuild --platform=browser --format=esm --target=es2019 --bundle plugins/vue2.js --outfile=dist/plugin.vue2.es2019.js --minify",
|
|
11
11
|
"build:docs:api": "documentation build lib/terafy.client.js --format html --config documentation.yml --output docs/",
|
|
12
12
|
"build:docs:markdown": "documentation build lib/terafy.client.js --format md --markdown-toc --output api.md",
|
|
13
|
+
"watch": "nodemon --watch lib/terafy.client.js --exec npm run build",
|
|
13
14
|
"lint": "eslint ."
|
|
14
15
|
},
|
|
15
16
|
"type": "module",
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
"default": "./dist/terafy.js"
|
|
26
27
|
},
|
|
27
28
|
"./server": "./lib/terafy.server.js",
|
|
28
|
-
"./plugins/*": "./plugins/*.js"
|
|
29
|
+
"./plugins/*": "./plugins/*.js",
|
|
30
|
+
"./widgets/*": "./widgets/*.js"
|
|
29
31
|
},
|
|
30
32
|
"repository": {
|
|
31
33
|
"type": "git",
|
|
@@ -62,21 +64,23 @@
|
|
|
62
64
|
"engines": {
|
|
63
65
|
"node": ">=18"
|
|
64
66
|
},
|
|
65
|
-
"
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"just-diff": "^6.0.2",
|
|
66
69
|
"lodash-es": "^4.17.21",
|
|
70
|
+
"mitt": "^3.0.1",
|
|
67
71
|
"nanoid": "^5.0.2"
|
|
68
72
|
},
|
|
69
|
-
"optionalDependencies": {
|
|
70
|
-
"just-diff": "^6.0.2",
|
|
71
|
-
"just-diff-apply": "^5.5.0",
|
|
72
|
-
"vue": "^3.3.7"
|
|
73
|
-
},
|
|
74
73
|
"devDependencies": {
|
|
75
74
|
"@momsfriendlydevco/eslint-config": "^1.0.7",
|
|
76
75
|
"concurrently": "^8.2.2",
|
|
77
76
|
"documentation": "^14.0.2",
|
|
78
77
|
"esbuild": "^0.19.5"
|
|
79
78
|
},
|
|
79
|
+
"optionalDependencies": {
|
|
80
|
+
"@iebh/reflib": "^2.2.2",
|
|
81
|
+
"just-diff-apply": "^5.5.0",
|
|
82
|
+
"vue": "^3.3.7"
|
|
83
|
+
},
|
|
80
84
|
"eslintConfig": {
|
|
81
85
|
"extends": "@momsfriendlydevco",
|
|
82
86
|
"env": {
|
|
@@ -87,9 +91,5 @@
|
|
|
87
91
|
"ecmaVersion": 13,
|
|
88
92
|
"sourceType": "module"
|
|
89
93
|
}
|
|
90
|
-
},
|
|
91
|
-
"dependencies": {
|
|
92
|
-
"@momsfriendlydevco/supabase-reactive": "^1.0.7",
|
|
93
|
-
"mitt": "^3.0.1"
|
|
94
94
|
}
|
|
95
95
|
}
|
package/plugins/vue2.js
CHANGED
|
@@ -151,6 +151,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
|
|
|
151
151
|
* @param {Object} options.app Root level Vue app to bind against
|
|
152
152
|
* @param {Vue} options.Vue Vue@2 instance to bind against
|
|
153
153
|
* @param {String} [options.globalName='$tera'] Global property to allocate this service as within Vue2
|
|
154
|
+
* @param {Boolean} [options.requireProject=true] Automatically call requireProject() prior to any operation
|
|
154
155
|
* @param {Boolean} [options.subscribeState=true] Setup `vm.$tera.state` as a live binding on init
|
|
155
156
|
* @param {Boolean} [options.subscribeList=true] Setup `vm.$tera.projects` as a list of accesible projects on init
|
|
156
157
|
* @param {Objecct} [options.stateOptions] Options passed to `bindProjectState()` when setting up the main state
|
|
@@ -162,6 +163,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
|
|
|
162
163
|
app: null,
|
|
163
164
|
Vue: null,
|
|
164
165
|
globalName: '$tera',
|
|
166
|
+
requireProject: true,
|
|
165
167
|
subscribeState: true,
|
|
166
168
|
subscribeProjects: true,
|
|
167
169
|
stateOptions: {
|
|
@@ -190,6 +192,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
|
|
|
190
192
|
|
|
191
193
|
// this.statePromisable becomes the promise we are waiting on to resolve
|
|
192
194
|
return Promise.resolve()
|
|
195
|
+
.then(()=> settings.requireProject && this.requireProject())
|
|
193
196
|
.then(()=> Promise.all([
|
|
194
197
|
// Bind available project and wait on it
|
|
195
198
|
settings.subscribeState && this.bindProjectState({
|
|
@@ -205,4 +208,69 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
|
|
|
205
208
|
.then(()=> this.debug('INFO', 'Loaded projects', this.projects)),
|
|
206
209
|
]))
|
|
207
210
|
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Override the regular _pathSet() handler so that it stays as a Vue observable
|
|
215
|
+
* This function will correctly populate any missing entities, calling vm.$set on each traversal of the path This is mainly to fix the issue where Vue can't see changes to objects if a key is added or removed
|
|
216
|
+
* Passing undefined as a value removes the key (unless removeUndefined is set to false)
|
|
217
|
+
*
|
|
218
|
+
* @override
|
|
219
|
+
* @param {Object} [target] The target to set the path of, if omitted the `vm` object is used as the base for traversal
|
|
220
|
+
* @param {string|array} path The path to set within the target / vm
|
|
221
|
+
* @param {*} value The value to set
|
|
222
|
+
* @param {Object} [options] Additional options
|
|
223
|
+
* @param {boolean} [options.arrayNumeric=true] Process numeric path segments as arrays
|
|
224
|
+
* @param {boolean} [options.removeUndefined=true] If undefined is specified as a value the key is removed instead of being set
|
|
225
|
+
* @param {boolean} [options.debug=false] Also print out debugging information when setting the value
|
|
226
|
+
* @returns {Object} The set value, like $set()
|
|
227
|
+
*
|
|
228
|
+
* @example Set a deeply nested path within a target object
|
|
229
|
+
* vm.$setPath(this, 'foo.bar.baz', 123); // this.$data.foo.bar.baz = 123
|
|
230
|
+
*
|
|
231
|
+
* @example Set a deeply nested path, with arrays, assuming VM as the root node
|
|
232
|
+
* vm.$setPath('foo.1.bar', 123); // vm.$data.foo = [{bar: 123}]
|
|
233
|
+
*/
|
|
234
|
+
_pathSet(target, path, value, options) {
|
|
235
|
+
// Sanity checks {{{
|
|
236
|
+
if (typeof target != 'object') throw new Error('Cannot use _pathSet on non-object target');
|
|
237
|
+
// }}}
|
|
238
|
+
|
|
239
|
+
let settings = {
|
|
240
|
+
arrayNumeric: true,
|
|
241
|
+
debug: false,
|
|
242
|
+
removeUndefined: true,
|
|
243
|
+
...options,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (settings.debug) console.log('[_pathSet]', path, '=', value, {target, options});
|
|
247
|
+
|
|
248
|
+
let node = target;
|
|
249
|
+
if (!path) throw new Error('Cannot _pathSet with undefined path');
|
|
250
|
+
(typeof path == 'string' ? path.split('.') : path).some((chunk, chunkIndex, chunks) => {
|
|
251
|
+
if (chunkIndex == chunks.length - 1) { // Leaf node
|
|
252
|
+
if (settings.removeUndefined && value === undefined) {
|
|
253
|
+
this.Vue.$delete(node, chunk);
|
|
254
|
+
} else {
|
|
255
|
+
this.Vue.set(node, chunk, value);
|
|
256
|
+
}
|
|
257
|
+
} else if (node[chunk] === undefined) { // This chunk (and all following chunks) does't exist - populate from here
|
|
258
|
+
chunks.slice(chunkIndex, chunks.length - 1).forEach(chunk => {
|
|
259
|
+
if (settings.arrayNumeric && isFinite(chunk)) {
|
|
260
|
+
this.Vue.set(node, chunk, []);
|
|
261
|
+
} else {
|
|
262
|
+
this.Vue.set(node, chunk, {});
|
|
263
|
+
}
|
|
264
|
+
node = node[chunk];
|
|
265
|
+
});
|
|
266
|
+
this.Vue.set(node, chunks[chunks.length - 1], value);
|
|
267
|
+
return true;
|
|
268
|
+
} else {
|
|
269
|
+
node = node[chunk];
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
208
276
|
}
|
package/utils/pDefer.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a defer object which represents a promise object which can resolve in the future - but without enclosing a function
|
|
3
|
+
* The defer object has the keys {promise, resolve(), reject()}
|
|
4
|
+
* @returns {Defer} A defered promise
|
|
5
|
+
*/
|
|
6
|
+
export default function() {
|
|
7
|
+
var deferred = {};
|
|
8
|
+
|
|
9
|
+
deferred.promise = new Promise((resolve, reject) => {
|
|
10
|
+
deferred.resolve = resolve;
|
|
11
|
+
deferred.reject = reject;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return deferred;
|
|
15
|
+
}
|