diff --git a/webadmin/src/3rdparty/js/angular-bootstrap-multiselect.js b/webadmin/src/3rdparty/js/angular-bootstrap-multiselect.js new file mode 100644 index 000000000..26efbcff3 --- /dev/null +++ b/webadmin/src/3rdparty/js/angular-bootstrap-multiselect.js @@ -0,0 +1,354 @@ +"use strict"; + +angular.module("ui.multiselect", ["multiselect.tpl.html"]) + //from bootstrap-ui typeahead parser + .factory("optionParser", ["$parse", function($parse) { + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; + + return { + parse: function(input) { + + var match = input.match(TYPEAHEAD_REGEXP); + if(!match) { + throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + " but got '" + input + "'."); + } + + return { + itemName : match[3], + source : $parse(match[4]), + viewMapper : $parse(match[2] || match[1]), + modelMapper: $parse(match[1]) + }; + } + }; + }]) + .directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "optionParser", function($parse, $document, $compile, $interpolate, optionParser) { + return { + restrict: "E", + require : "ngModel", + link : function(originalScope, element, attrs, modelCtrl) { + + var exp = attrs.options; + var parsedResult = optionParser.parse(exp); + var isMultiple = attrs.multiple ? true : false; + var compareByKey = attrs.compareBy; + var headerKey = attrs.headerKey; + var dividerKey = attrs.dividerKey; + var scrollAfterRows = attrs.scrollAfterRows; + var tabindex = attrs.tabindex; + var maxWidth = attrs.maxWidth; + var required = false; + var scope = originalScope.$new(); + scope.filterAfterRows = attrs.filterAfterRows; + var changeHandler = attrs.change || angular.noop; + + scope.items = []; + scope.header = "Select"; + scope.multiple = isMultiple; + scope.disabled = false; + + scope.ulStyle = {}; + if(scrollAfterRows !== undefined && parseInt(scrollAfterRows).toString() === scrollAfterRows) { + scope.ulStyle = {"max-height": (scrollAfterRows*26+14)+"px", "overflow-y": "auto", "overflow-x": "hidden"}; + } + if(tabindex !== undefined && parseInt(tabindex).toString() === tabindex) { + scope.tabindex = tabindex; + } + if(maxWidth !== undefined && parseInt(maxWidth).toString() === maxWidth) { + scope.maxWidth = {"max-width": maxWidth + "px"}; + } + + originalScope.$on("$destroy", function() { + scope.$destroy(); + }); + + var popUpEl = angular.element(""); + + //required validator + if(attrs.required || attrs.ngRequired) { + required = true; + } + attrs.$observe("required", function(newVal) { + required = newVal; + }); + + //watch disabled state + scope.$watch(function() { + return $parse(attrs.ngDisabled)(originalScope); + }, function(newVal) { + scope.disabled = newVal; + }); + + //watch single/multiple state for dynamically change single to multiple + scope.$watch(function() { + return $parse(attrs.multiple)(originalScope); + }, function(newVal) { + isMultiple = newVal || false; + }); + + //watch option changes for options that are populated dynamically + scope.$watch(function() { + return parsedResult.source(originalScope); + }, function(newVal) { + if(angular.isDefined(newVal)) { + parseModel(); + } + }, true); + + //watch model change + scope.$watch(function() { + return modelCtrl.$modelValue; + }, function(newVal, oldVal) { + //when directive initialize, newVal usually undefined. Also, if model value already set in the controller + //for preselected list then we need to mark checked in our scope item. But we don't want to do this every time + //model changes. We need to do this only if it is done outside directive scope, from controller, for example. + if(angular.isDefined(newVal)) { + markChecked(newVal); + scope.$eval(changeHandler); + } + getHeaderText(); + modelCtrl.$setValidity("required", scope.valid()); + }); + + function parseModel() { + scope.items.length = 0; + var model = parsedResult.source(originalScope); + if(!angular.isDefined(model) || model === null) { + return; + } + for(var i = 0; i < model.length; i++) { + var local = {}; + local[parsedResult.itemName] = model[i]; + + // calculate checked status of the option + // https://github.com/sebastianha/angular-bootstrap-multiselect/pull/4/files + var id = model[i]; + var checked = false; + var modelValue = modelCtrl.$modelValue; + if (modelValue) { + if (angular.isArray(modelValue)) { + for (var j = 0; j < modelValue.length; j++) + if (modelValue[j] == id) { + checked = true; + break; + } + } else { + checked = modelValue == id; + } + } + + scope.items.push({ + label : parsedResult.viewMapper(local), + model : model[i], + checked: checked, + header : model[i][headerKey], + divider : model[i][dividerKey] + }); + } + } + + parseModel(); + + element.append($compile(popUpEl)(scope)); + + function getHeaderText() { + if(isEmpty(modelCtrl.$modelValue)) { + scope.header = attrs.msHeader || "Select"; + return scope.header; + } + + if(isMultiple) { + if(attrs.msSelected) { + scope.header = $interpolate(attrs.msSelected)(scope); + } else { + scope.header = modelCtrl.$modelValue.length + " " + "selected"; + } + } else { + var local = {}; + local[parsedResult.itemName] = modelCtrl.$modelValue; + scope.header = parsedResult.viewMapper(local); + } + } + + function isEmpty(obj) { + if(obj === true || obj === false) { + return false; + } + if(!obj) { + return true; + } + if(obj.length && obj.length > 0) { + return false; + } + for(var prop in obj) { + if(obj[prop]) { + return false; + } + } + if(compareByKey !== undefined && obj[compareByKey] !== undefined) { + return false; + } + + return true; + } + + scope.valid = function validModel() { + if(!required) { + return true; + } + var value = modelCtrl.$modelValue; + return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value !== null); + }; + + function selectSingle(item) { + if(!item.checked) { + scope.uncheckAll(); + item.checked = !item.checked; + } + setModelValue(false); + } + + function selectMultiple(item) { + item.checked = !item.checked; + setModelValue(true); + } + + function setModelValue(isMultiple) { + var value; + + if(isMultiple) { + value = []; + angular.forEach(scope.items, function(item) { + if(item.checked) { + value.push(item.model); + } + }); + } else { + angular.forEach(scope.items, function(item) { + if(item.checked) { + value = item.model; + return false; + } + }); + } + modelCtrl.$setViewValue(value); + } + + function markChecked(newVal) { + if(!angular.isArray(newVal)) { + angular.forEach(scope.items, function(item) { + item.checked = false; + if(compareByKey === undefined && angular.equals(item.model, newVal)) { + item.checked = true; + } else if(compareByKey !== undefined && newVal !== null && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], newVal[compareByKey])) { + item.checked = true; + } + }); + } else { + angular.forEach(scope.items, function(item) { + item.checked = false; + angular.forEach(newVal, function(i) { + if(compareByKey === undefined && angular.equals(item.model, i)) { + item.checked = true; + } else if(compareByKey !== undefined && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], i[compareByKey])) { + item.checked = true; + } + }); + }); + } + } + + scope.checkAll = function() { + if(!isMultiple) { + return; + } + angular.forEach(scope.items, function(item) { + item.checked = true; + }); + setModelValue(true); + }; + + scope.uncheckAll = function() { + angular.forEach(scope.items, function(item) { + item.checked = false; + }); + setModelValue(true); + }; + + scope.select = function(event, item) { + if(isMultiple === false) { + selectSingle(item); + scope.toggleSelect(); + } else { + event.stopPropagation(); + selectMultiple(item); + } + }; + } + }; + }]) + .directive("multiselectPopup", ["$document", function($document) { + return { + restrict : "E", + scope : false, + replace : true, + templateUrl: "multiselect.tpl.html", + link : function(scope, element, attrs) { + + scope.isVisible = false; + + scope.toggleSelect = function() { + if(element.hasClass("open")) { + scope.filter = ""; + element.removeClass("open"); + $document.unbind("click", clickHandler); + } else { + scope.filter = ""; + element.addClass("open"); + $document.bind("click", clickHandler); + } + }; + + // $("ul.dropdown-menu").on("click", "[data-stopPropagation]", function(e) { + // e.stopPropagation(); + // }); + + function clickHandler(event) { + if(elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { + return; + } + element.removeClass("open"); + $document.unbind("click", clickHandler); + scope.$apply(); + } + + var elementMatchesAnyInArray = function(element, elementArray) { + for(var i = 0; i < elementArray.length; i++) { + if(element === elementArray[i]) { + return true; + } + } + return false; + }; + } + }; + }]); + +angular.module("multiselect.tpl.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("multiselect.tpl.html", + "
\n" + + " \n" + + " \n" + + "
"); +}]); diff --git a/webadmin/src/index.html b/webadmin/src/index.html index b4887de3b..ca04d6fc2 100644 --- a/webadmin/src/index.html +++ b/webadmin/src/index.html @@ -62,6 +62,10 @@ + + + + diff --git a/webadmin/src/js/index.js b/webadmin/src/js/index.js index a1c044042..485a20511 100644 --- a/webadmin/src/js/index.js +++ b/webadmin/src/js/index.js @@ -9,7 +9,7 @@ if (search.accessToken) localStorage.token = search.accessToken; // create main application module -var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld']); +var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']); app.config(['NotificationProvider', function (NotificationProvider) { NotificationProvider.setOptions({ diff --git a/webadmin/src/views/email.html b/webadmin/src/views/email.html index 8625552d5..11ea08e34 100644 --- a/webadmin/src/views/email.html +++ b/webadmin/src/views/email.html @@ -58,6 +58,21 @@ +
+
+

