(js) Create card from sender/recipient address

Fixes #3002
Fixes #4610
pull/248/head
Francis Lachapelle 2019-01-10 22:00:19 -05:00
parent 22762b3bd4
commit ca886aa662
8 changed files with 248 additions and 55 deletions

6
NEWS
View File

@ -1,3 +1,9 @@
4.0.6 (YYYY-MM-DD)
------------------
Enhancements
- [web] create card from sender or recipient address (#3002, #4610)
4.0.5 (2019-01-09)
------------------

View File

@ -179,6 +179,7 @@
/* Address Popup menu */
"Add to Address Book..." = "Add to Address Book...";
"Successfully created card" = "Successfully created card";
"Compose Mail To" = "Compose Mail To";
"Create Filter From Message..." = "Create Filter From Message...";

View File

@ -39,7 +39,7 @@
<var:string label:value="Search subfolders"/>
</sg-checkmark>
</md-menu-item>
<md-menu-divider> <!-- divider --></md-menu-divider>
<md-menu-divider><!-- divider --></md-menu-divider>
<md-menu-item>
<md-button ng-click="app.search.match='AND'">
<md-icon ng-class="{ 'icon-check': app.search.match == 'AND'}"
@ -376,4 +376,42 @@
</md-content>
</div>
<!-- template of contextual menu for a recipient -->
<script type="text/ng-template" id="UIxMailViewRecipientMenu">
<div md-whiteframe="3">
<md-menu-content class="md-dense" width="3">
<md-menu-item>
<md-button disabled="disabled" md-menu-align-target="md-menu-align-target">
{{ $menuCtrl.recipient.full }}
</md-button>
</md-menu-item>
<md-menu-item>
<md-button type="button"
ng-click="$menuCtrl.newMessage($event, { to: [$menuCtrl.recipient.full] })">
<var:string label:value="Write a new message"/>
</md-button>
</md-menu-item>
<md-menu-item>
<md-menu md-position-mode="cascade cascade">
<md-button
label:aria-label="Add to Address Book..."
ng-mouseenter="$mdMenu.open($event)"
ng-keydown="$mdMenu.onKeyDown($event)">
<var:string label:value="Add to Address Book..."/>
</md-button>
<md-menu-content class="md-dense" width="4">
<md-menu-item ng-repeat="addressbook in $menuCtrl.addressbooks track by addressbook.id">
<md-button ng-click="$menuCtrl.newCard($menuCtrl.recipient, addressbook.id)">{{addressbook.name}}</md-button>
</md-menu-item>
<md-menu-item ng-repeat="addressbook in $menuCtrl.subscriptions track by addressbook.id">
<md-button ng-click="$menuCtrl.newCard($menuCtrl.recipient, addressbook.id)"
ng-disabled="!addressbook.acls.objectCreator">{{addressbook.name}}</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</md-menu-item>
</md-menu-content>
</div>
</script>
</container>

View File

@ -127,56 +127,70 @@
<h5 class="sg-md-headline" ng-bind="viewer.message.subject"><!-- subject --></h5>
<time class="msg-date" datetime="viewer.message.date" ng-bind="::viewer.message.date"><!-- date --></time>
</div>
<div>
<div layout="row" layout-wrap="layout-wrap">
<div class="pseudo-input-container--compact" flex="50" flex-xs="100">
<div layout="row" layout-align="start center">
<sg-avatar-image class="md-tile-left"
sg-email="::viewer.message.from[0].email"
size="40">person</sg-avatar-image>
<div>
<span ng-bind="::viewer.message.from[0].name"><!-- from --></span>
<br/>
<a href="#" class="md-caption"
ng-bind="::viewer.message.from[0].email"
ng-click="viewer.newMessage($event, { to: [viewer.message.from[0].full] })"><!-- from --></a>
</div>
<div layout="row" layout-wrap="layout-wrap">
<div class="pseudo-input-container--compact" flex="50" flex-xs="100">
<div layout="row" layout-align="start center">
<sg-avatar-image class="md-tile-left"
sg-email="::viewer.message.from[0].email"
size="40">person</sg-avatar-image>
<div>
<span ng-bind="::viewer.message.from[0].name"><!-- from --></span>
<a href="#" class="md-caption"
ng-bind="::viewer.message.from[0].email"
ng-click="viewer.selectRecipient(viewer.message.from[0], $event)"><!-- from --></a>
</div>
</div>
<div class="msg-recipients" layout="column" flex="50" flex-xs="100">
<div class="pseudo-input-container--compact">
<label class="pseudo-input-label">
<var:string label:value="To"/>
</label>
<div class="pseudo-input-field"
ng-hide="viewer.$showDetailedRecipients">
<a href="#" ng-click="viewer.toggleDetailedRecipients($event)"
ng-bind="::viewer.message.$shortRecipients(5)"><!-- to --></a>
</div>
<div class="pseudo-input-field" ng-show="viewer.$showDetailedRecipients">
<span ng-repeat="recipient in viewer.message.to">
<a href="#" ng-bind="::recipient.full"
ng-click="viewer.newMessage($event, { to: [recipient.full] })"><!-- recipient --></a>
</span>
</div>
</div>
<div class="msg-recipients" layout="column" flex="50" flex-xs="100">
<div class="pseudo-input-container--compact">
<label class="pseudo-input-label">
<var:string label:value="To"/>
</label>
<div class="pseudo-input-field" ng-hide="viewer.$showDetailedRecipients">
<a href="#" ng-click="viewer.toggleDetailedRecipients($event)"
ng-bind="::viewer.message.$shortRecipients(5)"><!-- to --></a>
</div>
<div class="pseudo-input-container--compact" ng-show="viewer.$showDetailedRecipients">
<label class="pseudo-input-label" ng-show="::viewer.message.cc.length > 0">
<var:string label:value="Cc"/>
</label>
<div class="pseudo-input-field">
<span ng-repeat="recipient in ::viewer.message.cc">
<a href="#" ng-bind="::recipient.full"
ng-click="viewer.newMessage($event, { to: [recipient.full] })"><!-- recipient --></a>
</span>
<md-button style="float: right"
label:aria-label="Hide"
ng-click="viewer.toggleDetailedRecipients($event)">
<var:string label:value="Hide"/>
</md-button>
</div>
<div class="pseudo-input-field" ng-show="viewer.$showDetailedRecipients">
<md-chips
class="sg-dense sg-readonly"
ng-model="::viewer.message.to"
md-removable="::false"
readonly="::true">
<md-chip-template
ng-click="viewer.selectRecipient($chip, $event)"
ng-focus="viewer.focusChip($event)"
ng-blur="viewer.blurChip($event)">
{{ $chip.name || $chip.email }}
</md-chip-template>
</md-chips>
</div>
</div>
<div class="pseudo-input-container--compact" ng-show="viewer.$showDetailedRecipients">
<label class="pseudo-input-label" ng-show="::viewer.message.cc.length > 0">
<var:string label:value="Cc"/>
</label>
<div class="pseudo-input-field" ng-show="::viewer.message.cc.length > 0">
<md-chips
class="sg-dense sg-readonly"
ng-model="::viewer.message.cc"
md-removable="::false"
readonly="::true">
<md-chip-template
ng-click="viewer.selectRecipient($chip, $event)"
ng-focus="viewer.focusChip($event)"
ng-blur="viewer.blurChip($event)">
{{ $chip.name || $chip.email }}
</md-chip-template>
</md-chips>
</div>
<md-button
ng-hide="viewer.$alwaysShowDetailedRecipients || !viewer.$showDetailedRecipients"
style="float: right"
label:aria-label="Hide"
ng-click="viewer.toggleDetailedRecipients($event)">
<var:string label:value="Hide"/>
</md-button>
</div>
</div>
</div>
<div class="sg-padded" ng-show="viewer.showFlags">

View File

@ -45,6 +45,9 @@
$Preferences: Preferences,
$query: {value: '', sort: 'c_cn', asc: 1},
activeUser: Settings.activeUser(),
$addressbooks: [],
$subscriptions: [],
$remotes: [],
selectedFolder: null,
$refreshTimeout: null
});
@ -159,10 +162,10 @@
*/
AddressBook.$findAll = function(data) {
var _this = this;
if (data) {
this.$addressbooks = [];
this.$subscriptions = [];
this.$remotes = [];
if (data && data.length) {
this.$addressbooks.splice(0, this.$addressbooks.length);
this.$subscriptions.splice(0, this.$subscriptions.length);
this.$remotes.splice(0, this.$remotes.length);
// Instanciate AddressBook objects
angular.forEach(data, function(o, i) {
var addressbook = new AddressBook(o);
@ -174,6 +177,12 @@
_this.$addressbooks.push(addressbook);
});
}
else if (angular.isArray(data)) { // empty array
return AddressBook.$$resource.fetch('addressbooksList').then(function(data) {
return AddressBook.$findAll(data.addressbooks);
});
}
return _.union(this.$addressbooks, this.$subscriptions, this.$remotes);
};

View File

@ -6,8 +6,8 @@
/**
* @ngInject
*/
MessageController.$inject = ['$window', '$scope', '$q', '$state', '$mdMedia', '$mdDialog', 'sgConstant', 'stateAccounts', 'stateAccount', 'stateMailbox', 'stateMessage', 'sgHotkeys', 'encodeUriFilter', 'sgSettings', 'ImageGallery', 'sgFocus', 'Dialog', 'Preferences', 'Calendar', 'Component', 'Account', 'Mailbox', 'Message'];
function MessageController($window, $scope, $q, $state, $mdMedia, $mdDialog, sgConstant, stateAccounts, stateAccount, stateMailbox, stateMessage, sgHotkeys, encodeUriFilter, sgSettings, ImageGallery, focus, Dialog, Preferences, Calendar, Component, Account, Mailbox, Message) {
MessageController.$inject = ['$window', '$scope', '$q', '$state', '$mdMedia', '$mdDialog', '$mdPanel', 'sgConstant', 'stateAccounts', 'stateAccount', 'stateMailbox', 'stateMessage', 'sgHotkeys', 'encodeUriFilter', 'sgSettings', 'ImageGallery', 'sgFocus', 'Dialog', 'Preferences', 'Calendar', 'Component', 'Account', 'Mailbox', 'Message', 'AddressBook', 'Card'];
function MessageController($window, $scope, $q, $state, $mdMedia, $mdDialog, $mdPanel, sgConstant, stateAccounts, stateAccount, stateMailbox, stateMessage, sgHotkeys, encodeUriFilter, sgSettings, ImageGallery, focus, Dialog, Preferences, Calendar, Component, Account, Mailbox, Message, AddressBook, Card) {
var vm = this, popupWindow = null, hotkeys = [];
this.$onInit = function() {
@ -27,7 +27,8 @@
this.service = Message;
this.tags = { searchText: '', selected: '' };
this.showFlags = stateMessage.flags && stateMessage.flags.length > 0;
this.$showDetailedRecipients = false;
this.$alwaysShowDetailedRecipients = (!stateMessage.to || stateMessage.to.length < 5) && (!stateMessage.cc || stateMessage.cc.length < 5);
this.$showDetailedRecipients = this.$alwaysShowDetailedRecipients;
this.showRawSource = false;
_registerHotkeys(hotkeys);
@ -197,6 +198,108 @@
$event.preventDefault();
};
this.focusChip = function($event) {
var chipElement = $event.target;
while (chipElement.tagName !== 'MD-CHIP') {
chipElement = chipElement.parentNode;
}
chipElement.classList.add('md-focused');
};
this.blurChip = function($event) {
var chipElement = $event.target;
while (chipElement.tagName !== 'MD-CHIP') {
chipElement = chipElement.parentNode;
}
chipElement.classList.remove('md-focused');
if ($event.relatedTarget && $event.relatedTarget.tagName === 'MD-CHIP-TEMPLATE') {
// Moving to another chip; close menu
vm.panel.close();
}
};
this.selectRecipient = function(recipient, $event) {
// Fetch addressbooks list
AddressBook.$findAll([]);
var targetElement = $event.target;
var panelPosition = $mdPanel.newPanelPosition()
.relativeTo(targetElement)
.addPanelPosition(
$mdPanel.xPosition.ALIGN_START,
$mdPanel.yPosition.ALIGN_TOPS
);
var panelAnimation = $mdPanel.newPanelAnimation()
.openFrom(targetElement)
.duration(100)
.withAnimation($mdPanel.animation.FADE);
var config = {
attachTo: angular.element(document.body),
locals: {
recipient: recipient,
addressbooks: AddressBook.$addressbooks,
subscriptions: AddressBook.$subscriptions,
newMessage: angular.bind(this, this.newMessage)
},
bindToController: true,
controller: MenuController,
controllerAs: '$menuCtrl',
position: panelPosition,
animation: panelAnimation,
targetEvent: $event,
templateUrl: 'UIxMailViewRecipientMenu',
trapFocus: true,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false
};
$mdPanel.open(config)
.then(function(panelRef) {
vm.panel = panelRef;
// Automatically close panel when clicking inside of it
panelRef.panelEl.one('click', function() {
panelRef.close();
});
});
MenuController.$inject = ['mdPanelRef', '$state', '$mdToast'];
function MenuController(mdPanelRef, $state, $mdToast) {
this.onKeyDown = function($event) {
if ($event.which === 9) { // Tab
mdPanelRef.close();
}
};
this.newCard = function(recipient, addressbookId) {
var card = new Card({
pid: addressbookId,
c_cn: recipient.name,
emails: [{ value: recipient.email }]
});
card.$id().then(function(id) {
card.$save().then(function() {
// Show success toast when action succeeds
$mdToast.show(
$mdToast.simple()
.content(l('Successfully created card'))
.position('top right')
.hideDelay(2000));
});
});
mdPanelRef.close();
};
}
if (targetElement.tagName === 'A') {
$event.stopPropagation();
$event.preventDefault();
}
};
this.filterMailtoLinks = function($event) {
var href, match, to, cc, bcc, subject, body, data;
if ($event.target.tagName == 'A' && 'href' in $event.target.attributes) {
@ -382,8 +485,10 @@
};
this.newMessage = function($event, mailto) {
$event.stopPropagation();
$event.preventDefault();
if ($event.target.tagName === 'A') {
$event.stopPropagation();
$event.preventDefault();
}
this.account.$newMessage({ mailto: mailto }).then(function(message) {
_showMailEditor($event, message);
});

View File

@ -1,6 +1,11 @@
/// chips.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*-
@import 'extends';
$chip-dense-font-size: rem(1.2) !default;
$chip-dense-height: rem(2.4) !default;
$chip-dense-padding: 0 rem(0.8) 0 rem(0.8) !default;
$chip-dense-margin: rem(0.6) rem(0.6) 0 0 !default;
md-chips {
// Remove the line under the tags of the message viewer
&.sg-readonly {
@ -9,6 +14,9 @@ md-chips {
&.md-focused {
box-shadow: none;
}
md-chip-template:focus {
outline: 0;
}
.md-chip-content {
//max-width: initial; // fix bug in ng-material
}
@ -25,6 +33,15 @@ md-chips {
}
}
}
// Small, compact chip
&.sg-dense md-chip {
height: $chip-dense-height;
padding: $chip-dense-padding;
@include rtl(margin, $chip-dense-margin, rtl-value($chip-dense-margin));
font-size: $chip-dense-font-size;
line-height: $chip-dense-height;
}
.sg-chip-progress {
border-radius: $chip-height / 2;
bottom: 0;

View File

@ -61,6 +61,9 @@
padding-left: $mg;
padding-right: $mg;
}
md-chip {
cursor: pointer;
}
}
// Vertical buttons in header area of mail composer dialog