(js) Improve Preferences module

- Added constraint to timezone;
- Enable save button only if form is dirty and valid;
- Confirm unsaved changes before leaving.
pull/205/head
Francis Lachapelle 2016-03-29 12:15:37 -04:00
parent 29ec1a8ba0
commit 65f56f47b5
11 changed files with 289 additions and 86 deletions

1
NEWS
View File

@ -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)

View File

@ -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

View File

@ -623,9 +623,3 @@
@implementation UIxSidenavToolbarTemplate
@end
@interface UIxTopnavToolbarTemplate : UIxComponent
@end
@implementation UIxTopnavToolbarTemplate
@end

View File

@ -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 <SOGoUI/UIxComponent.h>
@interface UIxTopnavToolbar : UIxComponent
{
NSString *navButtonClick;
}
- (void) setNavButtonClick: (NSString *)_navButtonClick;
- (NSString *) navButtonClick;
@end
#endif /* UIXTOPNAVTOOLBAR_H */

View File

@ -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

View File

@ -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";

View File

@ -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">
<main class="view"
layout="row" layout-fill="layout-fill"
ui-view="preferences"
<main layout-fill="layout-fill" ui-view="preferences"
ng-controller="navController"><!-- preferences --> </main>
<script type="text/javascript">
@ -26,34 +24,40 @@
</script>
<script type="text/ng-template" id="preferences.html">
<form name="preferencesForm" class="view"
layout="row" layout-fill="layout-fill">
<md-sidenav id="left-sidenav" class="md-sidenav-left md-whiteframe-z1" layout="column"
md-component-id="left" md-is-locked-open="isGtMedium"
ng-class="{ 'sg-close': leftIsClose }">
<var:component className="UIxSidenavToolbarTemplate" />
<md-content md-scroll-y="md-scroll-y" class="md-flex md-hue-2">
<md-list>
<md-list-item ng-click="app.go('general')"
<md-list-item ng-click="app.go('general', preferencesForm)"
ng-disabled="preferencesForm.$invalid"
ui-sref="preferences.general"
ui-sref-active="md-default-theme md-background md-bg md-hue-1">
<md-icon>settings</md-icon>
<p class="sg-item-name"><var:string label:value="General"/></p>
</md-list-item>
<var:if condition="userHasCalendarAccess">
<md-list-item ng-click="app.go('calendars')"
<md-list-item ng-click="app.go('calendars', preferencesForm)"
ng-disabled="preferencesForm.$invalid"
ui-sref="preferences.calendars"
ui-sref-active="md-default-theme md-background md-bg md-hue-1">
<md-icon>event</md-icon>
<p class="sg-item-name"><var:string label:value="Calendar"/></p>
</md-list-item>
</var:if>
<md-list-item ng-click="app.go('addressbooks')"
<md-list-item ng-click="app.go('addressbooks', preferencesForm)"
ng-disabled="preferencesForm.$invalid"
ui-sref="preferences.addressbooks"
ui-sref-active="md-default-theme md-background md-bg md-hue-1">
<md-icon>contacts</md-icon>
<p class="sg-item-name"><var:string label:value="Contacts"/></p>
</md-list-item>
<var:if condition="userHasMailAccess">
<md-list-item ng-click="app.go('mailer')"
<md-list-item ng-click="app.go('mailer', preferencesForm)"
ng-disabled="preferencesForm.$invalid"
ui-sref="preferences.mailer"
ui-sref-active="md-default-theme md-background md-bg md-hue-1">
<md-icon>email</md-icon>
@ -68,18 +72,20 @@
<!-- TOP RIGHT TOOLBAR -->
<md-toolbar layout="row" layout-align="space-between start" class="md-tall">
<var:component className="UIxTopnavToolbarTemplate" />
<var:component className="UIxTopnavToolbarTemplate" const:navButtonClick="app.confirmChanges($event, preferencesForm)"/>
<md-button type="button" class="md-fab md-fab-bottom-right md-fab-overlap-bottom"
ng-click="app.save()">
ng-disabled="preferencesForm.$invalid || preferencesForm.$pristine"
ng-click="app.save(preferencesForm)">
<md-icon>save</md-icon>
</md-button>
</md-toolbar>
<md-content>
<md-content layout="column">
<div ui-view="module"><!-- view --></div>
</md-content>
</section>
</form>
</script>
<!--
@ -104,7 +110,7 @@
<md-input-container class="md-block" flex="50">
<label><var:string label:value="Language"/></label>
<md-select ng-model="app.preferences.defaults.SOGoLanguage"
ng-change="app.onLanguageChange()">
ng-change="app.onLanguageChange(preferencesForm)">
<var:foreach list="languages" item="item">
<md-option var:value="item">
<var:string value="languageText"/>
@ -114,15 +120,22 @@
</md-input-container>
<md-autocomplete
class="md-block" flex="50"
style="padding-bottom: 0"
md-search-text="app.timeZonesSearchText"
md-selected-item="app.preferences.defaults.SOGoTimeZone"
md-items="timezone in app.timeZonesListFilter(app.timeZonesSearchText)"
md-item-text="timezone"
md-min-length="3"
md-select-on-match="true"
md-match-case-insensitive="true"
ng-required="true"
sg-select-only="sg-select-only"
label:md-floating-label="Current Time Zone">
<span md-highlight-text="app.timeZonesSearchText">{{timezone}}</span>
<md-item-template>
<span md-highlight-text="app.timeZonesSearchText">{{timezone}}</span>
</md-item-template>
<md-not-found>
<var:string label:value="No matches found."/>
</md-not-found>
</md-autocomplete>
</div>
@ -365,7 +378,7 @@
<md-button class="sg-icon-button" type="button"
layout="row" layout-align="end center"
label:aria-label="Remove Calendar Category"
ng-click="app.removeCalendarCategory($index)">
ng-click="app.removeCalendarCategory($index, preferencesForm)">
<md-icon>remove_circle</md-icon>
</md-button>
</md-list-item>
@ -374,7 +387,7 @@
<div layout="row" layout-align="end center">
<md-button type="button"
label:aria-label="Add Calendar Category"
ng-click="app.addCalendarCategory()">
ng-click="app.addCalendarCategory(preferencesForm)">
<var:string label:value="Add Calendar Category"/>
</md-button>
</div>
@ -586,7 +599,7 @@
ng-show="app.preferences.defaults.SOGoMailComposeMessageType == 'html'">
<md-checkbox
ng-model="app.preferences.defaults.SOGoMailComposeFontSizeEnabled"
label:aria-label="Default font size">
label:aria-label="Default font size"><!-- default font size -->
</md-checkbox>
<md-input-container class="md-block md-flex">
<label><var:string label:value="Default font size"/></label>
@ -655,13 +668,13 @@
<input type="text" ng-model="app.preferences.defaults.SOGoSieveFilters[$index].name"/>
</md-input-container>
<md-button class="sg-icon-button" type="button"
ng-click="app.editMailFilter($event, $index)"
ng-click="app.editMailFilter($event, $index, preferencesForm)"
layout="row" layout-align="end center"
label:aria-label="Edit Filter">
<md-icon>edit</md-icon>
</md-button>
<md-button class="sg-icon-button" type="button"
ng-click="app.removeMailFilter($index)"
ng-click="app.removeMailFilter($index, preferencesForm)"
layout="row" layout-align="end center"
label:aria-label="Delete Filter">
<md-icon>remove_circle</md-icon>
@ -672,7 +685,7 @@
<!-- FIXME: move up/down to be replaced by DnD? -->
<div layout="row" layout-align="end center">
<md-button type="button"
ng-click="app.addMailFilter($event)"
ng-click="app.addMailFilter($event, preferencesForm)"
label:aria-label="Create Filter">
<var:string label:value="Create Filter"/>
</md-button>
@ -700,7 +713,7 @@
<md-button class="md-icon-button" type="button"
layout="row" layout-align="end center"
label:aria-label="Delete Label"
ng-click="app.removeMailLabel(key)">
ng-click="app.removeMailLabel(key, preferencesForm)">
<md-icon>remove_circle</md-icon>
</md-button>
</md-list-item>
@ -709,7 +722,7 @@
<div layout="row" layout-align="end center">
<md-button type="button"
label:aria-label="Create Label"
ng-click="app.addMailLabel()">
ng-click="app.addMailLabel(preferencesForm)">
<var:string label:value="Create Label"/>
</md-button>
</div>
@ -794,7 +807,7 @@
const:id="autoReplyEmailAddresses"
ng-model="app.preferences.defaults.Vacation.autoReplyEmailAddresses"/>
</md-input-container>
<md-button ng-click="app.addDefaultEmailAddresses()">
<md-button ng-click="app.addDefaultEmailAddresses(preferencesForm)">
<var:string label:value="Add default email addresses" type="button"/>
</md-button>
</div>

