Files
cloudron-box/src/views/appstore.js

658 lines
27 KiB
JavaScript
Raw Normal View History

2018-01-22 13:01:38 -08:00
'use strict';
/* global angular:false */
/* global $:false */
2019-09-24 18:50:52 +02:00
/* global ERROR */
/* global RSTATES */
/* global moment */
2019-05-04 21:57:42 -07:00
angular.module('Application').controller('AppStoreController', ['$scope', '$location', '$timeout', '$routeParams', 'Client', function ($scope, $location, $timeout, $routeParams, Client) {
2020-02-24 12:56:13 +01:00
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
2018-01-22 13:01:38 -08:00
$scope.HOST_PORT_MIN = 1024;
$scope.HOST_PORT_MAX = 65535;
$scope.ready = false;
$scope.apps = [];
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.users = [];
$scope.groups = [];
$scope.domains = [];
$scope.category = '';
$scope.cachedCategory = ''; // used to cache the selected category while searching
$scope.searchString = '';
2019-05-04 21:57:42 -07:00
$scope.validSubscription = false;
$scope.unstableApps = false;
2019-05-04 21:57:42 -07:00
$scope.subscription = {};
2019-12-20 10:02:01 -08:00
$scope.memory = null; // { memory, swap }
2018-01-22 13:01:38 -08:00
$scope.showView = function (view) {
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
$('.modal').on('hidden.bs.modal', function () {
$scope.$apply(function () {
$scope.appInstall.reset();
$('.modal').off('hidden.bs.modal');
$location.path(view);
});
});
$('.modal').modal('hide');
};
2020-06-12 15:02:41 +02:00
$scope.categories = [
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
{ id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
{ id: 'document', icon: 'fa fa-file-word', label: 'Documents'},
{ id: 'email', icon: 'fa fa-envelope', label: 'Email'},
{ id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
{ id: 'finance', icon: 'fa fa-hand-holding-usd', label: 'Finance'},
{ id: 'forum', icon: 'fa fa-users', label: 'Forum'},
{ id: 'gallery', icon: 'fa fa-images', label: 'Gallery'},
{ id: 'game', icon: 'fa fa-gamepad', label: 'Games'},
{ id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
{ id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
{ id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
{ id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
{ id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
];
$scope.categoryButtonLabel = function (category) {
if (category === 'new') return 'Category';
if (category === 'recent') return 'Category';
if (category === 'featured') return 'Category';
var tmp = $scope.categories.find(function (c) { return c.id === category; });
if (tmp) return tmp.label;
return 'Category';
};
2018-01-22 13:01:38 -08:00
$scope.appInstall = {
busy: false,
state: 'appInfo',
error: {},
app: {},
needsOverwrite: false,
2018-01-22 13:01:38 -08:00
location: '',
domain: null,
portBindings: {},
mediaLinks: [],
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: '',
accessRestrictionOption: 'any',
accessRestriction: { users: [], groups: [] },
customAuth: false,
optionalSso: false,
subscriptionErrorMesssage: '',
2018-01-22 13:01:38 -08:00
isAccessRestrictionValid: function () {
var tmp = $scope.appInstall.accessRestriction;
return !!(tmp.users.length || tmp.groups.length);
},
reset: function () {
$scope.appInstall.app = {};
$scope.appInstall.error = {};
$scope.appInstall.needsOverwrite = false;
2018-01-22 13:01:38 -08:00
$scope.appInstall.location = '';
$scope.appInstall.domain = null;
$scope.appInstall.portBindings = {};
$scope.appInstall.state = 'appInfo';
$scope.appInstall.mediaLinks = [];
$scope.appInstall.certificateFile = null;
$scope.appInstall.certificateFileName = '';
$scope.appInstall.keyFile = null;
$scope.appInstall.keyFileName = '';
$scope.appInstall.accessRestrictionOption = 'any';
$scope.appInstall.accessRestriction = { users: [], groups: [] };
$scope.appInstall.optionalSso = false;
$scope.appInstall.customAuth = false;
$scope.appInstall.subscriptionErrorMesssage = '';
2018-01-22 13:01:38 -08:00
$('#collapseInstallForm').collapse('hide');
$('#collapseResourceConstraint').collapse('hide');
$('#collapseSubscriptionRequired').collapse('hide');
$('#collapseMediaLinksCarousel').collapse('show');
2018-01-22 13:01:38 -08:00
if ($scope.appInstallForm) {
$scope.appInstallForm.$setPristine();
$scope.appInstallForm.$setUntouched();
}
},
showForm: function (force) {
2019-12-20 10:02:01 -08:00
var app = $scope.appInstall.app;
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM+Swap
var used = Client.getInstalledApps().reduce(function (prev, cur) {
if (cur.runState === RSTATES.STOPPED) return prev;
return prev + (cur.memoryLimit || cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT);
}, 0);
2019-12-20 10:02:01 -08:00
var totalMemory = ($scope.memory.memory + $scope.memory.swap) * 1.5;
var available = (totalMemory || 0) - used;
var enoughResourcesAvailable = (available - needed) >= 0;
if (enoughResourcesAvailable || force) {
2018-01-22 13:01:38 -08:00
$scope.appInstall.state = 'installForm';
$('#collapseMediaLinksCarousel').collapse('hide');
$('#collapseResourceConstraint').collapse('hide');
$('#collapseInstallForm').collapse('show');
$('#appInstallLocationInput').focus();
} else {
$scope.appInstall.state = 'resourceConstraint';
$('#collapseMediaLinksCarousel').collapse('hide');
$('#collapseResourceConstraint').collapse('show');
}
},
show: function (app) { // this is an appstore app object!
2018-01-22 13:01:38 -08:00
$scope.appInstall.reset();
// make a copy to work with in case the app object gets updated while polling
angular.copy(app, $scope.appInstall.app);
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
$scope.appInstall.domain = $scope.domains.find(function (d) { return $scope.config.adminDomain === d.domain; }); // pre-select the adminDomain
$scope.appInstall.portBindingsInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts); // Portbinding map only for information
2018-01-22 13:01:38 -08:00
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
var manifest = app.manifest;
$scope.appInstall.optionalSso = !!manifest.optionalSso;
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oauth']);
$scope.appInstall.accessRestrictionOption = 'any';
$scope.appInstall.accessRestriction = { users: [], groups: [] };
2018-01-22 13:01:38 -08:00
// set default ports
var allPorts = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts);
for (var env in allPorts) {
$scope.appInstall.portBindings[env] = allPorts[env].defaultValue || 0;
2018-01-22 13:01:38 -08:00
$scope.appInstall.portBindingsEnabled[env] = true;
}
$('#appInstallModal').modal('show');
},
submit: function () {
$scope.appInstall.busy = true;
$scope.appInstall.error.other = null;
$scope.appInstall.error.location = null;
$scope.appInstall.error.port = null;
// only use enabled ports from portBindings
var finalPortBindings = {};
for (var env in $scope.appInstall.portBindings) {
if ($scope.appInstall.portBindingsEnabled[env]) {
finalPortBindings[env] = $scope.appInstall.portBindings[env];
}
}
var finalAccessRestriction = null;
if ($scope.appInstall.accessRestrictionOption === 'groups') {
finalAccessRestriction = { users: [], groups: [] };
finalAccessRestriction.users = $scope.appInstall.accessRestriction.users.map(function (u) { return u.id; });
finalAccessRestriction.groups = $scope.appInstall.accessRestriction.groups.map(function (g) { return g.id; });
}
var data = {
2019-09-24 00:21:01 +02:00
overwriteDns: $scope.appInstall.needsOverwrite,
2018-01-22 13:01:38 -08:00
location: $scope.appInstall.location || '',
domain: $scope.appInstall.domain.domain,
portBindings: finalPortBindings,
accessRestriction: finalAccessRestriction,
cert: $scope.appInstall.certificateFile,
key: $scope.appInstall.keyFile,
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso')
};
Client.checkDNSRecords(data.domain, data.location, function (error, result) {
2019-09-23 23:47:33 +02:00
if (error) return Client.error(error);
if (!data.overwriteDns) {
if (result.error || result.needsOverwrite) {
if (result.error) {
if (result.error.reason === ERROR.ACCESS_DENIED) {
$scope.appInstall.error.location = 'DNS credentials for ' + data.domain + ' are invalid. Update it in Domains & Certs view';
} else {
$scope.appInstall.error.location = result.error.message;
}
} else {
$scope.appInstall.error.location = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
$scope.appInstall.needsOverwrite = true;
}
2019-09-23 23:47:33 +02:00
$scope.appInstall.busy = false;
$scope.appInstallForm.location.$setPristine();
$('#appInstallLocationInput').focus();
return;
}
}
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error, newAppId) {
if (error) {
if (error.statusCode === 402) {
$scope.appInstall.state = 'subscriptionRequired';
$scope.appInstall.subscriptionErrorMesssage = error.message;
$('#collapseMediaLinksCarousel').collapse('hide');
$('#collapseResourceConstraint').collapse('hide');
$('#collapseInstallForm').collapse('hide');
$('#collapseSubscriptionRequired').collapse('show');
} else if (error.statusCode === 409) {
if (error.portName) {
$scope.appInstall.error.port = error.message;
} else if (error.domain) {
$scope.appInstall.error.location = error.message;
$scope.appInstallForm.location.$setPristine();
$('#appInstallLocationInput').focus();
} else {
$scope.appInstall.error.other = error.message;
}
} else if (error.statusCode === 400) {
if (error.field === 'cert') {
$scope.appInstall.error.cert = error.message;
$scope.appInstall.certificateFileName = '';
$scope.appInstall.certificateFile = null;
$scope.appInstall.keyFileName = '';
$scope.appInstall.keyFile = null;
} else {
$scope.appInstall.error.other = error.message;
}
} else {
$scope.appInstall.error.other = error.message;
}
2019-09-23 23:47:33 +02:00
$scope.appInstall.busy = false;
return;
2018-01-22 13:01:38 -08:00
}
$scope.appInstall.busy = false;
2019-09-23 23:47:33 +02:00
// stash new app id for later
$scope.appInstall.app.id = newAppId;
2019-09-23 23:47:33 +02:00
// we track the postinstall confirmation for the current user's browser
// TODO later we might want to have a notification db to track the state across admins and browsers
if ($scope.appInstall.app.manifest.postInstallMessage) {
localStorage['confirmPostInstall_' + $scope.appInstall.app.id] = true;
}
2018-01-22 13:01:38 -08:00
2019-09-23 23:47:33 +02:00
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
$('#appInstallModal').on('hidden.bs.modal', function () {
$scope.$apply(function () {
$location.path('/apps').search({ });
});
});
2018-01-22 13:01:38 -08:00
2019-09-23 23:47:33 +02:00
$('#appInstallModal').modal('hide');
});
});
2018-01-22 13:01:38 -08:00
}
};
$scope.appNotFound = {
appId: '',
version: ''
};
$scope.appstoreLogin = {
busy: false,
error: {},
email: '',
password: '',
2018-04-22 18:52:37 +02:00
totpToken: '',
2018-01-22 13:01:38 -08:00
register: true,
termsAccepted: false,
purpose: '',
2018-01-22 13:01:38 -08:00
submit: function () {
$scope.appstoreLogin.error = {};
$scope.appstoreLogin.busy = true;
Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, $scope.appstoreLogin.purpose, function (error) {
2018-01-22 13:01:38 -08:00
if (error) {
$scope.appstoreLogin.busy = false;
if (error.statusCode === 409) {
$scope.appstoreLogin.error.email = 'An account with this email already exists';
$scope.appstoreLogin.password = '';
$scope.appstoreLoginForm.email.$setPristine();
$scope.appstoreLoginForm.password.$setPristine();
$('#inputAppstoreLoginEmail').focus();
2019-05-04 21:57:42 -07:00
} else if (error.statusCode === 412) {
if (error.message.indexOf('TOTP token missing') !== -1) {
$scope.appstoreLogin.error.totpToken = 'A 2FA token is required';
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
} else if (error.message.indexOf('TOTP token invalid') !== -1) {
$scope.appstoreLogin.error.totpToken = 'Wrong 2FA token';
$scope.appstoreLogin.totpToken = '';
setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
} else {
$scope.appstoreLogin.error.password = 'Wrong email or password';
$scope.appstoreLogin.password = '';
$('#inputAppstoreLoginPassword').focus();
$scope.appstoreLoginForm.password.$setPristine();
}
} else if (error.statusCode === 424) {
if (error.message === 'wrong user') {
$scope.appstoreLogin.error.generic = 'Wrong cloudron.io account';
$scope.appstoreLogin.email = '';
$scope.appstoreLogin.password = '';
$scope.appstoreLoginForm.email.$setPristine();
$scope.appstoreLoginForm.password.$setPristine();
$('#inputAppstoreLoginEmail').focus();
} else {
console.error(error);
$scope.appstoreLogin.error.generic = error.message;
}
2018-01-22 13:01:38 -08:00
} else {
console.error(error);
2019-05-06 20:05:12 -07:00
$scope.appstoreLogin.error.generic = error.message || 'Please retry later';
2018-01-22 13:01:38 -08:00
}
return;
}
getSubscription(function (error) {
if (error) return console.error(error);
onSubscribed(function (error) { if (error) console.error(error); });
});
2018-01-22 13:01:38 -08:00
});
}
};
function onSubscribed(callback) {
Client.getAppstoreApps(function (error) {
if (error) return callback(error);
2018-01-22 13:01:38 -08:00
2020-06-12 15:02:41 +02:00
// start with all apps listing. this also sets $scope.apps accordingly
$scope.showCategory('');
// do this in background
fetchUsers();
fetchGroups();
// domains is required since we populate the dropdown with domains[0]
Client.getDomains(function (error, result) {
if (error) return callback(error);
$scope.domains = result;
// show install app dialog immediately if an app id was passed in the query
// hashChangeListener calls $apply, so make sure we don't double digest here
setTimeout(hashChangeListener, 1);
setTimeout(function () { $('#appstoreSearch').focus(); }, 1);
callback();
});
2018-01-22 13:01:38 -08:00
});
}
// TODO does not support testing apps in search
$scope.search = function () {
if (!$scope.searchString) return $scope.showCategory($scope.cachedCategory);
2018-01-22 13:01:38 -08:00
$scope.category = '';
2019-05-04 18:15:33 -07:00
Client.getAppstoreAppsFast(function (error, apps) {
2018-01-22 13:01:38 -08:00
if (error) return $timeout($scope.search, 1000);
var token = $scope.searchString.toUpperCase();
$scope.apps = apps.filter(function (app) {
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) return true;
if (app.manifest.tagline.toUpperCase().indexOf(token) !== -1) return true;
if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
return false;
});
});
};
2019-11-18 22:43:33 +01:00
function filterForNewApps(apps) {
2020-06-12 15:02:41 +02:00
var minApps = apps.length < 10 ? apps.length : 10; // prevent endless loop
2019-11-18 22:43:33 +01:00
var tmp = [];
var i = 0;
do {
var offset = moment().subtract(i++, 'days');
tmp = apps.filter(function (app) { return moment(app.ts).isAfter(offset); });
} while(tmp.length < minApps);
return tmp;
}
function filterForRecentlyUpdatedApps(apps) {
2020-06-12 15:02:41 +02:00
var minApps = apps.length < 10 ? apps.length : 10; // prevent endless loop
var tmp = [];
var i = 0;
do {
var offset = moment().subtract(i++, 'days');
tmp = apps.filter(function (app) { return moment(app.creationDate).isAfter(offset); });
} while(tmp.length < minApps);
return tmp;
}
2020-06-12 15:02:41 +02:00
$scope.showCategory = function (category) {
$scope.category = category;
2018-01-22 13:01:38 -08:00
$scope.cachedCategory = $scope.category;
2019-05-04 18:15:33 -07:00
Client.getAppstoreAppsFast(function (error, apps) {
2020-06-12 15:02:41 +02:00
if (error) return $timeout($scope.showCategory.bind(null, category), 1000);
2018-01-22 13:01:38 -08:00
if (!$scope.category) {
$scope.apps = apps.slice(0).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
} else if ($scope.category === 'featured') {
$scope.apps = apps.filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; }); // reverse sort
2019-11-18 22:43:33 +01:00
} else if ($scope.category === 'new') {
$scope.apps = filterForNewApps(apps);
} else if ($scope.category === 'recent') {
$scope.apps = filterForRecentlyUpdatedApps(apps);
2018-01-22 13:01:38 -08:00
} else {
$scope.apps = apps.filter(function (app) {
return app.manifest.tags.some(function (tag) { return $scope.category === tag; }); // reverse sort;
}).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
2018-01-22 13:01:38 -08:00
}
// ensure we scroll to top
document.getElementById('ng-view').scrollTop = 0;
2018-01-22 13:01:38 -08:00
});
};
$scope.openSubscriptionSetup = function () {
Client.getSubscription(function (error, subscription) {
if (error) return console.error('Unable to get subscription.', error);
Client.openSubscriptionSetup(subscription);
});
};
2018-01-22 13:01:38 -08:00
document.getElementById('appInstallCertificateFileInput').onchange = function (event) {
$scope.$apply(function () {
$scope.appInstall.certificateFile = null;
$scope.appInstall.certificateFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appInstall.certificateFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
document.getElementById('appInstallKeyFileInput').onchange = function (event) {
$scope.$apply(function () {
$scope.appInstall.keyFile = null;
$scope.appInstall.keyFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appInstall.keyFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
$scope.showAppNotFound = function (appId, version) {
$scope.appNotFound.appId = appId;
$scope.appNotFound.version = version;
$('#appNotFoundModal').modal('show');
};
$scope.gotoApp = function (app) {
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
};
function hashChangeListener() {
// event listener is called from DOM not angular, need to use $apply
$scope.$apply(function () {
var appId = $location.path().slice('/appstore/'.length);
var version = $location.search().version;
if (appId) {
2019-05-04 18:15:33 -07:00
Client.getAppstoreAppByIdAndVersion(appId, version || 'latest', function (error, result) {
if (error) {
$scope.showAppNotFound(appId, version);
console.error(error);
return;
}
2018-01-22 13:01:38 -08:00
$scope.appInstall.show(result);
});
2018-01-22 13:01:38 -08:00
} else {
$scope.appInstall.reset();
}
});
}
function fetchUsers() {
Client.getUsers(function (error, users) {
if (error) {
console.error(error);
return $timeout(fetchUsers, 5000);
}
$scope.users = users;
});
}
function fetchGroups() {
Client.getGroups(function (error, groups) {
if (error) {
console.error(error);
2019-05-05 07:40:11 -07:00
return $timeout(fetchGroups, 5000);
2018-01-22 13:01:38 -08:00
}
$scope.groups = groups;
});
}
2019-05-04 21:57:42 -07:00
function getSubscription(callback) {
Client.getSubscription(function (error, subscription) {
if (error) {
if (error.statusCode === 412) { // not registered yet
$scope.validSubscription = false;
2019-10-24 18:09:48 -07:00
} else if (error.statusCode === 402) { // invalid token, license error
2019-07-08 09:45:14 -07:00
$scope.validSubscription = false;
} else { // 424/external error?
return callback(error);
}
2019-05-04 21:57:42 -07:00
} else {
$scope.validSubscription = true;
2019-05-04 21:57:42 -07:00
$scope.subscription = subscription;
}
2019-05-04 21:57:42 -07:00
// clear busy state when a login/signup was performed
$scope.appstoreLogin.busy = false;
2019-05-04 21:57:42 -07:00
// also update the root controller status
if ($scope.$parent) $scope.$parent.updateSubscriptionStatus();
2019-05-04 21:57:42 -07:00
callback();
2018-01-22 13:01:38 -08:00
});
}
2019-12-20 10:02:01 -08:00
function getMemory(callback) {
Client.memory(function (error, memory) {
if (error) console.error(error);
$scope.memory = memory;
callback();
});
}
2018-01-22 13:01:38 -08:00
function init() {
$scope.ready = false;
getSubscription(function (error) {
2018-01-22 13:01:38 -08:00
if (error) {
console.error(error);
return $timeout(init, 1000);
}
if (!$scope.validSubscription) { // show the login form
$scope.ready = true;
return;
}
2018-01-22 13:01:38 -08:00
onSubscribed(function (error) {
if (error) console.error(error);
2018-01-22 13:01:38 -08:00
$scope.ready = true;
});
2018-01-22 13:01:38 -08:00
});
}
2019-12-20 10:02:01 -08:00
Client.onReady(function () {
getMemory(function () {
init();
});
});
2018-01-22 13:01:38 -08:00
// note: do not use hide.bs.model since it is called immediately from switchToAppsView which is already in angular scope
$('#appInstallModal').on('hidden.bs.modal', function () {
// clear the appid and version in the search bar when dialog is cancelled
$scope.$apply(function () {
$location.path('/appstore', false).search({ }); // 'false' means do not reload
});
});
window.addEventListener('hashchange', hashChangeListener);
$scope.$on('$destroy', function handler() {
window.removeEventListener('hashchange', hashChangeListener);
});
// setup all the dialog focus handling
2018-03-12 19:08:05 +01:00
['appInstallModal'].forEach(function (id) {
2018-01-22 13:01:38 -08:00
$('#' + id).on('shown.bs.modal', function () {
2019-05-05 09:05:06 -07:00
$(this).find('[autofocus]:first').focus();
2018-01-22 13:01:38 -08:00
});
});
// autofocus if appstore login is shown
2019-05-04 21:57:42 -07:00
$scope.$watch('validSubscription', function (newValue/*, oldValue */) {
2018-01-22 13:01:38 -08:00
if (!newValue) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
});
$('.modal-backdrop').remove();
}]);