@iebh/tera-fy 1.0.16 → 1.0.18
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 -2
- package/api.md +333 -188
- package/dist/plugin.vue2.es2019.js +1 -1
- package/dist/plugin.vue2.es2019.js.map +3 -3
- package/dist/terafy.es2019.js +2 -2
- package/dist/terafy.es2019.js.map +3 -3
- package/dist/terafy.js +2 -2
- package/dist/terafy.js.map +3 -3
- package/hints.md +26 -0
- package/lib/terafy.client.js +149 -18
- package/lib/terafy.server.js +415 -54
- package/package.json +10 -11
- package/plugins/vue2.js +68 -0
- package/utils/pDefer.js +15 -0
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,91 @@ 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
|
+
// ... then refresh the project list as we're likely going to need it
|
|
451
|
+
await app.service('$projects').refresh();
|
|
452
|
+
|
|
453
|
+
// Go back to start of auth checking loop and repull the user data
|
|
454
|
+
throw 'REDO';
|
|
455
|
+
|
|
456
|
+
break;
|
|
457
|
+
default:
|
|
458
|
+
// Pass - Implied - Cannot authenticate via other method so just fall through to scalding the user
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
.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
462
|
title: 'TERA-tools account needed',
|
|
366
463
|
isHtml: true,
|
|
464
|
+
buttons: false,
|
|
367
465
|
}))
|
|
466
|
+
.then(()=> { throw 'REDO' }) // Go into loop to keep requesting user data
|
|
467
|
+
.catch(e => {
|
|
468
|
+
if (e === 'EXIT') {
|
|
469
|
+
return; // Exit with a valid user
|
|
470
|
+
} else if (e == 'REDO') {
|
|
471
|
+
return this.requireUser();
|
|
472
|
+
}
|
|
473
|
+
throw e;
|
|
474
|
+
})
|
|
368
475
|
}
|
|
369
476
|
|
|
370
477
|
// }}}
|
|
@@ -495,7 +602,7 @@ export default class TeraFyServer {
|
|
|
495
602
|
*
|
|
496
603
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
497
604
|
* @param {String} [options.title="Select a project to work with"] The title of the dialog to display
|
|
498
|
-
* @param {Boolean} [options.allowCancel=true]
|
|
605
|
+
* @param {Boolean} [options.allowCancel=true] Allow cancelling the operation, will throw `'CANCEL'` if actioned
|
|
499
606
|
* @param {Boolean} [options.setActive=false] Also set the project as active when selected
|
|
500
607
|
*
|
|
501
608
|
* @returns {Promise<Project>} The active project
|
|
@@ -526,7 +633,7 @@ export default class TeraFyServer {
|
|
|
526
633
|
|
|
527
634
|
// }}}
|
|
528
635
|
|
|
529
|
-
// Project State - getProjectState(), setProjectState(), saveProjectState(), replaceProjectState(), applyProjectStatePatch {{{
|
|
636
|
+
// Project State - getProjectState(), setProjectState(), saveProjectState(), replaceProjectState(), applyProjectStatePatch() {{{
|
|
530
637
|
|
|
531
638
|
/**
|
|
532
639
|
* Return the current, full snapshot state of the active project
|
|
@@ -576,16 +683,61 @@ export default class TeraFyServer {
|
|
|
576
683
|
if (!app.service('$projects').active) throw new Error('No active project');
|
|
577
684
|
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
685
|
|
|
579
|
-
|
|
686
|
+
this._pathSet(app.service('$projects').active, path, value);
|
|
580
687
|
|
|
581
688
|
return (
|
|
582
|
-
|
|
583
|
-
:
|
|
689
|
+
settings.save && settings.sync ? this.saveProjectState()
|
|
690
|
+
: settings.save ? void this.saveProjectState()
|
|
584
691
|
: (()=> { throw new Error('setProjectState({sync: true, save: false}) makes no sense') })()
|
|
585
692
|
);
|
|
586
693
|
}
|
|
587
694
|
|
|
588
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Internal recursive path setter used by setProjectState() / setProjectStateDefaults()
|
|
698
|
+
* The implementation defaults to _.set() unless overriden by a plugin
|
|
699
|
+
*
|
|
700
|
+
* @private
|
|
701
|
+
* @param {Object} subject The base subject to operate on
|
|
702
|
+
* @param {String|Array} path The path to set in dotted or array notation
|
|
703
|
+
* @param {*} value The value to set
|
|
704
|
+
*
|
|
705
|
+
* @returns {*} The set value
|
|
706
|
+
*/
|
|
707
|
+
_pathSet(subject, path, value) {
|
|
708
|
+
return pathSet(subject, path, value);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Internal recursive path checker used by setProjectStateDefaults()
|
|
714
|
+
* The implementation defaults to _.has() unless overriden by a plugin
|
|
715
|
+
*
|
|
716
|
+
* @private
|
|
717
|
+
* @param {Object} subject The base subject to examine
|
|
718
|
+
* @param {String|Array} path The path to fetch in dotted or array notation
|
|
719
|
+
* @returns {Boolean} True if the given path already exists within the subject
|
|
720
|
+
*/
|
|
721
|
+
_pathHas(subject, path) {
|
|
722
|
+
return pathExists(subject, path);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Internal recursive path fetcher
|
|
728
|
+
* The implementation defaults to _.get() unless overriden by a plugin
|
|
729
|
+
*
|
|
730
|
+
* @private
|
|
731
|
+
* @param {Object} subject The base subject to examine
|
|
732
|
+
* @param {String|Array} path The path to fetch in dotted or array notation
|
|
733
|
+
* @param {*} [fallback] Optional fallback to return if the end point does not exist
|
|
734
|
+
* @returns {*} True if the given path already exists within the subject
|
|
735
|
+
*/
|
|
736
|
+
_pathGet(subject, path, fallback) {
|
|
737
|
+
return pathGet(subject, path, fallback);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
|
|
589
741
|
/**
|
|
590
742
|
* Set a nested value within the project state - just like `setProjectState()` - but only if no value for that path exists
|
|
591
743
|
*
|
|
@@ -599,7 +751,7 @@ export default class TeraFyServer {
|
|
|
599
751
|
setProjectStateDefaults(path, value, options) {
|
|
600
752
|
if (!app.service('$projects').active) throw new Error('No active project');
|
|
601
753
|
|
|
602
|
-
if (!
|
|
754
|
+
if (!this._pathHas(app.service('$projects').active, path)) {
|
|
603
755
|
return this.setProjectState(path, value, options)
|
|
604
756
|
.then(()=> true)
|
|
605
757
|
} else {
|
|
@@ -654,7 +806,7 @@ export default class TeraFyServer {
|
|
|
654
806
|
}
|
|
655
807
|
// }}}
|
|
656
808
|
|
|
657
|
-
// Project files - getProjectFiles() {{{
|
|
809
|
+
// Project files - selectProjectFile(), getProjectFiles() {{{
|
|
658
810
|
|
|
659
811
|
/**
|
|
660
812
|
* Data structure for a project file
|
|
@@ -674,6 +826,72 @@ export default class TeraFyServer {
|
|
|
674
826
|
* @property {String} mime The associated mime type for the file
|
|
675
827
|
*/
|
|
676
828
|
|
|
829
|
+
/**
|
|
830
|
+
* Data structure for a file filter
|
|
831
|
+
* @class FileFilters
|
|
832
|
+
*
|
|
833
|
+
* @property {Boolean} [library=false] Restrict to library files only
|
|
834
|
+
* @property {String} [filename] CSV of @momsfriendlydevco/match expressions to filter the filename by (filenames are the basename sans extension)
|
|
835
|
+
* @property {String} [basename] CSV of @momsfriendlydevco/match expressions to filter the basename by
|
|
836
|
+
* @property {String} [ext] CSV of @momsfriendlydevco/match expressions to filter the file extension by
|
|
837
|
+
*/
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Prompt the user to select a library to operate on
|
|
841
|
+
*
|
|
842
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
843
|
+
* @param {String} [options.title="Select a file"] The title of the dialog to display
|
|
844
|
+
* @param {String|Array<String>} [options.hint] Hints to identify the file to select in array order of preference
|
|
845
|
+
* @param {FileFilters} [options.filters] Optional file filters
|
|
846
|
+
* @param {Boolean} [options.allowUpload=true] Allow uploading new files
|
|
847
|
+
* @param {Boolean} [options.allowRefresh=true] Allow the user to manually refresh the file list
|
|
848
|
+
* @param {Boolean} [options.allowDownloadZip=true] Allow the user to download a Zip of all files
|
|
849
|
+
* @param {Boolean} [options.allowCancel=true] Allow cancelling the operation. Will throw `'CANCEL'` as the promise rejection if acationed
|
|
850
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
851
|
+
* @param {FileFilters} [options.filter] Optional file filters
|
|
852
|
+
*
|
|
853
|
+
* @returns {Promise<ProjectFile>} The eventually selected file
|
|
854
|
+
*/
|
|
855
|
+
selectProjectFile(options) {
|
|
856
|
+
let settings = {
|
|
857
|
+
title: 'Select a file',
|
|
858
|
+
hint: null,
|
|
859
|
+
filters: {},
|
|
860
|
+
allowUpload: true,
|
|
861
|
+
allowRefresh: true,
|
|
862
|
+
allowDownloadZip: true,
|
|
863
|
+
allowCancel: true,
|
|
864
|
+
autoRequire: true,
|
|
865
|
+
...options,
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
return app.service('$projects').promise()
|
|
869
|
+
.then(()=> settings.autoRequire && this.requireProject())
|
|
870
|
+
.then(files => this.requestFocus(()=>
|
|
871
|
+
app.service('$prompt').dialog({
|
|
872
|
+
title: settings.title,
|
|
873
|
+
component: 'filesSelect',
|
|
874
|
+
componentProps: {
|
|
875
|
+
hint: settings.hint,
|
|
876
|
+
allowNavigate: false,
|
|
877
|
+
allowUpload: settings.allowUpload,
|
|
878
|
+
allowRefresh: settings.allowRefresh,
|
|
879
|
+
allowDownloadZip: settings.allowDownloadZip,
|
|
880
|
+
allowVerbs: false,
|
|
881
|
+
cardStyle: false,
|
|
882
|
+
filters: settings.filters,
|
|
883
|
+
},
|
|
884
|
+
componentEvents: {
|
|
885
|
+
fileSelect(file) {
|
|
886
|
+
app.service('$prompt').close(true, file);
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
modalDialogClass: 'modal-dialog-lg',
|
|
890
|
+
buttons: settings.allowCancel && ['cancel'],
|
|
891
|
+
})
|
|
892
|
+
))
|
|
893
|
+
}
|
|
894
|
+
|
|
677
895
|
|
|
678
896
|
/**
|
|
679
897
|
* Fetch the files associated with a given project
|
|
@@ -682,7 +900,7 @@ export default class TeraFyServer {
|
|
|
682
900
|
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
683
901
|
* @param {Boolean} [options.meta=true] Pull meta information for each file entity
|
|
684
902
|
*
|
|
685
|
-
* @returns {Promise<ProjectFile
|
|
903
|
+
* @returns {Promise<Array<ProjectFile>>} A collection of project files for the given project
|
|
686
904
|
*/
|
|
687
905
|
getProjectFiles(options) {
|
|
688
906
|
let settings = {
|
|
@@ -700,61 +918,81 @@ export default class TeraFyServer {
|
|
|
700
918
|
}
|
|
701
919
|
// }}}
|
|
702
920
|
|
|
703
|
-
// Project Libraries -
|
|
921
|
+
// Project Libraries - selectProjectLibrary(), parseProjectLibrary(), setProjectLibrary() {{{
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Prompt the user to select a library to operate on and return a array of references in a given format
|
|
925
|
+
*
|
|
926
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
927
|
+
* @param {String} [options.title="Select a citation library"] The title of the dialog to display
|
|
928
|
+
* @param {String|Array<String>} [options.hint] Hints to identify the library to select in array order of preference. Generally corresponds to the previous stage - e.g. 'deduped', 'review1', 'review2', 'dedisputed'
|
|
929
|
+
* @param {Boolean} [options.allowUpload=true] Allow uploading new files
|
|
930
|
+
* @param {Boolean} [options.allowRefresh=true] Allow the user to manually refresh the file list
|
|
931
|
+
* @param {Boolean} [options.allowDownloadZip=true] Allow the user to download a Zip of all files
|
|
932
|
+
* @param {Boolean} [options.allowCancel=true] Allow cancelling the operation. Will throw `'CANCEL'` as the promise rejection if acationed
|
|
933
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
934
|
+
* @param {FileFilters} [options.filters] Optional file filters, defaults to citation library selection only
|
|
935
|
+
* @param {*} [options.*] Additional options - see `parseProjectLibrary()`
|
|
936
|
+
*
|
|
937
|
+
* @returns {Promise<Array<Ref>>} A collection of references from the selected file
|
|
938
|
+
*/
|
|
939
|
+
selectProjectLibrary(options) {
|
|
940
|
+
let settings = {
|
|
941
|
+
title: 'Select a citation library',
|
|
942
|
+
hint: null,
|
|
943
|
+
allowUpload: true,
|
|
944
|
+
allowRefresh: true,
|
|
945
|
+
allowDownloadZip: true,
|
|
946
|
+
allowCancel: true,
|
|
947
|
+
autoRequire: true,
|
|
948
|
+
filters: {
|
|
949
|
+
library: true,
|
|
950
|
+
...options?.filter,
|
|
951
|
+
},
|
|
952
|
+
...options,
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
return app.service('$projects').promise()
|
|
956
|
+
.then(()=> this.selectProjectFile(settings))
|
|
957
|
+
.then(selectedFile => this.parseProjectLibrary(selectedFile.path, settings))
|
|
958
|
+
}
|
|
959
|
+
|
|
704
960
|
|
|
705
961
|
/**
|
|
706
|
-
*
|
|
962
|
+
* Convert a project file into a library of citations
|
|
707
963
|
*
|
|
708
|
-
* @param {String}
|
|
964
|
+
* @param {String} path File path to read, if omitted the contents of `options` are used to guess at a suitable file
|
|
709
965
|
*
|
|
710
966
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
711
967
|
* @param {String} [options.format='json'] Format for the file. ENUM: 'pojo' (return a parsed JS collection), 'blob' (raw JS Blob object), 'file' (named JS File object)
|
|
712
968
|
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
713
|
-
* @param {Boolean} [options.multiple=false] Allow selection of multiple libraries
|
|
714
969
|
* @param {Function} [options.filter] Optional async file filter, called each time as `(File:ProjectFile)`
|
|
715
970
|
* @param {Function} [options.find] Optional async final stage file filter to reduce all candidates down to one subject file
|
|
716
|
-
* @param {String|Array<String>} [options.hint] Hints to identify the library to select in array order of preference. Generally corresponds to the previous stage - e.g. 'deduped', 'review1', 'review2', 'dedisputed'
|
|
717
971
|
*
|
|
718
|
-
* @returns {Promise<Array<
|
|
972
|
+
* @returns {Promise<Array<Ref>>|Promise<*>} A collection of references (default bevahiour) or a whatever format was requested
|
|
719
973
|
*/
|
|
720
|
-
|
|
974
|
+
parseProjectLibrary(path, options) {
|
|
721
975
|
let settings = {
|
|
722
|
-
path,
|
|
723
976
|
format: 'pojo',
|
|
724
977
|
autoRequire: true,
|
|
725
|
-
multiple: false,
|
|
726
|
-
hint: null,
|
|
727
978
|
filter: file => true,
|
|
728
979
|
find: files => files.at(0),
|
|
729
|
-
...
|
|
980
|
+
...options,
|
|
730
981
|
};
|
|
731
982
|
|
|
732
983
|
return Promise.resolve()
|
|
733
|
-
.then(()=>
|
|
734
|
-
|
|
735
|
-
if (!settings.path.startsWith('/')) throw new Error('All file names must start with a forward slash');
|
|
736
|
-
return settings.path;
|
|
737
|
-
} else { // Try to guess the file from the options structure
|
|
738
|
-
return this.getProjectFiles({
|
|
739
|
-
autoRequire: settings.autoRequire,
|
|
740
|
-
})
|
|
741
|
-
.then(files => files.filter(file =>
|
|
742
|
-
settings.filter(file)
|
|
743
|
-
))
|
|
744
|
-
.then(files => settings.find(files))
|
|
745
|
-
.then(file => file.path)
|
|
746
|
-
}
|
|
747
|
-
})
|
|
748
|
-
.then(filePath => app.service('$supabase').fileGet(filePath, {
|
|
984
|
+
.then(()=> settings.autoRequire && this.requireProject())
|
|
985
|
+
.then(()=> app.service('$supabase').fileGet(path, {
|
|
749
986
|
toast: false,
|
|
750
987
|
}))
|
|
751
988
|
.then(blob => {
|
|
752
989
|
switch (settings.format) {
|
|
990
|
+
// NOTE: Any updates to the format list should also extend setProjectLibrary()
|
|
753
991
|
case 'pojo':
|
|
754
992
|
return Reflib.uploadFile({
|
|
755
993
|
file: new File(
|
|
756
994
|
[blob],
|
|
757
|
-
app.service('$supabase')._parsePath(
|
|
995
|
+
app.service('$supabase')._parsePath(path).basename,
|
|
758
996
|
),
|
|
759
997
|
});
|
|
760
998
|
case 'blob':
|
|
@@ -762,7 +1000,7 @@ export default class TeraFyServer {
|
|
|
762
1000
|
case 'file':
|
|
763
1001
|
return new File(
|
|
764
1002
|
[blob],
|
|
765
|
-
app.service('$supabase')._parsePath(
|
|
1003
|
+
app.service('$supabase')._parsePath(path).basename,
|
|
766
1004
|
);
|
|
767
1005
|
default:
|
|
768
1006
|
throw new Error(`Unsupported library format "${settings.format}"`);
|
|
@@ -772,26 +1010,59 @@ export default class TeraFyServer {
|
|
|
772
1010
|
|
|
773
1011
|
|
|
774
1012
|
/**
|
|
775
|
-
* Save back a
|
|
1013
|
+
* Save back a citation library from some input
|
|
776
1014
|
*
|
|
1015
|
+
* @param {String} [path] File path to save back to
|
|
777
1016
|
* @param {Array<RefLibRef>} Collection of references for the selected library
|
|
778
1017
|
*
|
|
779
1018
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
1019
|
+
* @param {String} [options.format='json'] Input format used. ENUM: 'pojo' (return a parsed JS collection), 'blob' (raw JS Blob object), 'file' (named JS File object)
|
|
780
1020
|
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
781
1021
|
* @param {String} [options.hint] Hint to store against the library. Generally corresponds to the current operation being performed - e.g. 'deduped'
|
|
1022
|
+
* @param {Boolean} [options.overwrite=true] Allow existing file upsert
|
|
1023
|
+
* @param {Object} [options.meta] Optional meta data to merge into the file data
|
|
782
1024
|
*
|
|
783
1025
|
* @returns {Promise} A promise which resolves when the save operation has completed
|
|
784
1026
|
*/
|
|
785
|
-
setProjectLibrary(refs, options) {
|
|
1027
|
+
setProjectLibrary(path, refs, options) {
|
|
786
1028
|
let settings = {
|
|
787
1029
|
autoRequire: true,
|
|
788
1030
|
hint: null,
|
|
1031
|
+
overwrite: true,
|
|
1032
|
+
meta: null,
|
|
789
1033
|
...options,
|
|
790
1034
|
};
|
|
791
1035
|
|
|
792
1036
|
return Promise.resolve()
|
|
793
1037
|
.then(()=> settings.autoRequire && this.requireProject())
|
|
794
|
-
|
|
1038
|
+
.then(()=> {
|
|
1039
|
+
switch (settings.format) {
|
|
1040
|
+
// NOTE: Any updates to the format list should also extend parseProjectLibrary()
|
|
1041
|
+
case 'pojo': // Use as is
|
|
1042
|
+
if (!Array.isArray(refs)) throw new Error('setProjectLibrary() with format=pojo requires an array of references');
|
|
1043
|
+
return refs;
|
|
1044
|
+
case 'blob':
|
|
1045
|
+
case 'file':
|
|
1046
|
+
return refs; // Following functions should pass thru these formats anyway
|
|
1047
|
+
default:
|
|
1048
|
+
throw new Error(`Unsupported library format "${settings.format}"`);
|
|
1049
|
+
}
|
|
1050
|
+
})
|
|
1051
|
+
.then(refs =>
|
|
1052
|
+
refs instanceof Blob ? new File([refs], app.service('$supabase')._parsePath(path).basename)
|
|
1053
|
+
: refs instanceof File ? refs
|
|
1054
|
+
: Reflib.downloadFile(refs, { // Get Reflib to encode for us
|
|
1055
|
+
filename: path,
|
|
1056
|
+
promptDownload: false, // Just return the fileBlob we hand to Supabase
|
|
1057
|
+
})
|
|
1058
|
+
)
|
|
1059
|
+
.then(fileBlob => this.$supabase.fileUpload(path, {
|
|
1060
|
+
file: fileBlob,
|
|
1061
|
+
mode: 'encoded',
|
|
1062
|
+
overwrite: true,
|
|
1063
|
+
meta: settings.meta,
|
|
1064
|
+
transcoders: false, // We can skip transcoders as we are supplying the meta information above anyway
|
|
1065
|
+
}))
|
|
795
1066
|
}
|
|
796
1067
|
|
|
797
1068
|
// }}}
|
|
@@ -825,6 +1096,7 @@ export default class TeraFyServer {
|
|
|
825
1096
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
826
1097
|
* @param {String} [options.title='TERA'] The title of the alert box
|
|
827
1098
|
* @param {Boolean} [options.isHtml=false] If falsy the text is rendered as plain-text otherwise it will be assumed as HTML content
|
|
1099
|
+
* @param {'ok'|false} [options.buttons='ok'] Button set to use or falsy to disable
|
|
828
1100
|
*
|
|
829
1101
|
* @returns {Promise} A promise which resolves when the alert has been dismissed
|
|
830
1102
|
*/
|
|
@@ -832,6 +1104,7 @@ export default class TeraFyServer {
|
|
|
832
1104
|
let settings = {
|
|
833
1105
|
title: 'TERA',
|
|
834
1106
|
isHtml: false,
|
|
1107
|
+
buttons: 'ok',
|
|
835
1108
|
...options,
|
|
836
1109
|
};
|
|
837
1110
|
|
|
@@ -839,12 +1112,100 @@ export default class TeraFyServer {
|
|
|
839
1112
|
app.service('$prompt').dialog({
|
|
840
1113
|
title: settings.title,
|
|
841
1114
|
body: text,
|
|
842
|
-
buttons:
|
|
1115
|
+
buttons:
|
|
1116
|
+
settings.buttons == 'ok' ? ['ok']
|
|
1117
|
+
: false,
|
|
843
1118
|
isHtml: settings.isHtml,
|
|
844
1119
|
dialogClose: 'resolve',
|
|
845
1120
|
})
|
|
846
1121
|
);
|
|
847
1122
|
}
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Open a popup window containing a new site
|
|
1127
|
+
*
|
|
1128
|
+
* @param {String} url The URL to open
|
|
1129
|
+
*
|
|
1130
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
1131
|
+
* @param {Number} [options.width=500] The desired width of the window
|
|
1132
|
+
* @param {Number} [options.height=600] The desired height of the window
|
|
1133
|
+
* @param {Boolean} [options.center=true] Attempt to center the window on the screen
|
|
1134
|
+
* @param {Object} [options.permissions] Additional permissions to set on opening, defaults to a suitable set of permission for popups (see code)
|
|
1135
|
+
*
|
|
1136
|
+
* @returns {WindowProxy} The opened window object (if `noopener` is not set in permissions)
|
|
1137
|
+
*/
|
|
1138
|
+
uiWindow(url, options) {
|
|
1139
|
+
let settings = {
|
|
1140
|
+
width: 500,
|
|
1141
|
+
height: 600,
|
|
1142
|
+
center: true,
|
|
1143
|
+
permissions: {
|
|
1144
|
+
popup: true,
|
|
1145
|
+
location: false,
|
|
1146
|
+
menubar: false,
|
|
1147
|
+
status: false,
|
|
1148
|
+
scrolbars: false,
|
|
1149
|
+
},
|
|
1150
|
+
...options,
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
return window.open(url, '_blank', Object.entries({
|
|
1154
|
+
...settings.permissions,
|
|
1155
|
+
width: settings.width,
|
|
1156
|
+
height: settings.height,
|
|
1157
|
+
...(settings.center && {
|
|
1158
|
+
left: screen.width/2 - settings.width/2,
|
|
1159
|
+
top: screen.height/2 - settings.height/2,
|
|
1160
|
+
}),
|
|
1161
|
+
})
|
|
1162
|
+
.map(([key, val]) => key + '=' + (
|
|
1163
|
+
typeof val == 'boolean' ? val ? '1' : '0' // Map booleans to 1/0
|
|
1164
|
+
: val
|
|
1165
|
+
))
|
|
1166
|
+
.join(', ')
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Display HTML content full-screen within TERA
|
|
1173
|
+
* This function is ideally called within a requestFocus() wrapper
|
|
1174
|
+
*
|
|
1175
|
+
* @param {DOMElement|String|false} content Either a prepared DOM element or string to compile, set to falsy to remove existing content
|
|
1176
|
+
*
|
|
1177
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
1178
|
+
* @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
|
|
1179
|
+
*/
|
|
1180
|
+
uiSplat(content, options) {
|
|
1181
|
+
let settings = {
|
|
1182
|
+
logo: false,
|
|
1183
|
+
...options,
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
if (!content) { // Remove content
|
|
1187
|
+
globalThis.document.body.querySelector('.tera-fy-uiSplat').remove();
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
let compiledContent = typeof content == 'string'
|
|
1192
|
+
? (()=> {
|
|
1193
|
+
let el = document.createElement('div')
|
|
1194
|
+
el.innerHTML = content;
|
|
1195
|
+
return el;
|
|
1196
|
+
})()
|
|
1197
|
+
: content;
|
|
1198
|
+
|
|
1199
|
+
compiledContent.classList.add('tera-fy-uiSplat');
|
|
1200
|
+
|
|
1201
|
+
if (settings.logo) {
|
|
1202
|
+
let logoEl = document.createElement('div');
|
|
1203
|
+
logoEl.innerHTML = `<img src="${typeof settings.logo == 'string' ? settings.logo : '/assets/logo/logo.svg'}" class="img-logo"/>`;
|
|
1204
|
+
compiledContent.prepend(logoEl);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
globalThis.document.body.append(compiledContent);
|
|
1208
|
+
}
|
|
848
1209
|
// }}}
|
|
849
1210
|
|
|
850
1211
|
// Utility - debug() {{{
|