See ChangeLog.
Monotone-Parent: 4822220bb92ce7ebcbd47af90d5aad37bb750ed0 Monotone-Revision: a7e7282f1911ceaff2a7f4f96320407cba1085cc Monotone-Author: flachapelle@inverse.ca Monotone-Date: 2011-10-03T19:32:37 Monotone-Branch: ca.inverse.sogomaint-2.0.2
parent
3234ee9a92
commit
10e3850d49
17
ChangeLog
17
ChangeLog
|
@ -1,3 +1,20 @@
|
|||
2011-10-03 Francis Lachapelle <flachapelle@inverse.ca>
|
||||
|
||||
* UI/WebServerResources/generic.js (snoozeAlarm): new function to
|
||||
snooze an alarm for a specific number of minutes.
|
||||
(showAlarmCallback): offer the user to snooze the alarm right
|
||||
after triggering the browser-native alert.
|
||||
(showSelectDialog): new function that construct a dialog box with
|
||||
a popup menu.
|
||||
|
||||
* UI/Scheduler/UIxAppointmentEditor.m (-viewAction): accepts the
|
||||
new form parameter snoozeAlarm, which delays the alarm to the
|
||||
specified number of minutes.
|
||||
|
||||
* SoObjects/Appointments/SOGoCalendarComponent.m (-snoozeAlarm:):
|
||||
new method to set the next alarm of the event in a specified
|
||||
number of minutes. Only affect the quick table, not the original vCalendar.
|
||||
|
||||
2011-09-30 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* OpenChange/MAPIStoreMailMessage.m (-getMessageData:inMemCtx:):
|
||||
|
|
2
NEWS
2
NEWS
|
@ -5,6 +5,8 @@ New Features
|
|||
creating an event or a task (selected, personal, first enabled)
|
||||
- new user defaults SOGoBusyOffHours to specify if off-hours should be
|
||||
automatically added to the free-busy information
|
||||
- new indicator in the link banner when a vacation message (auto-reply) is active
|
||||
- new snooze function for events alarms in Web interface
|
||||
|
||||
Enhancements
|
||||
- phone numbers in the contacts web module are now links (tel:)
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
2011-10-03 Francis Lachapelle <flachapelle@inverse.ca>
|
||||
|
||||
* GCSFolder.m (-updateQuickFields:whereColumn:isEqualTo:): new
|
||||
method to update some fields of the quick table matching the
|
||||
single specified condition.
|
||||
(-_quickTableEntity): the method was not returning all the fields.
|
||||
|
||||
2011-05-30 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* GCSFolder.m (-lastModificationDate): new method that returns the
|
||||
|
|
|
@ -126,6 +126,10 @@
|
|||
|
||||
- (NSException *) deleteFolder;
|
||||
|
||||
- (NSException *) updateQuickFields: (NSDictionary *) _fields
|
||||
whereColumn: (NSString *) _colname
|
||||
isEqualTo: (id) _value;
|
||||
|
||||
- (NSArray *) fetchFields: (NSArray *) _flds
|
||||
fetchSpecification: (EOFetchSpecification *) _fs;
|
||||
- (NSArray *) fetchFields: (NSArray *) fields
|
||||
|
|
|
@ -139,10 +139,13 @@ static NSArray *contentFieldNames = nil;
|
|||
folderManager:nil];
|
||||
}
|
||||
|
||||
- (id)initWithPath:(NSString *)_path primaryKey:(id)_folderId
|
||||
folderTypeName:(NSString *)_ftname folderType:(GCSFolderType *)_ftype
|
||||
location:(NSURL *)_loc quickLocation:(NSURL *)_qloc
|
||||
folderManager:(GCSFolderManager *)_fm
|
||||
- (id)initWithPath:(NSString *)_path
|
||||
primaryKey:(id)_folderId
|
||||
folderTypeName:(NSString *)_ftname
|
||||
folderType:(GCSFolderType *)_ftype
|
||||
location:(NSURL *)_loc
|
||||
quickLocation:(NSURL *)_qloc
|
||||
folderManager:(GCSFolderManager *)_fm
|
||||
{
|
||||
return [self initWithPath:_path primaryKey:_folderId folderTypeName:_ftname
|
||||
folderType:_ftype location:_loc quickLocation:_qloc
|
||||
|
@ -664,9 +667,9 @@ static NSArray *contentFieldNames = nil;
|
|||
}
|
||||
|
||||
- (NSString *)_generateUpdateStatementForRow:(NSDictionary *)_row
|
||||
tableName:(NSString *)_table
|
||||
whereColumn:(NSString *)_colname isEqualTo:(id)_value
|
||||
andColumn:(NSString *)_colname2 isEqualTo:(id)_value2
|
||||
tableName:(NSString *)_table
|
||||
whereColumn:(NSString *)_colname isEqualTo:(id)_value
|
||||
andColumn:(NSString *)_colname2 isEqualTo:(id)_value2
|
||||
{
|
||||
// TODO: move to NSDictionary category?
|
||||
NSMutableString *sql;
|
||||
|
@ -744,7 +747,23 @@ static NSArray *contentFieldNames = nil;
|
|||
|
||||
- (EOEntity *) _quickTableEntity
|
||||
{
|
||||
return [self _entityWithName: [self quickTableName]];
|
||||
EOEntity *entity;
|
||||
EOAttribute *attribute;
|
||||
GCSFieldInfo *field;
|
||||
NSEnumerator *fields;
|
||||
NSString *fieldName;
|
||||
|
||||
entity = [self _entityWithName: [self quickTableName]];
|
||||
fields = [quickFieldNames objectEnumerator];
|
||||
while ((fieldName = [fields nextObject]))
|
||||
{
|
||||
attribute = AUTORELEASE([[EOAttribute alloc] init]);
|
||||
[attribute setName: fieldName];
|
||||
[attribute setColumnName: fieldName];
|
||||
[entity addAttribute: attribute];
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
- (EOSQLQualifier *) _qualifierUsingWhereColumn:(NSString *)_colname
|
||||
|
@ -1171,6 +1190,36 @@ static NSArray *contentFieldNames = nil;
|
|||
return nil;
|
||||
}
|
||||
|
||||
- (NSException *) updateQuickFields: (NSDictionary *) _fields
|
||||
whereColumn: (NSString *) _colname
|
||||
isEqualTo: (id) _value
|
||||
{
|
||||
EOAdaptorChannel *quickChannel;
|
||||
NSException *error;
|
||||
|
||||
quickChannel = [self acquireQuickChannel];
|
||||
[[quickChannel adaptorContext] beginTransaction];
|
||||
error = [quickChannel updateRowX: _fields
|
||||
describedByQualifier: [self _qualifierUsingWhereColumn: _colname
|
||||
isEqualTo: _value andColumn: nil isEqualTo: nil
|
||||
entity: [self _quickTableEntity]]];
|
||||
|
||||
if (error)
|
||||
{
|
||||
[[quickChannel adaptorContext] rollbackTransaction];
|
||||
[self logWithFormat:
|
||||
@"ERROR(%s): cannot update content : %@", __PRETTY_FUNCTION__, error];
|
||||
}
|
||||
else
|
||||
{
|
||||
[[quickChannel adaptorContext] commitTransaction];
|
||||
}
|
||||
|
||||
[self releaseChannel: quickChannel];
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/* SQL generation */
|
||||
|
||||
/* fetching */
|
||||
|
|
|
@ -88,6 +88,8 @@
|
|||
- (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) component;
|
||||
- (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) recID;
|
||||
|
||||
- (void) snoozeAlarm: (unsigned int) minutes;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SOGOCALENDARCOMPONENT_H */
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSEnumerator.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSValue.h>
|
||||
|
||||
#import <NGObjWeb/NSException+HTTP.h>
|
||||
#import <NGObjWeb/SoSecurityManager.h>
|
||||
|
@ -41,6 +42,7 @@
|
|||
#import <NGMime/NGMimeBodyPart.h>
|
||||
#import <NGMime/NGMimeMultipartBody.h>
|
||||
#import <NGMail/NGMimeMessage.h>
|
||||
#import <GDLContentStore/GCSFolder.h>
|
||||
|
||||
#import <SOGo/NSCalendarDate+SOGo.h>
|
||||
#import <SOGo/NSDictionary+Utilities.h>
|
||||
|
@ -1211,6 +1213,29 @@ static inline BOOL _occurenceHasID (iCalRepeatableEntityObject *occurence,
|
|||
return roles;
|
||||
}
|
||||
|
||||
- (void) snoozeAlarm: (unsigned int) minutes
|
||||
{
|
||||
NSDictionary *quickFields;
|
||||
GCSFolder *folder;
|
||||
unsigned int nextAlarm;
|
||||
|
||||
folder = [[self container] ocsFolder];
|
||||
if (!folder)
|
||||
{
|
||||
[self errorWithFormat:@"(%s): missing folder for fetch!",
|
||||
__PRETTY_FUNCTION__];
|
||||
return;
|
||||
}
|
||||
|
||||
nextAlarm = [[NSCalendarDate calendarDate] timeIntervalSince1970] + minutes * 60;
|
||||
quickFields = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: nextAlarm]
|
||||
forKey: @"c_nextalarm"];
|
||||
|
||||
[folder updateQuickFields: quickFields
|
||||
whereColumn: @"c_name"
|
||||
isEqualTo: nameInContainer];
|
||||
}
|
||||
|
||||
/* SOGoComponentOccurence protocol */
|
||||
|
||||
- (iCalRepeatableEntityObject *) occurence
|
||||
|
|
|
@ -242,8 +242,7 @@
|
|||
NSString *webstatus;
|
||||
|
||||
anAlarm = [[self alarms] objectAtIndex: 0];
|
||||
if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"]
|
||||
== NSOrderedSame)
|
||||
if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame)
|
||||
{
|
||||
webstatus = [[anAlarm trigger] value: 0 ofAttribute: @"x-webstatus"];
|
||||
if (!webstatus
|
||||
|
|
|
@ -461,7 +461,8 @@
|
|||
NSTimeZone *timeZone;
|
||||
SOGoUserDefaults *ud;
|
||||
SOGoCalendarComponent *co;
|
||||
BOOL resetAlarm;
|
||||
BOOL resetAlarm;
|
||||
unsigned int snoozeAlarm;
|
||||
|
||||
[self event];
|
||||
|
||||
|
@ -480,17 +481,27 @@
|
|||
[componentCalendar retain];
|
||||
}
|
||||
|
||||
resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue];
|
||||
if (resetAlarm && [event hasAlarms] && ![event hasRecurrenceRules])
|
||||
if ([event hasAlarms] && ![event hasRecurrenceRules])
|
||||
{
|
||||
iCalAlarm *anAlarm;
|
||||
iCalTrigger *aTrigger;
|
||||
|
||||
anAlarm = [[event alarms] objectAtIndex: 0];
|
||||
aTrigger = [anAlarm trigger];
|
||||
[aTrigger setValue: 0 ofAttribute: @"x-webstatus" to: @"triggered"];
|
||||
|
||||
[co saveComponent: event];
|
||||
resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue];
|
||||
snoozeAlarm = [[[context request] formValueForKey: @"snoozeAlarm"] intValue];
|
||||
if (resetAlarm)
|
||||
{
|
||||
iCalTrigger *aTrigger;
|
||||
|
||||
anAlarm = [[event alarms] objectAtIndex: 0];
|
||||
aTrigger = [anAlarm trigger];
|
||||
[aTrigger setValue: 0 ofAttribute: @"x-webstatus" to: @"triggered"];
|
||||
|
||||
[co saveComponent: event];
|
||||
}
|
||||
else if (snoozeAlarm)
|
||||
{
|
||||
anAlarm = [[event alarms] objectAtIndex: 0];
|
||||
if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame)
|
||||
[co snoozeAlarm: snoozeAlarm];
|
||||
}
|
||||
}
|
||||
|
||||
data = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
|
|
|
@ -291,12 +291,13 @@ UL.choiceMenu LI._chosen:hover
|
|||
.menu LI.submenu
|
||||
{ background-image: url('arrow-right.png'); }
|
||||
|
||||
.menu LI[class~="disabled"].submenu
|
||||
{ background-image: url('submenu-disabled.gif') !important; }
|
||||
|
||||
.menu LI.submenu:hover, .menu LI.submenu-selected
|
||||
{ background-image: url('arrow-right.png') !important; }
|
||||
|
||||
.menu LI[class~="disabled"].submenu,
|
||||
.menu LI[class~="disabled"].submenu:hover
|
||||
{ background-image: url('submenu-disabled.gif') !important; }
|
||||
|
||||
DIV#logConsole
|
||||
{ position: absolute;
|
||||
overflow: auto;
|
||||
|
|
|
@ -28,7 +28,6 @@ var menus = new Array();
|
|||
var search = {};
|
||||
var sorting = {};
|
||||
var dialogs = {};
|
||||
var dialogActive = false;
|
||||
var dialogsStack = new Array();
|
||||
|
||||
var lastClickedRow = -1;
|
||||
|
@ -1291,19 +1290,33 @@ function triggerNextAlarm() {
|
|||
}
|
||||
}
|
||||
|
||||
function snoozeAlarm(url) {
|
||||
url += "?snoozeAlarm=" + this.value;
|
||||
triggerAjaxRequest(url, snoozeAlarmCallback);
|
||||
disposeDialog();
|
||||
}
|
||||
|
||||
function snoozeAlarmCallback(http) {
|
||||
if (http.readyState == 4
|
||||
&& http.status == 200) {
|
||||
refreshAlarms();
|
||||
}
|
||||
}
|
||||
|
||||
function showAlarm(url) {
|
||||
url = UserFolderURL + "Calendar/" + url + "/view?resetAlarm=yes";
|
||||
url = UserFolderURL + "Calendar/" + url + "/view";
|
||||
if (document.viewAlarmAjaxRequest) {
|
||||
document.viewAlarmAjaxRequest.aborted = true;
|
||||
document.viewAlarmAjaxRequest.abort();
|
||||
}
|
||||
document.viewAlarmAjaxRequest = triggerAjaxRequest(url, showAlarmCallback);
|
||||
document.viewAlarmAjaxRequest = triggerAjaxRequest(url + "?resetAlarm=yes", showAlarmCallback, url);
|
||||
}
|
||||
|
||||
function showAlarmCallback(http) {
|
||||
if (http.readyState == 4
|
||||
&& http.status == 200) {
|
||||
if (http.responseText.length) {
|
||||
var url = http.callbackData;
|
||||
var data = http.responseText.evalJSON(true);
|
||||
var msg = _("Reminder:") + " " + data["summary"] + "\n";
|
||||
if (data["startDate"]) {
|
||||
|
@ -1324,6 +1337,15 @@ function showAlarmCallback(http) {
|
|||
msg += "\n\n" + data["description"];
|
||||
|
||||
window.alert(msg);
|
||||
showSelectDialog(data["summary"], _('Snooze for '),
|
||||
{ '5': _('5 minutes'),
|
||||
'10': _('10 minutes'),
|
||||
'15': _('15 minutes'),
|
||||
'30': _('30 minutes'),
|
||||
'45': _('45 minutes'),
|
||||
'60': _('1 hour') }, _('OK'),
|
||||
snoozeAlarm, url,
|
||||
'10');
|
||||
}
|
||||
else
|
||||
log("showAlarmCallback ajax error: no data received");
|
||||
|
@ -1801,9 +1823,8 @@ function createButton(id, caption, action) {
|
|||
|
||||
function showAlertDialog(label) {
|
||||
var div = $("bgDialogDiv");
|
||||
if (div && div.visible() && div.getOpacity() > 0) {
|
||||
dialogsStack.push(label);
|
||||
}
|
||||
if (div && div.visible() && div.getOpacity() > 0)
|
||||
dialogsStack.push(_showAlertDialog.bind(this, label));
|
||||
else
|
||||
_showAlertDialog(label);
|
||||
}
|
||||
|
@ -1830,6 +1851,14 @@ function _showAlertDialog(label) {
|
|||
}
|
||||
|
||||
function showConfirmDialog(title, label, callbackYes, callbackNo) {
|
||||
var div = $("bgDialogDiv");
|
||||
if (div && div.visible() && div.getOpacity() > 0)
|
||||
dialogsStack.push(_showConfirmDialog.bind(this, title, label, callbackYes, callbackNo));
|
||||
else
|
||||
_showConfirmDialog(title, label, callbackYes, callbackNo);
|
||||
}
|
||||
|
||||
function _showConfirmDialog(title, label, callbackYes, callbackNo) {
|
||||
var key = title;
|
||||
if (Object.isElement(label)) key += label.allTextContent();
|
||||
else key += label;
|
||||
|
@ -1860,6 +1889,14 @@ function showConfirmDialog(title, label, callbackYes, callbackNo) {
|
|||
}
|
||||
|
||||
function showPromptDialog(title, label, callback, defaultValue) {
|
||||
var div = $("bgDialogDiv");
|
||||
if (div && div.visible() && div.getOpacity() > 0)
|
||||
dialogsStack.push(_showPromptDialog.bind(this, title, label, callback, defaultValue));
|
||||
else
|
||||
_showPromptDialog(title, label, callback, defaultValue);
|
||||
}
|
||||
|
||||
function _showPromptDialog(title, label, callback, defaultValue) {
|
||||
var dialog = dialogs[title+label];
|
||||
v = defaultValue?defaultValue:"";
|
||||
if (dialog) {
|
||||
|
@ -1887,8 +1924,54 @@ function showPromptDialog(title, label, callback, defaultValue) {
|
|||
document.body.appendChild(dialog);
|
||||
dialogs[title+label] = dialog;
|
||||
}
|
||||
dialog.appear({ duration: 0.2,
|
||||
afterFinish: function () { dialog.down("input").focus(); } });
|
||||
}
|
||||
|
||||
function showSelectDialog(title, label, options, button, callbackFcn, callbackArg, defaultValue) {
|
||||
var div = $("bgDialogDiv");
|
||||
if (div && div.visible() && div.getOpacity() > 0) {
|
||||
dialogsStack.push(_showSelectDialog.bind(this, title, label, options, button, callbackFcn, callbackArg, defaultValue));
|
||||
}
|
||||
else
|
||||
_showSelectDialog(title, label, options, button, callbackFcn, callbackArg, defaultValue);
|
||||
}
|
||||
|
||||
function _showSelectDialog(title, label, options, button, callbackFcn, callbackArg, defaultValue) {
|
||||
var dialog = dialogs[title+label];
|
||||
if (dialog) {
|
||||
$("bgDialogDiv").show();
|
||||
}
|
||||
else {
|
||||
var fields = createElement("p", null, []);
|
||||
fields.appendChild(document.createTextNode(label));
|
||||
var select = createElement("select"); //, null, null, { cname: name } );
|
||||
fields.appendChild(select);
|
||||
var values = $H(options).keys();
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var option = createElement("option", null, null,
|
||||
{ value: values[i] }, null, select);
|
||||
option.appendChild(document.createTextNode(options[values[i]]));
|
||||
}
|
||||
fields.appendChild(createElement("br"));
|
||||
|
||||
fields.appendChild(createButton(null,
|
||||
button,
|
||||
callbackFcn.bind(select, callbackArg)));
|
||||
fields.appendChild(createButton(null,
|
||||
_("Cancel"),
|
||||
disposeDialog));
|
||||
dialog = createDialog(null,
|
||||
title,
|
||||
null,
|
||||
fields,
|
||||
"none");
|
||||
document.body.appendChild(dialog);
|
||||
dialogs[title+label] = dialog;
|
||||
}
|
||||
if (defaultValue)
|
||||
defaultOption = dialog.down('option[value="'+defaultValue+'"]').selected = true;
|
||||
dialog.appear({ duration: 0.2 });
|
||||
dialog.down("input").focus();
|
||||
}
|
||||
|
||||
function disposeDialog() {
|
||||
|
@ -1898,9 +1981,9 @@ function disposeDialog() {
|
|||
});
|
||||
if (dialogsStack.length > 0) {
|
||||
// Show the next dialog box
|
||||
var label = dialogsStack.first();
|
||||
var dialogFcn = dialogsStack.first();
|
||||
dialogsStack.splice(0, 1);
|
||||
_showAlertDialog.delay(0.2, label);
|
||||
dialogFcn.delay(0.2);
|
||||
}
|
||||
else {
|
||||
var bgFade = Effect.Fade('bgDialogDiv', { duration: 0.2 });
|
||||
|
@ -1916,9 +1999,9 @@ function _disposeDialog(bgEffect) {
|
|||
bgEffect.cancel();
|
||||
div.show();
|
||||
div.appear({ duration: 0.2, to: 0.3 });
|
||||
var label = dialogsStack.first();
|
||||
var dialogFcn = dialogsStack.first();
|
||||
dialogsStack.splice(0, 1);
|
||||
_showAlertDialog(label);
|
||||
dialogFcn();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue