diff --git a/ChangeLog b/ChangeLog index 8748a06d8..10e3ddbfe 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,20 @@ +2011-10-03 Francis Lachapelle + + * 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 * OpenChange/MAPIStoreMailMessage.m (-getMessageData:inMemCtx:): diff --git a/NEWS b/NEWS index 20f0a6601..ae95b1d51 100644 --- a/NEWS +++ b/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:) diff --git a/SOPE/GDLContentStore/ChangeLog b/SOPE/GDLContentStore/ChangeLog index d570302cb..b22ce9cb4 100644 --- a/SOPE/GDLContentStore/ChangeLog +++ b/SOPE/GDLContentStore/ChangeLog @@ -1,3 +1,10 @@ +2011-10-03 Francis Lachapelle + + * 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 * GCSFolder.m (-lastModificationDate): new method that returns the diff --git a/SOPE/GDLContentStore/GCSFolder.h b/SOPE/GDLContentStore/GCSFolder.h index d10185a7e..4baf3f1dd 100644 --- a/SOPE/GDLContentStore/GCSFolder.h +++ b/SOPE/GDLContentStore/GCSFolder.h @@ -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 diff --git a/SOPE/GDLContentStore/GCSFolder.m b/SOPE/GDLContentStore/GCSFolder.m index 95eca6f52..8a4be357a 100644 --- a/SOPE/GDLContentStore/GCSFolder.m +++ b/SOPE/GDLContentStore/GCSFolder.m @@ -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 */ diff --git a/SoObjects/Appointments/SOGoCalendarComponent.h b/SoObjects/Appointments/SOGoCalendarComponent.h index c20bf8b5a..d08389f56 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.h +++ b/SoObjects/Appointments/SOGoCalendarComponent.h @@ -88,6 +88,8 @@ - (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) component; - (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) recID; +- (void) snoozeAlarm: (unsigned int) minutes; + @end #endif /* SOGOCALENDARCOMPONENT_H */ diff --git a/SoObjects/Appointments/SOGoCalendarComponent.m b/SoObjects/Appointments/SOGoCalendarComponent.m index 5e06a17ba..65935d164 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.m +++ b/SoObjects/Appointments/SOGoCalendarComponent.m @@ -24,6 +24,7 @@ #import #import #import +#import #import #import @@ -41,6 +42,7 @@ #import #import #import +#import #import #import @@ -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 diff --git a/SoObjects/Appointments/iCalEvent+SOGo.m b/SoObjects/Appointments/iCalEvent+SOGo.m index 089592dba..4cb974c1e 100644 --- a/SoObjects/Appointments/iCalEvent+SOGo.m +++ b/SoObjects/Appointments/iCalEvent+SOGo.m @@ -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 diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index d7c9a8b3c..670ae5a8c 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -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: diff --git a/UI/WebServerResources/generic.css b/UI/WebServerResources/generic.css index 6ef29e1a5..5ef5cd2d6 100644 --- a/UI/WebServerResources/generic.css +++ b/UI/WebServerResources/generic.css @@ -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; diff --git a/UI/WebServerResources/generic.js b/UI/WebServerResources/generic.js index d37532b50..5791e13c1 100644 --- a/UI/WebServerResources/generic.js +++ b/UI/WebServerResources/generic.js @@ -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(); } }