@iebh/tera-fy 1.0.1
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/.storybook/main.js +22 -0
- package/.storybook/preview.js +17 -0
- package/LICENSE +20 -0
- package/README.md +7 -0
- package/dist/terafy.js +12 -0
- package/dist/terafy.js.map +7 -0
- package/docs/terafy.client.md +154 -0
- package/docs/terafy.server.md +221 -0
- package/index.html +258 -0
- package/lib/terafy.client.js +345 -0
- package/lib/terafy.server.js +362 -0
- package/package.json +78 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {nanoid} from 'nanoid';
|
|
2
|
+
import {reactive, watch} from 'vue';
|
|
3
|
+
import diff from 'just-diff';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Main Tera-Fy Client (class singleton) to be used in a frontend browser
|
|
8
|
+
*
|
|
9
|
+
* @class TeraFy
|
|
10
|
+
*/
|
|
11
|
+
export default class TeraFy {
|
|
12
|
+
/**
|
|
13
|
+
* Various settings to configure behaviour
|
|
14
|
+
*
|
|
15
|
+
* @type {Object}
|
|
16
|
+
* @property {Boolean} devMode Operate in devMode - i.e. force outer refresh when encountering an existing TeraFy instance
|
|
17
|
+
* @property {String} siteUrl The TERA URL to connect to
|
|
18
|
+
* @property {String} restrictOrigin URL to restrict communications to
|
|
19
|
+
*/
|
|
20
|
+
settings = {
|
|
21
|
+
devMode: true,
|
|
22
|
+
siteUrl: 'http://localhost:5173/embed',
|
|
23
|
+
restrictOrigin: '*', // DEBUG: Need to restrict this to TERA site
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* DOMElements for this TeraFy instance
|
|
29
|
+
*
|
|
30
|
+
* @type {Object}
|
|
31
|
+
* @property {DOMElement} el The main tera-fy div wrapper
|
|
32
|
+
* @property {DOMElement} iframe The internal iFrame element
|
|
33
|
+
* @property {DOMElement} stylesheet The corresponding stylesheet
|
|
34
|
+
*/
|
|
35
|
+
dom = {
|
|
36
|
+
el: null,
|
|
37
|
+
iframe: null,
|
|
38
|
+
stylesheet: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List of function stubs mapped here from the server
|
|
44
|
+
* This array is forms the reference of `TeraFy.METHOD()` objects to provide locally which will be mapped via `TeraFy.rpc(METHOD, ...args)`
|
|
45
|
+
*
|
|
46
|
+
* @type {Array<String>}
|
|
47
|
+
*/
|
|
48
|
+
methods = [
|
|
49
|
+
// Basics
|
|
50
|
+
'handshake',
|
|
51
|
+
|
|
52
|
+
// Session
|
|
53
|
+
'getUser',
|
|
54
|
+
|
|
55
|
+
// Projects
|
|
56
|
+
'bindProject', 'getProject', 'getProjects', 'requireProject', 'selectProject',
|
|
57
|
+
|
|
58
|
+
// Project state
|
|
59
|
+
'getProjectStateSnapshot', 'applyProjectStatePatch',
|
|
60
|
+
// bindProjectState() - See below
|
|
61
|
+
|
|
62
|
+
// Project Libraries
|
|
63
|
+
'getProjectLibrary', 'setProjectLibrary',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
// Messages - send(), sendRaw(), rpc(), acceptMessage() {{{
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Send a message + wait for a response object
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} message Message object to send
|
|
73
|
+
* @returns {Promise<*>} A promise which resolves when the operation has completed with the remote reply
|
|
74
|
+
*/
|
|
75
|
+
send(message) {
|
|
76
|
+
let id = nanoid();
|
|
77
|
+
|
|
78
|
+
this.acceptPostboxes[id] = {}; // Stub for the deferred promise
|
|
79
|
+
this.acceptPostboxes[id].promise = new Promise((resolve, reject) => {
|
|
80
|
+
Object.assign(this.acceptPostboxes[id], {
|
|
81
|
+
resolve, reject,
|
|
82
|
+
});
|
|
83
|
+
this.sendRaw({
|
|
84
|
+
id,
|
|
85
|
+
...message,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return this.acceptPostboxes[id].promise;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send raw message content to the server
|
|
95
|
+
* This function does not return or wait for a reply - use `send()` for that
|
|
96
|
+
*
|
|
97
|
+
* @param {Object} message Message object to send
|
|
98
|
+
*/
|
|
99
|
+
sendRaw(message) {
|
|
100
|
+
this.dom.iframe.contentWindow.postMessage(
|
|
101
|
+
{
|
|
102
|
+
TERA: 1,
|
|
103
|
+
id: message.id || nanoid(),
|
|
104
|
+
...message,
|
|
105
|
+
},
|
|
106
|
+
this.settings.restrictOrigin
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Call an RPC function in the server instance
|
|
113
|
+
*
|
|
114
|
+
* @param {String} method The method name to call
|
|
115
|
+
* @param {*} [...] Optional arguments to pass to the function
|
|
116
|
+
* @returns {Promise<*>} The resolved output of the server function
|
|
117
|
+
*/
|
|
118
|
+
rpc(method, ...args) {
|
|
119
|
+
return this.send({
|
|
120
|
+
action: 'rpc',
|
|
121
|
+
method,
|
|
122
|
+
args,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Accept an incoming message
|
|
129
|
+
*
|
|
130
|
+
* @param {MessageEvent} Raw message event to process
|
|
131
|
+
*/
|
|
132
|
+
acceptMessage(rawMessage) {
|
|
133
|
+
let message = rawMessage.data;
|
|
134
|
+
if (!message.TERA) return; // Ignore non-TERA signed messages
|
|
135
|
+
|
|
136
|
+
if (message?.id && this.acceptPostboxes[message.id]) { // Postbox waiting for reply
|
|
137
|
+
if (message.isError === true) {
|
|
138
|
+
this.acceptPostboxes[message.id].reject(message.response);
|
|
139
|
+
} else {
|
|
140
|
+
this.acceptPostboxes[message.id].resolve(message.response);
|
|
141
|
+
}
|
|
142
|
+
} else if (message?.id) {
|
|
143
|
+
console.info(`Ignoring message ID ${message.id} - was meant for someone else?`);
|
|
144
|
+
} else {
|
|
145
|
+
console.log('Unexpected incoming TERA-FY CLIENT message', {message});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Listening postboxes, these correspond to outgoing message IDs that expect a response
|
|
152
|
+
*/
|
|
153
|
+
acceptPostboxes = {};
|
|
154
|
+
|
|
155
|
+
// }}}
|
|
156
|
+
|
|
157
|
+
// Init - constructor(), toggleDevMode(), init(), injectMain(), injectStylesheet(), injectMethods() {{{
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Setup the TERA-fy client singleton
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} [options] Additional options to merge into `settings`
|
|
163
|
+
*/
|
|
164
|
+
constructor(options) {
|
|
165
|
+
Object.assign(this.settings, options);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Set or toggle devMode
|
|
171
|
+
*
|
|
172
|
+
* @param {String|Boolean} [devModeEnabled='toggle'] Optional boolean to force dev mode
|
|
173
|
+
*
|
|
174
|
+
* @returns {TeraFy} This chainable terafy instance
|
|
175
|
+
*/
|
|
176
|
+
toggleDevMode(devModeEnabled = 'toggle') {
|
|
177
|
+
this.settings.devMode = devModeEnabled === 'toggle'
|
|
178
|
+
? !this.settings.devMode
|
|
179
|
+
: devModeEnabled;
|
|
180
|
+
|
|
181
|
+
this.dom.el.classList.toggle('dev-mode', this.settings.devMode);
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Initalize the TERA client singleton
|
|
188
|
+
*/
|
|
189
|
+
init() {
|
|
190
|
+
window.addEventListener('message', this.acceptMessage.bind(this));
|
|
191
|
+
|
|
192
|
+
this.injectMain();
|
|
193
|
+
this.injectStylesheet();
|
|
194
|
+
this.injectMethods();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Find an existing active TERA server OR initalize one
|
|
200
|
+
*/
|
|
201
|
+
injectMain() {
|
|
202
|
+
this.dom.el = document.createElement('div')
|
|
203
|
+
this.dom.el.id = 'tera-fy';
|
|
204
|
+
this.dom.el.classList.toggle('dev-mode', this.settings.devMode);
|
|
205
|
+
document.body.append(this.dom.el);
|
|
206
|
+
|
|
207
|
+
this.dom.iframe = document.createElement('iframe')
|
|
208
|
+
|
|
209
|
+
// Queue up event chain when document loads
|
|
210
|
+
this.dom.iframe.setAttribute('sandbox', 'allow-downloads allow-scripts allow-same-origin');
|
|
211
|
+
this.dom.iframe.addEventListener('load', ()=> {
|
|
212
|
+
console.log('TERA EMBED FRAME READY');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Start document load sequence + append to DOM
|
|
216
|
+
this.dom.iframe.src = this.settings.siteUrl;
|
|
217
|
+
this.dom.el.append(this.dom.iframe);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Inject a local stylesheet to handle TERA server functionality
|
|
223
|
+
*/
|
|
224
|
+
injectStylesheet() {
|
|
225
|
+
this.dom.stylesheet = document.createElement('style');
|
|
226
|
+
this.dom.stylesheet.innerHTML = [
|
|
227
|
+
':root {',
|
|
228
|
+
'--TERA-accent: #4d659c;',
|
|
229
|
+
'}',
|
|
230
|
+
|
|
231
|
+
'#tera-fy {',
|
|
232
|
+
'display: none;',
|
|
233
|
+
'position: fixed;',
|
|
234
|
+
'right: 50px;',
|
|
235
|
+
'bottom: 50px;',
|
|
236
|
+
'width: 300px;',
|
|
237
|
+
'height: 150px;',
|
|
238
|
+
'background: transparent;',
|
|
239
|
+
|
|
240
|
+
'&.dev-mode {',
|
|
241
|
+
'display: flex;',
|
|
242
|
+
'border: 5px solid var(--TERA-accent);',
|
|
243
|
+
'background: #FFF;',
|
|
244
|
+
'}',
|
|
245
|
+
|
|
246
|
+
'& > iframe {',
|
|
247
|
+
'width: 100%;',
|
|
248
|
+
'height: 100%;',
|
|
249
|
+
'}',
|
|
250
|
+
'}',
|
|
251
|
+
|
|
252
|
+
// Fullscreen functionality {{{
|
|
253
|
+
'body.tera-fy-fullscreen {',
|
|
254
|
+
'overflow: hidden;',
|
|
255
|
+
|
|
256
|
+
'& #tera-fy {',
|
|
257
|
+
'display: flex !important;',
|
|
258
|
+
'position: fixed !important;',
|
|
259
|
+
'top: 0px !important;',
|
|
260
|
+
'width: 100vw !important;',
|
|
261
|
+
'height: 100vh !important;',
|
|
262
|
+
'left: 0px !important;',
|
|
263
|
+
'z-index: 10000 !important;',
|
|
264
|
+
'}',
|
|
265
|
+
'}',
|
|
266
|
+
// }}}
|
|
267
|
+
].join('\n');
|
|
268
|
+
document.head.appendChild(this.dom.stylesheet);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Inject all server methods defined in `methods` as local functions wrapped in the `rpc` function
|
|
274
|
+
*/
|
|
275
|
+
injectMethods() {
|
|
276
|
+
this.methods.forEach(method =>
|
|
277
|
+
this[method] = this.rpc.bind(this, method)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
// }}}
|
|
281
|
+
|
|
282
|
+
// Client unique functions - bindProjectState(), toggleFullscreen() {{{
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Return a Vue reactive object that can be read/written which whose changes will transparently be written back to the TERA server instance
|
|
286
|
+
*
|
|
287
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
288
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
289
|
+
* @param {Boolean} [options.write=true] Allow local reactivity to writes - send these to the server
|
|
290
|
+
* @param {Array<String>} Paths to subscribe to e.g. ['/users/'],
|
|
291
|
+
*
|
|
292
|
+
* @returns {Promies<Reactive<Object>>} A reactive object representing the project state
|
|
293
|
+
*/
|
|
294
|
+
bindProjectState(options) {
|
|
295
|
+
let settings = {
|
|
296
|
+
autoRequire: true,
|
|
297
|
+
write: true,
|
|
298
|
+
...options,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return Promise.resolve()
|
|
302
|
+
.then(()=> settings.autoRequire && this.requireProject())
|
|
303
|
+
.then(()=> this.getProjectStateSnapshot({
|
|
304
|
+
autoRequire: false, // already handled this
|
|
305
|
+
paths: settings.paths,
|
|
306
|
+
}))
|
|
307
|
+
.then(snapshot => {
|
|
308
|
+
// Create initial reactive
|
|
309
|
+
let stateReactive = reactive(snapshot);
|
|
310
|
+
|
|
311
|
+
// Watch for remote changes and update
|
|
312
|
+
// FIXME: Not yet supported
|
|
313
|
+
|
|
314
|
+
// Watch for local writes and react
|
|
315
|
+
if (settings.write) {
|
|
316
|
+
watch(
|
|
317
|
+
stateReactive,
|
|
318
|
+
(newVal, oldVal) => {
|
|
319
|
+
let diff = diff(newVal, oldVal);
|
|
320
|
+
console.log('DEBUG APPLY DIFF', diff);
|
|
321
|
+
this.applyProjectStatePatch(diff);
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
deep: true,
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Return Vue Reactive
|
|
330
|
+
return stateReactive;
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Fit the nested TERA server to a full-screen context
|
|
337
|
+
* This is usually because the server component wants to perform some user activity like calling $prompt
|
|
338
|
+
* @param {String|Boolean} [isFullscreen='toggle'] Whether to fullscreen the embedded component
|
|
339
|
+
*/
|
|
340
|
+
toggleFullscreen(isFullscreen) {
|
|
341
|
+
globalThis.document.body.classList.toggle('tera-fy-fullscreen', isFullscreen === 'toggle' ? undefined : isFullscreen);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// }}}
|
|
345
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side functions available to the Tera-Fy client library
|
|
3
|
+
*
|
|
4
|
+
* @class TeraFyServer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* globals globalThis, app */
|
|
8
|
+
export default class TeraFyServer {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Various settings to configure behaviour
|
|
12
|
+
*
|
|
13
|
+
* @type {Object}
|
|
14
|
+
* @property {Boolean} devMode Operate in devMode - i.e. force outer refresh when encountering an existing TeraFy instance
|
|
15
|
+
* @property {String} restrictOrigin URL to restrict communications to
|
|
16
|
+
*/
|
|
17
|
+
settings = {
|
|
18
|
+
restrictOrigin: '*',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Messages - acceptMessage() {{{
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Send raw message content to the client
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} message Message object to send
|
|
27
|
+
*/
|
|
28
|
+
sendRaw(message) {
|
|
29
|
+
globalThis.parent.postMessage(
|
|
30
|
+
{
|
|
31
|
+
TERA: 1,
|
|
32
|
+
...message,
|
|
33
|
+
},
|
|
34
|
+
this.settings.restrictOrigin
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Accept a message from the parent event listener
|
|
41
|
+
*
|
|
42
|
+
* @param {MessageEvent} Raw message event to process
|
|
43
|
+
*/
|
|
44
|
+
acceptMessage(rawMessage) {
|
|
45
|
+
let message = rawMessage.data;
|
|
46
|
+
if (!message.TERA) return; // Ignore non-TERA signed messages
|
|
47
|
+
console.log('TERA-FY Server message', {message});
|
|
48
|
+
|
|
49
|
+
Promise.resolve()
|
|
50
|
+
.then(()=> {
|
|
51
|
+
if (message.action == 'rpc') { // Relay RPC calls
|
|
52
|
+
if (!this[message.method]) throw new Error(`Unknown RPC method "${message.method}"`);
|
|
53
|
+
return this[message.method].call(this, message.args);
|
|
54
|
+
} else {
|
|
55
|
+
console.log('Unexpected incoming TERA-FY SERVER message', {message});
|
|
56
|
+
throw new Error('Unknown message format');
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.then(res => this.sendRaw({
|
|
60
|
+
id: message.id,
|
|
61
|
+
action: 'response',
|
|
62
|
+
response: res,
|
|
63
|
+
}))
|
|
64
|
+
.catch(e => {
|
|
65
|
+
console.warn(`TERA-FY server threw on RPC:${message.method}:`, e);
|
|
66
|
+
this.sendRaw({
|
|
67
|
+
id: message.id,
|
|
68
|
+
action: 'response',
|
|
69
|
+
isError: true,
|
|
70
|
+
response: e.toString(),
|
|
71
|
+
});
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
// }}}
|
|
75
|
+
// Basics - handshake() {{{
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Return basic server information as a form of validation
|
|
79
|
+
*
|
|
80
|
+
* @returns {Promise<Object>} Basic promise result
|
|
81
|
+
* @property {Date} date Server date
|
|
82
|
+
*/
|
|
83
|
+
handshake() {
|
|
84
|
+
return {
|
|
85
|
+
date: new Date(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// }}}
|
|
89
|
+
|
|
90
|
+
// Session / user - getUser() {{{
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* User / active session within TERA
|
|
94
|
+
* @class User
|
|
95
|
+
* @property {String} id Unique identifier of the user
|
|
96
|
+
* @property {String} email The email address of the current user
|
|
97
|
+
* @property {String} name The provided full name of the user
|
|
98
|
+
* @property {Boolean} isSubscribed Whether the active user has a TERA subscription
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fetch the current session user
|
|
103
|
+
*
|
|
104
|
+
* @returns {Promise<User>} The current logged in user or null if none
|
|
105
|
+
*/
|
|
106
|
+
getUser() {
|
|
107
|
+
let $auth = app.service('$auth');
|
|
108
|
+
let $subscriptions = app.service('$subscriptions');
|
|
109
|
+
|
|
110
|
+
return Promise.all([
|
|
111
|
+
$auth.promise(),
|
|
112
|
+
$subscriptions.promise(),
|
|
113
|
+
])
|
|
114
|
+
.then(()=> ({
|
|
115
|
+
id: $auth.user.id,
|
|
116
|
+
email: $auth.user.email,
|
|
117
|
+
name: [
|
|
118
|
+
$auth.user.given_name,
|
|
119
|
+
$auth.user.family_name,
|
|
120
|
+
].filter(Boolean).join(' '),
|
|
121
|
+
isSubscribed: $subscriptions.isSubscribed,
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// }}}
|
|
126
|
+
|
|
127
|
+
// Projects - getProject(), getProjects(), requireProject(), selectProject() {{{
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Project entry within TERA
|
|
131
|
+
* @class Project
|
|
132
|
+
* @property {String} id The Unique ID of the project
|
|
133
|
+
* @property {String} name The name of the project
|
|
134
|
+
* @property {String} created The creation date of the project as an ISO string
|
|
135
|
+
* @property {Boolean} isOwner Whether the current session user is the owner of the project
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the currently active project, if any
|
|
141
|
+
*
|
|
142
|
+
* @returns {Promise<Project|null>} The currently active project, if any
|
|
143
|
+
*/
|
|
144
|
+
getProject() {
|
|
145
|
+
let $projects = app.service('$projects');
|
|
146
|
+
|
|
147
|
+
return $projects.promise()
|
|
148
|
+
.then(()=> $projects.active
|
|
149
|
+
? {
|
|
150
|
+
id: $projects.active.id,
|
|
151
|
+
name: $projects.active.name,
|
|
152
|
+
created: $projects.active.created,
|
|
153
|
+
isOwner: $projects.active.$isOwner,
|
|
154
|
+
}
|
|
155
|
+
: null
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get a list of projects the current session user has access to
|
|
162
|
+
*
|
|
163
|
+
* @returns {Promise<Array<Project>>} Collection of projects the user has access to
|
|
164
|
+
*/
|
|
165
|
+
getProjects() {
|
|
166
|
+
let $projects = app.service('$projects');
|
|
167
|
+
|
|
168
|
+
return $projects.promise()
|
|
169
|
+
.then(()=> $projects.list.map(project => ({
|
|
170
|
+
id: project.id,
|
|
171
|
+
name: project.name,
|
|
172
|
+
created: project.created,
|
|
173
|
+
isOwner: project.$isOwner,
|
|
174
|
+
})))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Ask the user to select a project from those available - if one isn't already active
|
|
180
|
+
* Note that this function will percist in asking the uesr even if they try to cancel
|
|
181
|
+
*
|
|
182
|
+
* @returns {Promise<Project>} The active project
|
|
183
|
+
*/
|
|
184
|
+
requireProject() {
|
|
185
|
+
let $prompt = app.service('$prompt');
|
|
186
|
+
return this.getProject()
|
|
187
|
+
.then(active => {
|
|
188
|
+
if (active) return active; // Use active project
|
|
189
|
+
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
let askProject = ()=> Promise.resolve()
|
|
192
|
+
.then(()=> this.selectProject({
|
|
193
|
+
allowCancel: false,
|
|
194
|
+
}))
|
|
195
|
+
.then(project => resolve(project))
|
|
196
|
+
.catch(e => {
|
|
197
|
+
if (e == 'cancel') {
|
|
198
|
+
return $prompt.dialog({
|
|
199
|
+
title: 'Select project',
|
|
200
|
+
body: 'A project needs to be selected to continue',
|
|
201
|
+
buttons: ['ok'],
|
|
202
|
+
})
|
|
203
|
+
.then(()=> askProject())
|
|
204
|
+
.catch(reject)
|
|
205
|
+
} else {
|
|
206
|
+
reject(e);
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Prompt the user to select a project from those available
|
|
216
|
+
*
|
|
217
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
218
|
+
* @param {String} [options.title="Select a project to work with"] The title of the dialog to display
|
|
219
|
+
* @param {Boolean} [options.allowCancel=true] Advertise cancelling the operation, the dialog can still be cancelled by closing it
|
|
220
|
+
*
|
|
221
|
+
* @returns {Promise<Project>} The active project
|
|
222
|
+
*/
|
|
223
|
+
selectProject(options) {
|
|
224
|
+
let settings = {
|
|
225
|
+
title: 'Select a project to work with',
|
|
226
|
+
allowCancel: true,
|
|
227
|
+
...options,
|
|
228
|
+
};
|
|
229
|
+
let $projects = app.service('$projects');
|
|
230
|
+
let $prompt = app.service('$prompt');
|
|
231
|
+
return $projects.promise()
|
|
232
|
+
.then(()=> $prompt.dialog({
|
|
233
|
+
title: 'Select project',
|
|
234
|
+
component: 'projectsSelect',
|
|
235
|
+
dialogClose: 'reject',
|
|
236
|
+
buttons: settings.allowCancel && ['cancel'],
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
// }}}
|
|
242
|
+
|
|
243
|
+
// Project State {{{
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Return the current, full snapshot state of the active project
|
|
247
|
+
*
|
|
248
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
249
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
250
|
+
* @param {Array<String>} Paths to subscribe to e.g. ['/users/'],
|
|
251
|
+
*
|
|
252
|
+
* @returns {Promise<Object>} The current project state snapshot
|
|
253
|
+
*/
|
|
254
|
+
getProjectStateSnapshot(options) {
|
|
255
|
+
let settings = {
|
|
256
|
+
autoRequire: true,
|
|
257
|
+
paths: null,
|
|
258
|
+
...options,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return Promise.resolve()
|
|
262
|
+
.then(()=> settings.autoRequire && this.requireProject())
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Apply a computed `just-diff` patch to the current project state
|
|
268
|
+
*/
|
|
269
|
+
applyProjectStatePatch(patch) {
|
|
270
|
+
console.log('Applying sever state patch', {patch});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// }}}
|
|
274
|
+
|
|
275
|
+
// Project Libraries {{{
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Fetch the active projects citation library
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
281
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
282
|
+
* @param {Boolean} [options.multiple=false] Allow selection of multiple libraries
|
|
283
|
+
* @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'
|
|
284
|
+
*
|
|
285
|
+
* @returns {Promise<Array<RefLibRef>>} Collection of references for the selected library
|
|
286
|
+
*/
|
|
287
|
+
getProjectLibrary(options) {
|
|
288
|
+
let settings = {
|
|
289
|
+
autoRequire: true,
|
|
290
|
+
multiple: false,
|
|
291
|
+
hint: null,
|
|
292
|
+
...options,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return Promise.resolve()
|
|
296
|
+
.then(()=> settings.autoRequire && this.requireProject())
|
|
297
|
+
// FIXME: Stub
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Save back a projects citation library
|
|
303
|
+
*
|
|
304
|
+
* @param {Array<RefLibRef>} Collection of references for the selected library
|
|
305
|
+
*
|
|
306
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
307
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
308
|
+
* @param {String} [options.hint] Hint to store against the library. Generally corresponds to the current operation being performed - e.g. 'deduped'
|
|
309
|
+
*
|
|
310
|
+
* @returns {Promise} A promise which resolves when the save operation has completed
|
|
311
|
+
*/
|
|
312
|
+
setProjectLibrary(refs, options) {
|
|
313
|
+
let settings = {
|
|
314
|
+
autoRequire: true,
|
|
315
|
+
hint: null,
|
|
316
|
+
...options,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return Promise.resolve()
|
|
320
|
+
.then(()=> settings.autoRequire && this.requireProject())
|
|
321
|
+
// FIXME: Stub
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// }}}
|
|
325
|
+
|
|
326
|
+
// Init - constructor(), init() {{{
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Setup the TERA-fy client singleton
|
|
330
|
+
*
|
|
331
|
+
* @param {Object} [options] Additional options to merge into `settings`
|
|
332
|
+
*/
|
|
333
|
+
constructor(options) {
|
|
334
|
+
Object.assign(this.settings, options);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Set or toggle devMode
|
|
340
|
+
*
|
|
341
|
+
* @param {String|Boolean} [devModeEnabled='toggle'] Optional boolean to force dev mode
|
|
342
|
+
*
|
|
343
|
+
* @returns {TeraFy} This chainable terafy instance
|
|
344
|
+
*/
|
|
345
|
+
toggleDevMode(devModeEnabled = 'toggle') {
|
|
346
|
+
this.settings.devMode = devModeEnabled === 'toggle'
|
|
347
|
+
? !this.settings.devMode
|
|
348
|
+
: devModeEnabled;
|
|
349
|
+
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Initialize the browser listener
|
|
356
|
+
*/
|
|
357
|
+
init() {
|
|
358
|
+
console.log('TERA server init');
|
|
359
|
+
globalThis.addEventListener('message', this.acceptMessage.bind(this));
|
|
360
|
+
}
|
|
361
|
+
// }}}
|
|
362
|
+
}
|