From 65f56f47b50a840fbc186d6a4bd794c6894674c6 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Tue, 29 Mar 2016 12:15:37 -0400 Subject: [PATCH] (js) Improve Preferences module - Added constraint to timezone; - Enable save button only if form is dirty and valid; - Confirm unsaved changes before leaving. --- NEWS | 1 + UI/Common/GNUmakefile | 21 +-- UI/Common/UIxPageFrame.m | 6 - UI/Common/UIxTopnavToolbar.h | 36 ++++++ UI/Common/UIxTopnavToolbar.m | 51 ++++++++ .../English.lproj/Localizable.strings | 15 +++ UI/Templates/PreferencesUI/UIxPreferences.wox | 57 +++++---- ...olbarTemplate.wox => UIxTopnavToolbar.wox} | 5 + .../js/Common/sgSelectOnly.directive.js | 49 +++++++ .../js/Preferences/PreferencesController.js | 120 +++++++++++------- .../scss/components/list/list.scss | 14 +- 11 files changed, 289 insertions(+), 86 deletions(-) create mode 100644 UI/Common/UIxTopnavToolbar.h create mode 100644 UI/Common/UIxTopnavToolbar.m rename UI/Templates/{UIxTopnavToolbarTemplate.wox => UIxTopnavToolbar.wox} (93%) create mode 100644 UI/WebServerResources/js/Common/sgSelectOnly.directive.js diff --git a/NEWS b/NEWS index 421cb60b9..cc0cfd68e 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,7 @@ Enhancements - [web] we now "cc" delegates during invitation updates (#3195) - [web] new SOGoHelpURL preference to set a custom URL for SOGo help (#2768) - [web] now able to copy/move events and also duplicate them (#3196) + - [web] improve preferences validation and check for unsaved changes Bug fixes - [web] fixed missing columns in SELECT statements (PostgreSQL) diff --git a/UI/Common/GNUmakefile b/UI/Common/GNUmakefile index 39698a28f..39e5a584f 100644 --- a/UI/Common/GNUmakefile +++ b/UI/Common/GNUmakefile @@ -8,19 +8,20 @@ CommonUI_PRINCIPAL_CLASS = CommonUIProduct CommonUI_LANGUAGES = Arabic Basque BrazilianPortuguese Catalan ChineseTaiwan Croatian Czech Danish Dutch English Finnish French German Hungarian Icelandic Italian Lithuanian Macedonian NorwegianBokmal NorwegianNynorsk Polish Portuguese Russian Slovak Slovenian SpanishSpain SpanishArgentina Swedish Ukrainian Welsh -CommonUI_OBJC_FILES += \ - CommonUIProduct.m \ - UIxPageFrame.m \ +CommonUI_OBJC_FILES += \ + CommonUIProduct.m \ + UIxPageFrame.m \ \ - UIxAclEditor.m \ - UIxObjectActions.m \ - UIxFolderActions.m \ - UIxParentFolderActions.m \ - UIxUserRightsEditor.m \ + UIxAclEditor.m \ + UIxObjectActions.m \ + UIxFolderActions.m \ + UIxParentFolderActions.m \ + UIxUserRightsEditor.m \ \ - UIxToolbar.m \ + UIxTopnavToolbar.m \ + UIxToolbar.m \ \ - WODirectAction+SOGo.m \ + WODirectAction+SOGo.m \ CommonUI_RESOURCE_FILES += \ product.plist diff --git a/UI/Common/UIxPageFrame.m b/UI/Common/UIxPageFrame.m index 4dc8cc570..1754b10b0 100644 --- a/UI/Common/UIxPageFrame.m +++ b/UI/Common/UIxPageFrame.m @@ -623,9 +623,3 @@ @implementation UIxSidenavToolbarTemplate @end - -@interface UIxTopnavToolbarTemplate : UIxComponent -@end - -@implementation UIxTopnavToolbarTemplate -@end diff --git a/UI/Common/UIxTopnavToolbar.h b/UI/Common/UIxTopnavToolbar.h new file mode 100644 index 000000000..20996db54 --- /dev/null +++ b/UI/Common/UIxTopnavToolbar.h @@ -0,0 +1,36 @@ +/* UIxTopnavToolbar.h - this file is part of SOGo + * + * Copyright (C) 2016 Inverse inc. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef UIXTOPNAVTOOLBAR_H +#define UIXTOPNAVTOOLBAR_H + +#import + +@interface UIxTopnavToolbar : UIxComponent +{ + NSString *navButtonClick; +} + +- (void) setNavButtonClick: (NSString *)_navButtonClick; +- (NSString *) navButtonClick; + +@end + +#endif /* UIXTOPNAVTOOLBAR_H */ diff --git a/UI/Common/UIxTopnavToolbar.m b/UI/Common/UIxTopnavToolbar.m new file mode 100644 index 000000000..7e1363cee --- /dev/null +++ b/UI/Common/UIxTopnavToolbar.m @@ -0,0 +1,51 @@ +/* UIxTopnavToolbar.m - this file is part of SOGo + * + * Copyright (C) 2016 Inverse inc. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import "UIxTopnavToolbar.h" + +@implementation UIxTopnavToolbar + +- (id) init +{ + if ((self = [super init])) + { + navButtonClick = nil; + } + + return self; +} + +- (void) dealloc +{ + [navButtonClick release]; + [super dealloc]; +} + +- (void) setNavButtonClick: (NSString *)_navButtonClick +{ + ASSIGN (navButtonClick, _navButtonClick); +} + +- (NSString *) navButtonClick +{ + return navButtonClick; +} + +@end diff --git a/UI/PreferencesUI/English.lproj/Localizable.strings b/UI/PreferencesUI/English.lproj/Localizable.strings index a201cf460..9d5a39e46 100644 --- a/UI/PreferencesUI/English.lproj/Localizable.strings +++ b/UI/PreferencesUI/English.lproj/Localizable.strings @@ -3,6 +3,18 @@ "Close" = "Close"; "Preferences saved" = "Preferences saved"; +/* Unsaved changes confirmation dialog title */ +"Unsaved Changes" = "Unsaved Changes"; + +/* Unsaved changes confirmation dialog text */ +"Do you want to save your changes made to the configuration?" = "Do you want to save your changes made to the configuration?"; + +/* Unsaved changes confirmation dialog button */ +"Save" = "Save"; + +/* Unsaved changes confirmation dialog button */ +"Don't Save" = "Don't Save"; + /* tabs */ "General" = "General"; "Calendar Options" = "Calendar Options"; @@ -87,6 +99,9 @@ "timeFmt_3" = ""; "timeFmt_4" = ""; +/* Timezone autocompletion */ +"No matches found." = "No matches found."; + /* calendar */ "Week begins on" = "Week begins on"; "Day start time" = "Day start time"; diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index edbf437f3..63f046e68 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -10,9 +10,7 @@ title="title" const:jsFiles="Common.js, vendor/ng-sortable.js, Preferences.js, Preferences.services.js, Mailer.services.js, Contacts.services.js, vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js"> -
@@ -655,13 +668,13 @@ edit remove_circle @@ -672,7 +685,7 @@
@@ -700,7 +713,7 @@ + ng-click="app.removeMailLabel(key, preferencesForm)"> remove_circle @@ -709,7 +722,7 @@
+ ng-click="app.addMailLabel(preferencesForm)">
@@ -794,7 +807,7 @@ const:id="autoReplyEmailAddresses" ng-model="app.preferences.defaults.Vacation.autoReplyEmailAddresses"/> - +
diff --git a/UI/Templates/UIxTopnavToolbarTemplate.wox b/UI/Templates/UIxTopnavToolbar.wox similarity index 93% rename from UI/Templates/UIxTopnavToolbarTemplate.wox rename to UI/Templates/UIxTopnavToolbar.wox index d2b6c1b4f..39924846d 100644 --- a/UI/Templates/UIxTopnavToolbarTemplate.wox +++ b/UI/Templates/UIxTopnavToolbar.wox @@ -27,12 +27,14 @@ event contacts @@ -40,6 +42,7 @@ email @@ -47,6 +50,7 @@ settings_applications @@ -62,6 +66,7 @@ settings_power diff --git a/UI/WebServerResources/js/Common/sgSelectOnly.directive.js b/UI/WebServerResources/js/Common/sgSelectOnly.directive.js new file mode 100644 index 000000000..6294f30f1 --- /dev/null +++ b/UI/WebServerResources/js/Common/sgSelectOnly.directive.js @@ -0,0 +1,49 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + 'use strict'; + + /** + * sgSelectOnly - A directive that restricts an autocomplete field to its selectable values. + * @memberof SOGo.Common + * @ngInject + * @example: + + + */ + function sgSelectOnly() { + return { + link: postLink, + require: 'mdAutocomplete', + restrict: 'A' + }; + + function postLink(scope, element, attrs, autoComplete) { + function getInput() { + return element.find('input').eq(0); + } + + // We need to wait for the autocomplete directive to be compiled + var listener = scope.$watch(getInput, function (input) { + var ngModel; + + if (input.length) { + listener(); // self release + ngModel = input.controller('ngModel'); + input.on('blur', function () { + if (!autoComplete.scope.selectedItem) { + scope.$applyAsync(ngModel.$setValidity('required', false)); + } + }); + } + }); + } + } + + angular + .module('SOGo.Common') + .directive('sgSelectOnly', sgSelectOnly); +})(); diff --git a/UI/WebServerResources/js/Preferences/PreferencesController.js b/UI/WebServerResources/js/Preferences/PreferencesController.js index cfe8ed34f..44c9e4543 100644 --- a/UI/WebServerResources/js/Preferences/PreferencesController.js +++ b/UI/WebServerResources/js/Preferences/PreferencesController.js @@ -30,6 +30,7 @@ vm.removeMailFilter = removeMailFilter; vm.addDefaultEmailAddresses = addDefaultEmailAddresses; vm.userFilter = User.$filter; + vm.confirmChanges = confirmChanges; vm.save = save; vm.canChangePassword = canChangePassword; vm.changePassword = changePassword; @@ -55,46 +56,52 @@ User.$alternateAvatar = statePreferences.defaults.SOGoAlternateAvatar; }); - function go(module) { - // Close sidenav on small devices - if ($mdMedia('xs')) - $mdSidenav('left').close(); - $state.go('preferences.' + module); + function go(module, form) { + if (form.$valid) { + // Close sidenav on small devices + if ($mdMedia('xs')) + $mdSidenav('left').close(); + $state.go('preferences.' + module); + } } - function onLanguageChange() { + function onLanguageChange(form) { Dialog.confirm(l('Warning'), l('Save preferences and reload page now?'), {ok: l('Yes'), cancel: l('No')}) .then(function() { - save().then(function() { + save(form, { quick: true }).then(function() { $window.location.reload(true); }); }); } - function addCalendarCategory() { + function addCalendarCategory(form) { vm.preferences.defaults.SOGoCalendarCategoriesColors["New category"] = "#aaa"; vm.preferences.defaults.SOGoCalendarCategories.push("New category"); focus('calendarCategory_' + (vm.preferences.defaults.SOGoCalendarCategories.length - 1)); + form.$setDirty(); } - function removeCalendarCategory(index) { + function removeCalendarCategory(index, form) { var key = vm.preferences.defaults.SOGoCalendarCategories[index]; vm.preferences.defaults.SOGoCalendarCategories.splice(index, 1); delete vm.preferences.defaults.SOGoCalendarCategoriesColors[key]; + form.$setDirty(); } - function addContactCategory() { + function addContactCategory(form) { vm.preferences.defaults.SOGoContactsCategories.push(""); focus('contactCategory_' + (vm.preferences.defaults.SOGoContactsCategories.length - 1)); + form.$setDirty(); } - function removeContactCategory(index) { + function removeContactCategory(index, form) { vm.preferences.defaults.SOGoContactsCategories.splice(index, 1); + form.$setDirty(); } - function addMailAccount(ev) { + function addMailAccount(ev, form) { var account; vm.preferences.defaults.AuxiliaryMailAccounts.push({}); @@ -125,10 +132,12 @@ accountId: (vm.preferences.defaults.AuxiliaryMailAccounts.length-1), mailCustomFromEnabled: window.mailCustomFromEnabled } + }).then(function() { + form.$setDirty(); }); } - function editMailAccount(event, index) { + function editMailAccount(event, index, form) { var account = vm.preferences.defaults.AuxiliaryMailAccounts[index]; $mdDialog.show({ controller: 'AccountDialogController', @@ -143,24 +152,28 @@ } }).then(function() { vm.preferences.defaults.AuxiliaryMailAccounts[index] = account; + form.$setDirty(); }); } - function removeMailAccount(index) { + function removeMailAccount(index, form) { vm.preferences.defaults.AuxiliaryMailAccounts.splice(index, 1); + form.$setDirty(); } - function addMailLabel() { + function addMailLabel(form) { // See $omit() in the Preferences services for real key generation var key = '_$$' + guid(); vm.preferences.defaults.SOGoMailLabelsColors[key] = ["New label", "#aaa"]; + form.$setDirty(); } - function removeMailLabel(key) { + function removeMailLabel(key, form) { delete vm.preferences.defaults.SOGoMailLabelsColors[key]; + form.$setDirty(); } - function addMailFilter(ev) { + function addMailFilter(ev, form) { var filter = { match: 'all' }; $mdDialog.show({ @@ -177,10 +190,11 @@ if (!vm.preferences.defaults.SOGoSieveFilters) vm.preferences.defaults.SOGoSieveFilters = []; vm.preferences.defaults.SOGoSieveFilters.push(filter); + form.$setDirty(); }); } - function editMailFilter(ev, index) { + function editMailFilter(ev, index, form) { var filter = angular.copy(vm.preferences.defaults.SOGoSieveFilters[index]); $mdDialog.show({ @@ -195,14 +209,16 @@ } }).then(function() { vm.preferences.defaults.SOGoSieveFilters[index] = filter; + form.$setDirty(); }); } - function removeMailFilter(index) { + function removeMailFilter(index, form) { vm.preferences.defaults.SOGoSieveFilters.splice(index, 1); + form.$setDirty(); } - function addDefaultEmailAddresses() { + function addDefaultEmailAddresses(form) { var v = []; if (angular.isDefined(vm.preferences.defaults.Vacation.autoReplyEmailAddresses)) { @@ -210,9 +226,38 @@ } vm.preferences.defaults.Vacation.autoReplyEmailAddresses = (_.union(window.defaultEmailAddresses.split(','), v)).join(','); + form.$setDirty(); } - - function save() { + + function confirmChanges($event, form) { + var target; + + if (form.$dirty) { + // Stop default action + $event.preventDefault(); + $event.stopPropagation(); + + // Find target link + target = $event.target; + while (target.tagName != 'A') + target = target.parentNode; + + Dialog.confirm(l('Unsaved Changes'), + l('Do you want to save your changes made to the configuration?'), + { ok: l('Save'), cancel: l('Don\'t Save') }) + .then(function() { + // Save & follow link + save(form, { quick: true }).then(function() { + $window.location = target.href; + }); + }, function() { + // Don't save & follow link + $window.location = target.href; + }); + } + } + + function save(form, options) { var i, sendForm, addresses, defaultAddresses, domains, domain; sendForm = true; @@ -252,21 +297,14 @@ if (sendForm) return vm.preferences.$save().then(function(data) { - $mdToast.show({ - controller: 'savePreferencesToastCtrl', - template: [ - '', - '
', - ' ' + l('Preferences saved') + '', - ' ', - ' close', - ' ', - '
', - '
' - ].join(''), - hideDelay: 2000, - position: 'top right' - }); + if (!options || !options.quick) { + $mdToast.show( + $mdToast.simple() + .content(l('Preferences saved')) + .position('bottom right') + .hideDelay(2000)); + form.$setPristine(); + } }); return $q.reject(); @@ -312,16 +350,8 @@ } } - savePreferencesToastCtrl.$inject = ['$scope', '$mdToast']; - function savePreferencesToastCtrl($scope, $mdToast) { - $scope.closeToast = function() { - $mdToast.hide(); - }; - } - angular .module('SOGo.PreferencesUI') - .controller('savePreferencesToastCtrl', savePreferencesToastCtrl) .controller('PreferencesController', PreferencesController); })(); diff --git a/UI/WebServerResources/scss/components/list/list.scss b/UI/WebServerResources/scss/components/list/list.scss index 2026e3650..64b5781cf 100644 --- a/UI/WebServerResources/scss/components/list/list.scss +++ b/UI/WebServerResources/scss/components/list/list.scss @@ -80,11 +80,19 @@ md-list-item { .md-button, &.md-clickable { margin: 0; - transition: background-color $swift-ease-in-duration $swift-ease-in-timing-function; + transition: background-color $swift-ease-in-duration $swift-ease-in-timing-function, + color $swift-linear-duration $swift-linear-timing-function; } - &.md-clickable:hover { - background-color: rgba(158,158,158,0.2); + &.md-clickable:not([disabled]):hover { + background-color: rgba($colorGrey500, 0.2); // See button-theme.scss + } + + &[disabled] { + color: rgba(0,0,0,0.38) !important; // = {{foreground-3}}; See button-theme.scss + md-icon { + color: rgba(0,0,0,0.38); + } } } }