1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15/**
16 * The base RapidContext namespace.
17 * @namespace RapidContext
18 * @private
19 */
20if (typeof(RapidContext) == "undefined") {
21 RapidContext = {};
22}
23
24/**
25 * Provides functions for application bootstrap and server communication.
26 * @namespace RapidContext.App
27 */
28if (typeof(RapidContext.App) == "undefined") {
29 RapidContext.App = {};
30}
31
32/**
33 * Initializes the platform, API:s and RapidContext UI. If an app
34 * identifier is provided, the default platform UI will not be
35 * created. Instead the app will be launched with the root document
36 * as its UI container.
37 *
38 * @param {string} [app] the app id to start
39 *
40 * @return {Promise} a promise that will resolve when initialization has
41 * either completed or failed
42 */
43RapidContext.App.init = function (app) {
44 // Initialize UI
45 RapidContext.Log.context("RapidContext.App.init()");
46 console.info("Initializing RapidContext");
47 document.body.innerHTML = "";
48 document.body.append(new RapidContext.Widget.Overlay({ message: "Loading..." }));
49
50 // Load platform data (into cache)
51 var cachedData = [
52 RapidContext.App.callProc("system/status"),
53 RapidContext.App.callProc("system/session/current"),
54 RapidContext.App.callProc("system/app/list")
55 ];
56
57 // Launch app
58 return Promise.all(cachedData)
59 .then(function () {
60 const hourly = 60 * 60 * 1000;
61 setInterval(() => RapidContext.App.callProc("system/session/current"), hourly);
62 if (app && app !== "start") {
63 return RapidContext.App.startApp(app || "start").catch(function (err) {
64 RapidContext.UI.showError(err);
65 return RapidContext.App.startApp("start");
66 });
67 } else {
68 return RapidContext.App.startApp("start");
69 }
70 })
71 .catch(function (err) {
72 RapidContext.UI.showError(err);
73 return Promise.reject(err);
74 })
75 .finally(function () {
76 RapidContext.Log.context(null);
77 });
78};
79
80/**
81 * Returns an object with status information about the platform and
82 * currently loaded environment. The object returned is a copy of
83 * the internal data structure and can be modified without affecting
84 * the real data.
85 *
86 * @return {Object} the status data object
87 */
88RapidContext.App.status = function () {
89 return Object.assign({}, RapidContext.App._Cache.status);
90};
91
92/**
93 * Returns an object with information about the user. The object
94 * returned is a copy of the internal data structure and can be
95 * modified without affecting the real data.
96 *
97 * @return {Object} the user data object
98 */
99RapidContext.App.user = function () {
100 return Object.assign({}, RapidContext.App._Cache.user);
101};
102
103/**
104 * Returns an array with app launchers. The data returned is an
105 * internal data structure and should not be modified.
106 *
107 * @return {Array} the loaded app launchers (read-only)
108 */
109RapidContext.App.apps = function () {
110 return Object.values(RapidContext.App._Cache.apps);
111};
112
113/**
114 * Returns an array with running app instances.
115 *
116 * @return {Array} the array of app instances
117 */
118RapidContext.App._instances = function () {
119 var res = [];
120 var apps = RapidContext.App.apps();
121 for (var i = 0; i < apps.length; i++) {
122 res = res.concat(apps[i].instances || []);
123 }
124 return res;
125};
126
127/**
128 * Finds the app launcher from an app instance, class name or
129 * launcher. In the last case, the matching cached launcher will be
130 * returned.
131 *
132 * @param {string|Object} app the app id, instance, class name or
133 * launcher
134 *
135 * @return {Object} the read-only app launcher, or
136 * `null` if not found
137 */
138RapidContext.App.findApp = function (app) {
139 if (app == null) {
140 return null;
141 }
142 var apps = RapidContext.App.apps();
143 for (var i = 0; i < apps.length; i++) {
144 var l = apps[i];
145 if (l.id == app || l.className == app || l.id == app.id) {
146 return l;
147 }
148 }
149 return null;
150};
151
152/**
153 * Creates and starts an app instance. Normally apps are started in the default
154 * app tab container or in a provided widget DOM node. By specifying a newly
155 * created window object as the parent container, apps can also be launched
156 * into separate windows.
157 *
158 * Note that when a new window is used, the returned deferred will callback
159 * immediately with a `null` app instance (normal app communication is not
160 * possible cross-window).
161 *
162 * @param {string|Object} app the app id, class name or launcher
163 * @param {Widget/Window} [container] the app container widget or
164 * window, defaults to create a new pane in the app tab
165 * container
166 *
167 * @return {Promise} a `RapidContext.Async` promise that will
168 * resolve when the app has launched
169 *
170 * @example
171 * // Starts the help app in a new tab
172 * RapidContext.App.startApp('help');
173 *
174 * // Starts the help app in a new window
175 * RapidContext.App.startApp('help', window.open());
176 */
177RapidContext.App.startApp = function (app, container) {
178 function loadResource(launcher, res) {
179 if (res.type == "code") {
180 return RapidContext.App.loadScript(res.url);
181 } else if (res.type == "module") {
182 return import(new URL(res.url, document.baseURI)).then(function (mod) {
183 launcher.creator = launcher.creator || mod["default"] || mod["create"];
184 });
185 } else if (res.type == "style") {
186 return RapidContext.App.loadStyles(res.url);
187 } else if (res.type == "ui") {
188 return RapidContext.App.loadXML(res.url).then(function (node) {
189 launcher.ui = node;
190 });
191 } else if (res.type == "json" && res.id != null) {
192 return RapidContext.App.loadJSON(res.url).then(function (data) {
193 launcher.resource[res.id] = data;
194 });
195 } else {
196 if (res.id != null) {
197 launcher.resource[res.id] = res.url;
198 }
199 return Promise.resolve(res);
200 }
201 }
202 function load(launcher) {
203 console.info("Loading app/" + launcher.id + " resources", launcher);
204 launcher.resource = {};
205 var promises = launcher.resources.map((res) => loadResource(launcher, res));
206 return Promise.all(promises).then(function () {
207 launcher.creator = launcher.creator || window[launcher.className];
208 if (launcher.creator == null) {
209 console.error("App constructor " + launcher.className + " not defined", launcher);
210 throw new Error("App constructor " + launcher.className + " not defined");
211 }
212 });
213 }
214 function buildUI(parent, ids, ui) {
215 var root = ui.documentElement;
216 for (var i = 0; i < root.attributes.length; i++) {
217 var attr = root.attributes[i];
218 if (typeof(parent.setAttrs) === "function") {
219 parent.setAttrs({ [attr.name]: attr.value });
220 } else if (attr.name === "class") {
221 attr.value.split(/\s+/g).forEach(function (cls) {
222 parent.classList.add(cls);
223 });
224 } else {
225 parent.setAttribute(attr.name, attr.value);
226 }
227 }
228 let arr = Array.from(ui.documentElement.childNodes);
229 parent.append(...arr.map((o) => RapidContext.UI.create(o)).filter(Boolean));
230 parent.querySelectorAll("[id]").forEach((el) => ids[el.attributes.id.value] = el);
231 }
232 function launch(launcher, ui) {
233 RapidContext.Log.context("RapidContext.App.startApp(" + launcher.id + ")");
234 return RapidContext.Async.wait(0)
235 .then(() => launcher.creator ? true : load(launcher))
236 .then(function () {
237 console.info("Starting app/" + launcher.id, launcher);
238 /* eslint new-cap: "off" */
239 var instance = new launcher.creator();
240 launcher.instances.push(instance);
241 var props = MochiKit.Base.setdefault({ ui: ui }, launcher);
242 delete props.creator;
243 delete props.instances;
244 MochiKit.Base.setdefault(instance, props);
245 MochiKit.Signal.disconnectAll(ui.root, "onclose");
246 var halt = () => RapidContext.App.stopApp(instance);
247 MochiKit.Signal.connect(ui.root, "onclose", halt);
248 if (launcher.ui != null) {
249 buildUI(ui.root, ui, launcher.ui);
250 }
251 ui.overlay.hide();
252 ui.overlay.setAttrs({ message: "Working..." });
253 return RapidContext.App.callApp(instance, "start");
254 })
255 .catch(function (e) {
256 console.error("Failed to start app", e);
257 MochiKit.Signal.disconnectAll(ui.root, "onclose");
258 let div = document.createElement("div");
259 div.append(String(e));
260 ui.root.append(div);
261 if (e.url) {
262 let link = document.createElement("a");
263 link.className = "block mt-2";
264 link.setAttribute("href", e.url);
265 link.append(e.url);
266 ui.root.append(link);
267 }
268 ui.overlay.hide();
269 return Promise.reject(e);
270 })
271 .finally(function () {
272 RapidContext.Log.context(null);
273 });
274 }
275 function moveAppToStart(instance, elems) {
276 var start = RapidContext.App.findApp("start").instances[0];
277 var opts = { title: instance.name, closeable: false, background: true };
278 var ui = start.initAppPane(null, opts);
279 ui.root.removeAll();
280 ui.root.addAll(elems);
281 }
282 return new RapidContext.Async(function (resolve, reject) {
283 var launcher = RapidContext.App.findApp(app);
284 if (launcher == null) {
285 var msg = "No matching app launcher found";
286 console.error(msg, app);
287 throw new Error([msg, ": ", app].join(""));
288 }
289 var instances = RapidContext.App._instances();
290 var start = RapidContext.App.findApp("start");
291 if ($.isWindow(container)) {
292 // Launch app into separate window/tab
293 var href = "rapidcontext/app/" + launcher.id;
294 container.location.href = new URL(href, document.baseURI).toString();
295 resolve();
296 } else if (start && start.instances && start.instances.length > 0) {
297 // Launch app into start app tab
298 var paneOpts = { title: launcher.name, closeable: (launcher.launch != "once") };
299 var paneUi = start.instances[0].initAppPane(container, paneOpts);
300 resolve(launch(launcher, paneUi));
301 } else if (instances.length > 0) {
302 // Switch from single-app to multi-app mode
303 var elems = Array.from(document.body.childNodes);
304 var overlay = new RapidContext.Widget.Overlay({ message: "Loading..." });
305 document.body.insertBefore(overlay, document.body.childNodes[0]);
306 let ui = { root: document.body, overlay: overlay };
307 var move = () => moveAppToStart(instances[0], elems);
308 var recall = () => (launcher.id === "start") ? true : RapidContext.App.startApp(launcher.id);
309 resolve(launch(start, ui).then(move).then(recall));
310 } else {
311 // Launch single-app mode
312 var ui = { root: document.body, overlay: document.body.childNodes[0] };
313 resolve(launch(launcher, ui));
314 }
315 });
316};
317
318/**
319 * Stops an app instance. If only the class name or launcher is specified, the
320 * most recently created instance will be stopped.
321 *
322 * @param {string|Object} app the app id, instance, class name or launcher
323 *
324 * @return {Promise} a `RapidContext.Async` promise that will
325 * resolve when the app has been stopped
326 */
327RapidContext.App.stopApp = function (app) {
328 return new RapidContext.Async(function (resolve, reject) {
329 var launcher = RapidContext.App.findApp(app);
330 if (!launcher || launcher.instances.length <= 0) {
331 var msg = "No running app instance found";
332 console.error(msg, app);
333 throw new Error([msg, ": ", app].join(""));
334 }
335 console.info("Stopping app " + launcher.name);
336 var pos = launcher.instances.indexOf(app);
337 if (pos < 0) {
338 app = launcher.instances.pop();
339 } else {
340 launcher.instances.splice(pos, 1);
341 }
342 app.stop();
343 if (app.ui.root != null) {
344 RapidContext.Widget.destroyWidget(app.ui.root);
345 }
346 for (var k in app.ui) {
347 delete app.ui[k];
348 }
349 for (var n in app) {
350 delete app[n];
351 }
352 MochiKit.Signal.disconnectAllTo(app);
353 resolve();
354 });
355};
356
357/**
358 * Performs an asynchronous call to a method in an app. If only the class name
359 * or launcher is specified, the most recently created instance will be used.
360 * If no instance is running, one will be started. Also, before calling the app
361 * method, the app UI will be focused.
362 *
363 * @param {string|Object} app the app id, instance, class name or launcher
364 * @param {string} method the app method name
365 * @param {Mixed} [args] additional parameters sent to method
366 *
367 * @return {Promise} a `RapidContext.Async` promise that will
368 * resolve with the result of the call on success
369 */
370RapidContext.App.callApp = function (app, method) {
371 var args = Array.from(arguments).slice(2);
372 return new RapidContext.Async(function (resolve, reject) {
373 var launcher = RapidContext.App.findApp(app);
374 if (launcher == null) {
375 var msg = "No matching app launcher found";
376 console.error(msg, app);
377 throw new Error([msg, ": ", app].join(""));
378 }
379 var exists = launcher.instances.length > 0;
380 var promise = exists ? RapidContext.Async.wait(0) : RapidContext.App.startApp(app);
381 promise = promise.then(function () {
382 RapidContext.Log.context("RapidContext.App.callApp(" + launcher.id + "," + method + ")");
383 var pos = launcher.instances.indexOf(app);
384 var instance = (pos >= 0) ? app : launcher.instances[launcher.instances.length - 1];
385 var child = instance.ui.root;
386 var parent = child.parentNode.closest(".widget");
387 if (parent != null && typeof(parent.selectChild) == "function") {
388 parent.selectChild(child);
389 }
390 var methodName = launcher.className + "." + method;
391 if (instance[method] == null) {
392 var msg = "No app method " + methodName + " found";
393 console.error(msg);
394 throw new Error(msg);
395 }
396 console.log("Calling app method " + methodName, args);
397 try {
398 return instance[method].apply(instance, args);
399 } catch (e) {
400 var reason = "Caught error in " + methodName;
401 console.error(reason, e);
402 throw new Error(reason + ": " + e.toString(), { cause: e });
403 }
404 }).finally(function () {
405 RapidContext.Log.context(null);
406 });
407 resolve(promise);
408 });
409};
410
411/**
412 * Performs an asynchronous procedure call.
413 *
414 * @param {string} name the procedure name
415 * @param {Array|Object} [args] the arguments array, dictionary, or `null`
416 * @param {Object} [opts] the procedure call options
417 * @param {boolean} [opts.session] the HTTP session required flag
418 * @param {boolean} [opts.trace] the procedure call trace flag
419 * @param {number} [opts.timeout] the timeout in milliseconds, default is 60s
420 *
421 * @return {Promise} a `RapidContext.Async` promise that will
422 * resolve with the response data on success
423 */
424RapidContext.App.callProc = function (name, args, opts) {
425 args = args || [];
426 opts = opts || {};
427 console.log("Call request " + name, args);
428 let params = RapidContext.Data.map(RapidContext.Encode.toJSON, args);
429 if (Array.isArray(params)) {
430 params = RapidContext.Data.object(params.map((val, idx) => ["arg" + idx, val]));
431 }
432 params["system:session"] = !!opts.session;
433 params["system:trace"] = !!opts.trace || ["all", "log"].includes(RapidContext.Log.level());
434 let url = "rapidcontext/procedure/" + name;
435 let options = { method: "POST", timeout: opts.timeout || 60000 };
436 return RapidContext.App.loadJSON(url, params, options).then(function (res) {
437 if (res.trace) {
438 console.log(name + " trace:", res.trace);
439 }
440 if (res.error) {
441 console.info(name + " error:", res.error);
442 RapidContext.App._Cache.handleError(res.error);
443 throw new Error(res.error);
444 } else {
445 console.log(name + " response:", res.data);
446 if (name.startsWith("system/")) {
447 RapidContext.App._Cache.update(name, res.data);
448 }
449 return res.data;
450 }
451 });
452};
453
454/**
455 * Performs an asynchronous login. If the current session is already bound to a
456 * user, that session will be terminated and a new one will be created. If an
457 * authentication token is specified, the login and password fields are not
458 * used (can be null).
459 *
460 * @param {string} login the user login name or email address
461 * @param {string} password the password to authenticate the user
462 * @param {string} [token] the authentication token to identify user/password
463 *
464 * @return {Promise} a `RapidContext.Async` promise that resolves when
465 * the authentication has either succeeded or failed
466 */
467RapidContext.App.login = function (login, password, token) {
468 function searchLogin() {
469 var proc = "system/user/search";
470 return RapidContext.App.callProc(proc, [login]).then(function (user) {
471 if (user && user.id) {
472 return login = user.id;
473 } else {
474 throw new Error("no user with that email address");
475 }
476 });
477 }
478 function getNonce() {
479 let proc = "system/session/current";
480 let opts = { session: true };
481 return RapidContext.App.callProc(proc, [], opts).then(function (session) {
482 return session.nonce;
483 });
484 }
485 function passwordAuth(nonce) {
486 var realm = RapidContext.App.status().realm;
487 var hash = CryptoJS.MD5(login + ":" + realm + ":" + password);
488 hash = CryptoJS.MD5(hash.toString() + ":" + nonce).toString();
489 var args = [login, nonce, hash];
490 return RapidContext.App.callProc("system/session/authenticate", args);
491 }
492 function tokenAuth() {
493 return RapidContext.App.callProc("system/session/authenticateToken", [token]);
494 }
495 function verifyAuth(res) {
496 if (!res.success || res.error) {
497 console.info("login failed", login, res.error);
498 throw new Error(res.error || "authentication failed");
499 }
500 }
501 var promise = RapidContext.Async.wait(0);
502 var user = RapidContext.App.user();
503 if (user && user.id) {
504 promise = promise.then(RapidContext.App.logout);
505 }
506 if (token) {
507 return promise.then(tokenAuth).then(verifyAuth);
508 } else {
509 if (/@/.test(login)) {
510 promise = promise.then(searchLogin);
511 }
512 return promise.then(getNonce).then(passwordAuth).then(verifyAuth);
513 }
514};
515
516/**
517 * Performs an asyncronous logout. This function terminates the current
518 * session and either reloads the browser window or returns a deferred object
519 * that will produce either a `callback` or an `errback` response.
520 *
521 * @param {boolean} [reload=true] the reload browser flag
522 *
523 * @return {Promise} a `RapidContext.Async` promise that will
524 * resolve when user is logged out
525 */
526RapidContext.App.logout = function (reload) {
527 var promise = RapidContext.App.callProc("system/session/terminate", [null]);
528 if (reload !== false) {
529 promise.then(() => window.location.reload());
530 }
531 return promise;
532};
533
534/**
535 * Performs an asynchronous HTTP request and parses the JSON response. The
536 * request parameters are automatically encoded to query string or JSON format,
537 * depending on the `Content-Type` header. The parameters will be sent either
538 * in the URL or as the request payload (depending on the HTTP `method`).
539 *
540 * @param {string} url the URL to request
541 * @param {Object} [params] the request parameters, or `null`
542 * @param {Object} [opts] the request options, or `null`
543 * @param {string} [opts.method] the HTTP method, default is `GET`
544 * @param {number} [opts.timeout] the timeout in milliseconds, default is 30s
545 * @param {Object} [opts.headers] the specific HTTP headers to use
546 *
547 * @return {Promise} a `RapidContext.Async` promise that will
548 * resolve with the parsed response JSON on success
549 */
550RapidContext.App.loadJSON = function (url, params, opts) {
551 opts = { responseType: "json", ...opts };
552 return RapidContext.App.loadXHR(url, params, opts).then((xhr) => xhr.response);
553};
554
555/**
556 * Performs an asynchronous HTTP request for a text document. The request
557 * parameters are automatically encoded to query string or JSON format,
558 * depending on the `Content-Type` header. The parameters will be sent either
559 * in the URL or as the request payload (depending on the HTTP `method`).
560 *
561 * @param {string} url the URL to request
562 * @param {Object} [params] the request parameters, or `null`
563 * @param {Object} [opts] the request options, or `null`
564 * @param {string} [opts.method] the HTTP method, "GET" or "POST"
565 * @param {number} [opts.timeout] the timeout in milliseconds, default is 30s
566 * @param {Object} [opts.headers] the specific HTTP headers to use
567 *
568 * @return {Promise} a `RapidContext.Async` promise that will
569 * resolve with the response text on success
570 */
571RapidContext.App.loadText = function (url, params, opts) {
572 opts = { responseType: "text", ...opts };
573 return RapidContext.App.loadXHR(url, params, opts).then((xhr) => xhr.response);
574};
575
576/**
577 * Performs an asynchronous HTTP request for an XML document. The request
578 * parameters are automatically encoded to query string or JSON format,
579 * depending on the `Content-Type` header. The parameters will be sent either
580 * in the URL or as the request payload (depending on the HTTP `method`).
581 *
582 * @param {string} url the URL to request
583 * @param {Object} [params] the request parameters, or `null`
584 * @param {Object} [opts] the request options, or `null`
585 * @param {string} [opts.method] the HTTP method, "GET" or "POST"
586 * @param {number} [opts.timeout] the timeout in milliseconds, default is 30s
587 * @param {Object} [opts.headers] the specific HTTP headers to use
588 *
589 * @return {Promise} a `RapidContext.Async` promise that will
590 * resolve with the parsed response XML document on success
591 */
592RapidContext.App.loadXML = function (url, params, opts) {
593 opts = { responseType: "document", ...opts };
594 return RapidContext.App.loadXHR(url, params, opts).then((xhr) => xhr.response);
595};
596
597/**
598 * Performs an asynchronous HTTP request. The request parameters are
599 * automatically encoded to query string or JSON format, depending on the
600 * `Content-Type` header. The parameters will be sent either in the URL or as
601 * the request payload (depending on the HTTP `method`).
602 *
603 * @param {string} url the URL to request
604 * @param {Object} [params] the request parameters, or `null`
605 * @param {Object} [opts] the request options, or `null`
606 * @param {string} [opts.method] the HTTP method, default is `GET`
607 * @param {number} [opts.timeout] the timeout in milliseconds, default is 30s
608 * @param {Object} [opts.headers] the specific HTTP headers to use
609 *
610 * @return {Promise} a `RapidContext.Async` promise that will
611 * resolve with the XMLHttpRequest instance on success
612 */
613RapidContext.App.loadXHR = function (url, params, opts) {
614 opts = { method: "GET", headers: {}, timeout: 30000, ...opts };
615 opts.timeout = (opts.timeout < 1000) ? opts.timeout * 1000 : opts.timeout;
616 var hasBody = params && ["PATCH", "POST", "PUT"].includes(opts.method);
617 var hasJsonBody = opts.headers["Content-Type"] === "application/json";
618 if (!hasBody) {
619 url += params ? "?" + RapidContext.Encode.toUrlQuery(params) : "";
620 } else if (params && hasBody && hasJsonBody) {
621 opts.body = RapidContext.Encode.toJSON(params);
622 } else if (params && hasBody) {
623 opts.headers["Content-Type"] = "application/x-www-form-urlencoded";
624 opts.body = RapidContext.Encode.toUrlQuery(params);
625 }
626 console.log("Starting XHR loading", url, opts);
627 return RapidContext.Async.xhr(url, opts).then(
628 function (res) {
629 console.log("Completed XHR loading", url);
630 return res;
631 },
632 function (err) {
633 let logger = /timeout/i.test(err) ? console.info : console.warn;
634 logger("Failed XHR loading", url, err);
635 return Promise.reject(err);
636 }
637 );
638};
639
640/**
641 * Loads a JavaScript to the the current page. The script is loaded by
642 * inserting a `<script>` tag in the document `<head>`. Function definitions
643 * and values must therefore be stored to global variables by the script to
644 * become accessible after loading. If the script is already loaded, the
645 * promise will resolve immediately.
646 *
647 * @param {string} url the URL to the script
648 *
649 * @return {Promise} a `RapidContext.Async` promise that will
650 * resolve when the script has loaded
651 *
652 * @see Use dynamic `import()` instead, if supported by the environment.
653 */
654RapidContext.App.loadScript = function (url) {
655 var selector = ["script[src*='", url, "']"].join("");
656 if (document.querySelectorAll(selector).length > 0) {
657 console.log("script already loaded, skipping", url);
658 return RapidContext.Async.wait(0);
659 } else {
660 console.log("loading script", url);
661 return RapidContext.Async.script(url);
662 }
663};
664
665/**
666 * Loads a CSS stylesheet to the the current page asynchronously. The
667 * stylesheet is loaded by inserting a `<link>` tag in the document `head`.
668 * If the stylesheet is already loaded, the promise will resolve immediately.
669 *
670 * @param {string} url the URL to the stylesheet
671 *
672 * @return {Promise} a `RapidContext.Async` promise that will
673 * callback when the stylesheet has been loaded
674 */
675RapidContext.App.loadStyles = function (url) {
676 var selector = ["link[href*='", url, "']"].join("");
677 if (document.querySelectorAll(selector).length > 0) {
678 console.log("stylesheet already loaded, skipping", url);
679 return RapidContext.Async.wait(0);
680 } else {
681 console.log("loading stylesheet", url);
682 return RapidContext.Async.css(url);
683 }
684};
685
686/**
687 * Downloads a file to the user desktop. This works by creating a new
688 * window or an inner frame which downloads the file from the server.
689 * Due to `Content-Disposition` headers being set on the server, the
690 * web browser will popup a dialog for the user to save the file.
691 * This function can also be used for saving a file that doesn't
692 * exist by first posting the file (text) content to the server.
693 *
694 * @param {string} url the URL or filename to download
695 * @param {string} [data] the optional file data (if not available
696 * on the server-side)
697 */
698RapidContext.App.downloadFile = function (url, data) {
699 if (data == null) {
700 url = url + (url.includes("?") ? "&" : "?") + "download";
701 let attrs = {
702 src: url,
703 border: "0",
704 frameborder: "0",
705 height: "0",
706 width: "0"
707 };
708 let iframe = RapidContext.UI.create("iframe", attrs);
709 document.body.append(iframe);
710 } else {
711 let name = RapidContext.UI.INPUT({ name: "fileName", value: url });
712 let file = RapidContext.UI.INPUT({ name: "fileData", value: data });
713 let flag = RapidContext.UI.INPUT({ name: "download", value: "1" });
714 let attrs = {
715 action: "rapidcontext/download",
716 method: "POST",
717 target: "_blank",
718 style: { display: "none" }
719 };
720 let form = RapidContext.UI.FORM(attrs, name, file, flag);
721 document.body.append(form);
722 form.submit();
723 }
724};
725
726/**
727 * Uploads a file to the server. The upload is handled by an
728 * asynchronous HTTP request.
729 *
730 * @param {string} id the upload identifier to use
731 * @param {File} file the file object to upload
732 * @param {function} [onProgress] the progress event handler
733 */
734RapidContext.App.uploadFile = function (id, file, onProgress) {
735 var formData = new FormData();
736 formData.append("file", file);
737 var opts = {
738 method: "POST",
739 body: formData,
740 timeout: 300000,
741 progress: onProgress,
742 log: id + " upload"
743 };
744 return RapidContext.Async.xhr("rapidcontext/upload/" + id, opts);
745};
746
747/**
748 * The application data cache. Contains the most recently retrieved
749 * data for some commonly used objects in the execution environment.
750 */
751RapidContext.App._Cache = {
752 version: new Date().getTime().toString(16).slice(-8),
753 status: null,
754 user: null,
755 apps: {},
756
757 // Normalizes an app manifest and its resources
758 _normalizeApp(app) {
759 function toType(type, url) {
760 var isJs = !type && /\.js$/i.test(url);
761 var isCss = !type && /\.css$/i.test(url);
762 if (["code", "js", "javascript"].includes(type) || isJs) {
763 return "code";
764 } else if (!type && /\.mjs$/i.test(url)) {
765 return "module";
766 } else if (["style", "css"].includes(type) || isCss) {
767 return "style";
768 } else if (!type && /\.json$/i.test(url)) {
769 return "json";
770 } else if (!type && /ui\.xml$/i.test(url)) {
771 return "ui";
772 } else if (!type && !app.icon && /\.(gif|jpg|jpeg|png|svg)$/i.test(url)) {
773 return "icon";
774 } else {
775 return type;
776 }
777 }
778 function toIcon(res) {
779 if (res.url) {
780 return $("<img/>").attr({ src: res.url }).addClass("-app-icon").get(0);
781 } else if (res.html) {
782 var node = $("<span/>").html(res.html).addClass("-app-icon").get(0);
783 return node.childNodes.length === 1 ? node.childNodes[0] : node;
784 } else if (res["class"]) {
785 return $("<i/>").addClass(res["class"]).addClass("-app-icon").get(0);
786 } else {
787 return null;
788 }
789 }
790 function toResource(res) {
791 res = (typeof(res) === "string") ? { url: res } : res;
792 res.type = toType(res.type, res.url);
793 if (!app.icon && res.type === "icon") {
794 app.icon = toIcon(res);
795 }
796 return res;
797 }
798 app = Object.assign({}, app);
799 app.launch = app.launch || "manual";
800 app.resources = [].concat(app.resources).filter(Boolean).map(toResource);
801 if (!app.icon) {
802 app.icon = toIcon({ "class": "fa fa-4x fa-question-circle unimportant" });
803 }
804 return app;
805 },
806
807 // Updates the cache data with the procedure results.
808 update(proc, data) {
809 switch (proc) {
810 case "system/status":
811 this.status = { ...data };
812 console.log("Updated cached status", this.status);
813 break;
814 case "system/session/current":
815 if (this.user && this.user.id !== (data && data.user && data.user.id)) {
816 RapidContext.UI.Msg.error.loggedOut();
817 RapidContext.App.callProc = () => Promise.reject(new Error("logged out"));
818 } else if (!this.user && data && data.user) {
819 let o = this.user = RapidContext.Data.clone(data.user);
820 o.longName = o.name ? `${o.name} (${o.id})` : o.id;
821 console.log("Updated cached user", this.user);
822 }
823 break;
824 case "system/app/list":
825 if (data) {
826 for (let o of data) {
827 let launcher = this._normalizeApp(o);
828 launcher.instances = (this.apps[launcher.id] || {}).instances || [];
829 this.apps[launcher.id] = Object.assign(this.apps[launcher.id] || {}, launcher);
830 }
831 console.log("Updated cached apps", this.apps);
832 }
833 break;
834 }
835 },
836
837 // Updates the cache data on some procedure errors.
838 handleError(error) {
839 if (this.user && /permission denied/i.test(error)) {
840 setTimeout(() => RapidContext.App.callProc("system/session/current"));
841 }
842 }
843};
844
RapidContext
Access · Discovery · Insight
www.rapidcontext.com