View File

@ -27,12 +27,14 @@
<md-button class="md-icon-button"
ng-show="activeUser.path.calendar.length"
ng-disabled="baseURL.endsWith('/Calendar/')"
var:ng-click="navButtonClick"
ng-href="{{activeUser.path.calendar}}">
<md-tooltip><var:string label:value="Calendar"/></md-tooltip>
<md-icon>event</md-icon>
</md-button>
<md-button class="md-icon-button"
ng-disabled="baseURL.endsWith('/Contacts/')"
var:ng-click="navButtonClick"
ng-href="{{activeUser.path.contacts}}">
<md-icon>contacts</md-icon>
<md-tooltip><var:string label:value="Address Book"/></md-tooltip>
@ -40,6 +42,7 @@
<md-button class="md-icon-button"
ng-show="activeUser.path.mail.length"
ng-disabled="baseURL.endsWith('/Mail/')"
var:ng-click="navButtonClick"
ng-href="{{activeUser.path.mail}}">
<md-icon>email</md-icon>
<md-tooltip><var:string label:value="Mail"/></md-tooltip>
@ -47,6 +50,7 @@
<md-button class="md-icon-button"
ng-disabled="baseURL.endsWith('/Administration/')"
ng-show="activeUser.isSuperUser"
var:ng-click="navButtonClick"
ng-href="{{activeUser.path.administration}}">
<md-icon>settings_applications</md-icon>
<md-tooltip><var:string label:value="Administration"/></md-tooltip>
@ -62,6 +66,7 @@
</md-button>
<md-button class="md-icon-button"
ng-show="activeUser.path.logoff.length"
var:ng-click="navButtonClick"
ng-href="{{activeUser.path.logoff}}">
<md-icon>settings_power</md-icon>
<md-tooltip><var:string label:value="Disconnect"/></md-tooltip>

View File

@ -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:
<md-autocomplete
md-items="timezone in timeZones"
ng-required="true"
sg-select-only>
*/
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);
})();

View File

@ -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: [
'<md-toast>',
' <div class="md-toast-content">',
' <span flex>' + l('Preferences saved') + '</span>',
' <md-button class="md-icon-button md-primary" ng-click="closeToast()">',
' <md-icon>close</md-icon>',
' </md-button>',
' </div>',
'</md-toast>'
].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);
})();

View File

@ -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);
}
}
}
}