Catch-All

+
+
+ +
+
+
+ Catch all Emails sent to non existing addresses and forward to those accounts: + +
+
+
+

DNS Records

diff --git a/webadmin/src/views/email.js b/webadmin/src/views/email.js index 41fcd4067..d5dc7a940 100644 --- a/webadmin/src/views/email.js +++ b/webadmin/src/views/email.js @@ -17,6 +17,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio { name: 'PTR', value: 'ptr' } ]; $scope.mailConfig = null; + $scope.users = []; $scope.showView = function (view) { // wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already @@ -28,6 +29,21 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio $('.modal').modal('hide'); }; + $scope.catchall = { + addresses: [], + busy: false, + + submit: function () { + $scope.catchall.busy = true; + + Client.setCatchallAddresses($scope.catchall.addresses, function (error) { + if (error) console.error('Unable to add catchall address.', error); + + $scope.catchall.busy = false; + }); + } + }; + $scope.email = { refreshBusy: false, @@ -107,9 +123,31 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio }); } + function getUsers() { + Client.getUsers(function (error, result) { + if (error) return console.error('Unable to get user listing.', error); + + // only allow users with a Cloudron email address + $scope.catchall.availableAddresses = result.filter(function (u) { return !!u.email; }).map(function (u) { return u.email; }); + }); + } + + function getCatchallAddresses() { + Client.getCatchallAddresses(function (error, result) { + if (error) return console.error('Unable to get catchall address listing.', error); + + // dedupe in case to avoid angular breakage + $scope.catchall.addresses = result.filter(function(item, pos, self) { + return self.indexOf(item) == pos; + }); + }); + } + Client.onReady(function () { getMailConfig(); getDnsConfig(); + getUsers(); + getCatchallAddresses(); $scope.email.refresh(); });