New events list view

pull/218/merge
Francis Lachapelle 2017-09-08 13:24:58 -04:00
parent 614a137128
commit 77f9f60276
9 changed files with 419 additions and 189 deletions

1
NEWS
View File

@ -5,6 +5,7 @@ New features
- [core] can now invite attendees to exceptions only (#2561)
- [web] display freebusy information of owner in appointment editor
- [web] register SOGo as a handler for the mailto scheme (#1223)
- [web] new events list view where events are grouped by day
Enhancements
- [web] follow requested URL after user authentication

View File

@ -27,6 +27,9 @@
@class WOResponse, WOResourceManager;
@interface SOGoDirectAction : WODirectAction
{
NSDictionary *locale;
}
- (WOResponse *) responseWithStatus: (unsigned int) status;
- (WOResponse *) responseWithStatus: (unsigned int) status

View File

@ -19,6 +19,7 @@
*/
#import <Foundation/NSKeyValueCoding.h>
#import <Foundation/NSUserDefaults.h> /* for locale strings */
#import <NGObjWeb/SoObjects.h>
#import <NGObjWeb/WOContext+SoObjects.h>
@ -30,7 +31,9 @@
#import <SoObjects/SOGo/NSString+Utilities.h>
#import <SoObjects/SOGo/SOGoSession.h>
#import <SoObjects/SOGo/SOGoSystemDefaults.h>
#import <SoObjects/SOGo/SOGoUser.h>
#import <SoObjects/SOGo/SOGoWebAuthenticator.h>
#import <SoObjects/SOGo/WOResourceManager+SOGo.h>
#import <NGExtensions/NSObject+Logs.h>
@ -50,6 +53,31 @@ static SoProduct *commonProduct = nil;
}
}
- (id) initWithContext: (WOContext *)_context;
{
NSString *language;
SOGoUserDefaults *userDefaults;
WOResourceManager *resMgr;
if ((self = [super initWithContext: _context]))
{
userDefaults = [[_context activeUser] userDefaults];
if (!userDefaults)
userDefaults = [SOGoSystemDefaults sharedSystemDefaults];
language = [userDefaults language];
resMgr = [[WOApplication application] resourceManager];
ASSIGN (locale, [resMgr localeForLanguageNamed: language]);
}
return self;
}
- (void) dealloc
{
[locale release];
[super dealloc];
}
- (WOResponse *) responseWithStatus: (unsigned int) status
{
WOResponse *response;

View File

@ -104,7 +104,7 @@ static NSArray *tasksFields = nil;
- (id) initWithRequest: (WORequest *) newRequest
{
SOGoUser *user;
if ((self = [super initWithRequest: newRequest]))
{
componentsData = [NSMutableDictionary new];
@ -120,7 +120,7 @@ static NSArray *tasksFields = nil;
currentView = nil;
ASSIGN (enabledWeekDays, [[user userDefaults] calendarWeekdays]);
}
return self;
}
@ -139,7 +139,7 @@ static NSArray *tasksFields = nil;
{
NSCalendarDate *newDate;
NSString *param;
if ([popupValue isEqualToString: @"view_today"])
{
newDate = [NSCalendarDate calendarDate];
@ -204,10 +204,10 @@ static NSArray *tasksFields = nil;
{
SOGoUser *user;
NSString *param;
user = [context activeUser];
userLogin = [user login];
criteria = [request formValueForKey: @"search"];
value = [request formValueForKey: @"value"];
param = [request formValueForKey: @"filterpopup"];
@ -224,7 +224,7 @@ static NSArray *tasksFields = nil;
inTimeZone: userTimeZone] beginOfDay];
else
startDate = nil;
param = [request formValueForKey: @"ed"];
if ([param length] > 0)
endDate = [[NSCalendarDate dateFromShortDateString: param
@ -232,7 +232,7 @@ static NSArray *tasksFields = nil;
inTimeZone: userTimeZone] endOfDay];
else
endDate = nil;
param = [request formValueForKey: @"view"];
currentView = param;
dayBasedView = ![param isEqualToString: @"monthview"];
@ -243,7 +243,7 @@ static NSArray *tasksFields = nil;
withType: (NSString *) type
{
NSString *labelKey;
labelKey = [NSString stringWithFormat: @"%@_class%@",
type, [component objectForKey: @"c_classification"]];
[component setObject: [self labelForKey: labelKey inContext: context]
@ -264,23 +264,23 @@ static NSArray *tasksFields = nil;
unsigned int count;
static NSString *fields[] = { @"startDate", @"c_startdate", @"endDate", @"c_enddate" };
/* WARNING: This condition has been put and removed many times, please leave
it. Here is the story...
If _fixDates: is conditional to dayBasedView, the recurrences are computed
properly but the display time is wrong.
If _fixDates: is non-conditional, the reverse occurs.
If only this part of _fixDates: is conditional, both are right.
Regarding all day events, we need to execute this code no matter what the
date and the view are, otherwise the event will span on two days.
ref bugs:
http://www.sogo.nu/bugs/view.php?id=909
http://www.sogo.nu/bugs/view.php?id=678
...
*/
//NSLog(@"***[UIxCalListingActions _fixDates:] %@", [theRecord objectForKey: @"c_title"]);
if (dayBasedView || [[theRecord objectForKey: @"c_isallday"] boolValue])
{
@ -362,10 +362,10 @@ static NSArray *tasksFields = nil;
SOGoAppointmentFolder *currentFolder;
SOGoAppointmentFolders *clientObject;
SOGoUser *ownerUser;
BOOL isErasable, folderIsRemote, quickInfosFlag, searchByTitleOrContent;
int i;
infos = [NSMutableArray array];
marker = [NSNull null];
clientObject = [self clientObject];
@ -377,7 +377,7 @@ static NSArray *tasksFields = nil;
if ([currentFolder isActive])
{
folderIsRemote = [currentFolder isKindOfClass: [SOGoWebAppointmentFolder class]];
//
// criteria can have the following values: "title", "title_Category_Location" and "entireContent"
//
@ -400,7 +400,7 @@ static NSArray *tasksFields = nil;
additionalFilters: criteria];
searchByTitleOrContent = ([value length] > 0);
// Save the c_name in another array to compare with
if ([quickInfos count] > 0)
{
@ -409,7 +409,7 @@ static NSArray *tasksFields = nil;
for (i = 0; i < [quickInfos count]; i++)
[quickInfosName addObject:[[quickInfos objectAtIndex:i] objectForKey:@"c_name"]];
}
// Second research : Every objects except for those already in the quickInfos array
allInfos = (NSMutableArray *)[currentFolder fetchCoreInfosFrom: startDate
to: endDate
@ -438,7 +438,7 @@ static NSArray *tasksFields = nil;
}
}
}
currentInfos = [quickInfos objectEnumerator];
} // else if ([criteria isEqualToString:@"entireContent"])
else
@ -449,7 +449,7 @@ static NSArray *tasksFields = nil;
component: component]
objectEnumerator];
}
owner = [currentFolder ownerInContext: context];
ownerUser = [SOGoUser userWithLogin: owner];
isErasable = ([owner isEqualToString: userLogin] || [[currentFolder aclsForUser: userLogin] containsObject: SOGoRole_ObjectEraser]);
@ -511,7 +511,7 @@ static NSArray *tasksFields = nil;
// Identifies whether the owner is the organizer of this event.
NSString *c_orgmail;
c_orgmail = [newInfo objectForKey: @"c_orgmail"];
if ([c_orgmail isKindOfClass: [NSString class]] && [ownerUser hasEmail: c_orgmail])
[newInfo setObject: [NSNumber numberWithInt: 1]
forKey: @"ownerIsOrganizer"];
@ -603,9 +603,6 @@ static NSArray *tasksFields = nil;
}
else if (abs(delta = [date dayOfCommonEra] - [now dayOfCommonEra]) < 7)
{
WOResourceManager *resMgr = [[WOApplication application] resourceManager];
NSString *language = [[[context activeUser] userDefaults] language];
NSDictionary *locale = [resMgr localeForLanguageNamed: language];
NSString *dayOfWeek = [[locale objectForKey: NSWeekDayNameArray] objectAtIndex: [date dayOfWeek]];
if (delta < 7)
// Wihtin the next 7 days
@ -658,7 +655,7 @@ static NSArray *tasksFields = nil;
laterTime = browserTime + 60*60*48;
clientObject = [self clientObject];
allAlarms = [NSMutableArray array];
folders = [[clientObject subFolders] objectEnumerator];
while ((currentFolder = [folders nextObject]))
{
@ -667,14 +664,14 @@ static NSArray *tasksFields = nil;
NSDictionary *entry;
NSArray *alarms;
int i;
alarms = [currentFolder fetchAlarmInfosFrom: [NSNumber numberWithInt: browserTime]
to: [NSNumber numberWithInt: laterTime]];
for (i = 0; i < [alarms count]; i++)
{
entry = [alarms objectAtIndex: i];
[allAlarms addObject: [NSArray arrayWithObjects:
[currentFolder nameInContainer],
[entry objectForKey: @"c_name"],
@ -687,7 +684,7 @@ static NSArray *tasksFields = nil;
data = [NSDictionary dictionaryWithObjectsAndKeys:
[NSArray arrayWithObjects: @"c_folder", @"c_name", @"c_nextalarm", nil], @"fields",
allAlarms, @"alarms", nil];
return [self _responseWithData: data];
}
@ -696,7 +693,7 @@ static NSArray *tasksFields = nil;
NSString *filter;
SOGoUserSettings *us;
NSMutableDictionary *calendarSettings;
filter = [[context request] formValueForKey: @"filterpopup"];
if ([filter length]
&& ![filter isEqualToString: @"view_all"]
@ -721,7 +718,7 @@ static NSArray *tasksFields = nil;
NSString *sort, *ascending;
SOGoUserSettings *us;
NSMutableDictionary *calendarSettings;
sort = [[context request] formValueForKey: @"sort"];
ascending = [[context request] formValueForKey: @"asc"];
if ([sort length])
@ -740,6 +737,38 @@ static NSArray *tasksFields = nil;
}
}
static inline NSString* _userStateInEvent (NSArray *event)
{
unsigned int count, max;
NSString *state;
NSArray *participants, *states;
SOGoUser *user;
participants = nil;
state = nil;
participants = [event objectAtIndex: eventPartMailsIndex];
// We guard ourself against bogus value coming from the quick tables
if ([participants isKindOfClass: [NSArray class]])
{
states = [event objectAtIndex: eventPartStatesIndex];
count = 0;
max = [participants count];
user = [SOGoUser userWithLogin: [event objectAtIndex: eventOwnerIndex]
roles: nil];
while (state == nil && count < max)
{
if ([user hasEmail: [participants objectAtIndex: count]])
state = [states objectAtIndex: count];
else
count++;
}
}
return state;
}
/**
* @api {get} /so/:username/Calendar/eventslist List events
* @apiVersion 1.0.0
@ -755,34 +784,39 @@ static NSArray *tasksFields = nil;
* @apiParam {String} [search] Search field criteria. Either title_Category_Location or entireContent.
* @apiParam {String} [value] String to match
*
* @apiSuccess (Success 200) {String[]} fields List of fields for each event definition
* @apiSuccess (Success 200) {String[]} events List of events
* @apiSuccess (Success 200) {String} events.c_name Event UID
* @apiSuccess (Success 200) {String} events.c_folder Calendar ID
* @apiSuccess (Success 200) {String} events.calendarName Human readable name of calendar
* @apiSuccess (Success 200) {Number} events.c_status 0: Cancelled, 1: Normal, 2: Tentative
* @apiSuccess (Success 200) {Number} events.c_isopaque 1 if event is opaque (not transparent)
* @apiSuccess (Success 200) {String} events.c_title Title
* @apiSuccess (Success 200) {String} events.c_startdate Epoch time of start date
* @apiSuccess (Success 200) {String} events.c_enddate Epoch time of end date
* @apiSuccess (Success 200) {String} events.c_location Event's location
* @apiSuccess (Success 200) {Number} events.c_isallday 1 if event lasts all day
* @apiSuccess (Success 200) {Number} events.c_classification 0: Public, 1: Private, 2: Confidential
* @apiSuccess (Success 200) {String} events.c_category Category
* @apiSuccess (Success 200) {Number} events.c_priority Priority (0 to 9)
* @apiSuccess (Success 200) {String[]} events.c_partmails Participants email addresses
* @apiSuccess (Success 200) {String[]} events.c_partstates Participants states
* @apiSuccess (Success 200) {String} events.c_owner Event's owner
* @apiSuccess (Success 200) {Number} events.c_iscycle 1 if the event is cyclic/recurring
* @apiSuccess (Success 200) {Number} events.c_nextalarm Epoch time of next alarm
* @apiSuccess (Success 200) {String} [events.c_recurrence_id] Recurrence ID if event is cyclic
* @apiSuccess (Success 200) {Number} events.isException 1 if recurrence is an exception
* @apiSuccess (Success 200) {Number} events.viewable 1 if active user can view the event
* @apiSuccess (Success 200) {Number} events.editable 1 if active user can edit the event
* @apiSuccess (Success 200) {Number} events.erasable 1 if active user can erase the event
* @apiSuccess (Success 200) {Number} events.ownerIsOrganizer 1 if owner is also the organizer
* @apiSuccess (Success 200) {String} events.formatted_startdate Localized start date
* @apiSuccess (Success 200) {String} events.formatted_enddate Localized end date
* @apiSuccess (Success 200) {String[]} fields List of fields for each event definition
* @apiSuccess (Success 200) {String} events.<month>.month Full month and year
* @apiSuccess (Success 200) {Object} events.<month>.days List of days
* @apiSuccess (Success 200) {String[]} events.<month>.days.<date>.month Abbreviated month name
* @apiSuccess (Success 200) {String[]} events.<month>.days.<date>.monthDay Day of month
* @apiSuccess (Success 200) {String[]} events.<month>.days.<date>.weekDay Abbreviated weeday name
* @apiSuccess (Success 200) {String[]} events.<month>.days.<date>.events List of events
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_name Event UID
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_folder Calendar ID
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.calendarName Human readable name of calendar
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.c_status 0: Cancelled, 1: Normal, 2: Tentative
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.c_isopaque 1 if event is opaque (not transparent)
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_title Title
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_startdate Epoch time of start date
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_enddate Epoch time of end date
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_location Event's location
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.c_isallday 1 if event lasts all day
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.c_classification 0: Public, 1: Private, 2: Confidential
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_category Category
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.c_priority Priority (0 to 9)
* @apiSuccess (Success 200) {String[]} events.<month>.days.<date>.events.c_partmails Participants email addresses
* @apiSuccess (Success 200) {String[]} events.<month>.days.<date>.events.c_partstates Participants states
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.c_owner Event's owner
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.c_iscycle 1 if the event is cyclic/recurring
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.c_nextalarm Epoch time of next alarm
* @apiSuccess (Success 200) {String} [events.<month>.days.<date>.events.c_recurrence_id] Recurrence ID if event is cyclic
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.isException 1 if recurrence is an exception
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.viewable 1 if active user can view the event
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.editable 1 if active user can edit the event
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.erasable 1 if active user can erase the event
* @apiSuccess (Success 200) {Number} events.<month>.days.<date>.events.ownerIsOrganizer 1 if owner is also the organizer
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.formatted_startdate Localized start date
* @apiSuccess (Success 200) {String} events.<month>.days.<date>.events.formatted_enddate Localized end date
*/
- (WOResponse *) eventsListAction
{
@ -791,15 +825,18 @@ static NSArray *tasksFields = nil;
NSCalendarDate *date;
NSDictionary *data;
NSEnumerator *events;
NSMutableArray *fields, *newEvents, *newEvent;
NSString *sort, *ascending, *weekDay;
unsigned int interval;
NSMutableArray *fields, *dayEvents, *newEvent, *allDayEvents;
NSMutableDictionary *allEvents, *monthData, *monthEvents, *dayData;
NSString *sort, *ascending, *day, *weekDay, *month, *userState;
unsigned int interval, count, max;
SEL sortSelector;
[self _setupContext];
[self saveFilterValue: @"EventsFilterState"];
[self saveSortValue: @"EventsSortingState"];
newEvents = [NSMutableArray array];
allEvents = [NSMutableDictionary dictionary];
allDayEvents = [NSMutableArray array];
events = [[self _fetchFields: eventsFields
forComponentOfType: @"vevent"] objectEnumerator];
while ((oldEvent = [events nextObject]))
@ -813,6 +850,33 @@ static NSArray *tasksFields = nil;
if ([enabledWeekDays count] && ![enabledWeekDays containsObject: weekDay])
continue;
month = [date descriptionWithCalendarFormat: @"%Y%m" locale: locale];
day = [date shortDateString];
if (!(monthData = [allEvents objectForKey: month]))
{
monthEvents = [NSMutableDictionary dictionary];
monthData = [NSMutableDictionary
dictionaryWithObjectsAndKeys:
[date descriptionWithCalendarFormat: @"%B %Y" locale: locale], @"month",
monthEvents, @"days",
nil];
[allEvents setObject: monthData forKey: month];
}
monthEvents = [monthData objectForKey: @"days"];
if (!(dayData = [monthEvents objectForKey: day]))
{
dayEvents = [NSMutableArray array];
[allDayEvents addObject: dayEvents];
dayData = [NSMutableDictionary
dictionaryWithObjectsAndKeys:
[date descriptionWithCalendarFormat: @"%b" locale: locale], @"month",
[date descriptionWithCalendarFormat: @"%e" locale: locale], @"monthDay",
[date descriptionWithCalendarFormat: @"%a" locale: locale], @"weekDay",
dayEvents, @"events",
nil];
[monthEvents setObject: dayData forKey: day];
}
dayEvents = [dayData objectForKey: @"events"];
newEvent = [NSMutableArray arrayWithArray: oldEvent];
isAllDay = [[oldEvent objectAtIndex: eventIsAllDayIndex] boolValue];
[newEvent addObject: [self _formattedDateForSeconds: interval
@ -820,31 +884,40 @@ static NSArray *tasksFields = nil;
interval = [[oldEvent objectAtIndex: eventEndDateIndex] intValue];
[newEvent addObject: [self _formattedDateForSeconds: interval
forAllDay: isAllDay]];
[newEvents addObject: newEvent];
userState = _userStateInEvent(oldEvent);
if (userState != nil) [newEvent addObject: userState];
[dayEvents addObject: newEvent];
}
// Sort affects events of each day (but not the days)
sort = [[context request] formValueForKey: @"sort"];
if ([sort isEqualToString: @"title"])
[newEvents sortUsingSelector: @selector (compareEventsTitleAscending:)];
sortSelector = @selector (compareEventsTitleAscending:);
else if ([sort isEqualToString: @"end"])
[newEvents sortUsingSelector: @selector (compareEventsEndDateAscending:)];
sortSelector = @selector (compareEventsEndDateAscending:);
else if ([sort isEqualToString: @"location"])
[newEvents sortUsingSelector: @selector (compareEventsLocationAscending:)];
sortSelector = @selector (compareEventsLocationAscending:);
else if ([sort isEqualToString: @"calendarName"])
[newEvents sortUsingSelector: @selector (compareEventsCalendarNameAscending:)];
sortSelector = @selector (compareEventsCalendarNameAscending:);
else
[newEvents sortUsingSelector: @selector (compareEventsStartDateAscending:)];
sortSelector = @selector (compareEventsStartDateAscending:);
ascending = [[context request] formValueForKey: @"asc"];
if (![ascending boolValue])
[newEvents reverseArray];
max = [allDayEvents count];
for (count = 0; count < max; count++)
{
dayEvents = [allDayEvents objectAtIndex: count];
[dayEvents sortUsingSelector: sortSelector];
if (![ascending boolValue])
[dayEvents reverseArray];
}
// Fields names
fields = [NSMutableArray arrayWithArray: eventsFields];
[fields addObject: @"formatted_startdate"];
[fields addObject: @"formatted_enddate"];
[fields addObject: @"userState"];
data = [NSDictionary dictionaryWithObjectsAndKeys: fields, @"fields", newEvents, @"events", nil];
data = [NSDictionary dictionaryWithObjectsAndKeys: fields, @"fields", allEvents, @"events", nil];
return [self _responseWithData: data];
}
@ -853,7 +926,7 @@ static inline void _feedBlockWithDayBasedData (NSMutableDictionary *block, unsig
unsigned int end, unsigned int dayStart)
{
unsigned int delta, quarterStart, length, swap;
if (start > end)
{
swap = end;
@ -879,7 +952,7 @@ static inline void _feedBlockWithDayBasedData (NSMutableDictionary *block, unsig
// {
// NSCalendarDate *eventStartDate;
// NSString *startHour;
// eventStartDate = [NSCalendarDate dateWithTimeIntervalSince1970: start];
// [eventStartDate setTimeZone: userTimeZone];
// startHour = [dateFormatter formattedTime: eventStartDate];
@ -909,9 +982,9 @@ static inline void _feedBlockWithDayBasedData (NSMutableDictionary *block, unsig
userState: (NSString *) userState
{
NSMutableDictionary *block;
block = [NSMutableDictionary dictionary];
if (dayBasedView)
_feedBlockWithDayBasedData (block, start, end, dayStart);
// else
@ -923,42 +996,10 @@ static inline void _feedBlockWithDayBasedData (NSMutableDictionary *block, unsig
if (userState != nil)
[block setObject: userState
forKey: @"userState"];
return block;
}
static inline NSString* _userStateInEvent (NSArray *event)
{
unsigned int count, max;
NSString *state;
NSArray *participants, *states;
SOGoUser *user;
participants = nil;
state = nil;
participants = [event objectAtIndex: eventPartMailsIndex];
// We guard ourself against bogus value coming from the quick tables
if ([participants isKindOfClass: [NSArray class]])
{
states = [event objectAtIndex: eventPartStatesIndex];
count = 0;
max = [participants count];
user = [SOGoUser userWithLogin: [event objectAtIndex: eventOwnerIndex]
roles: nil];
while (state == nil && count < max)
{
if ([user hasEmail: [participants objectAtIndex: count]])
state = [states objectAtIndex: count];
else
count++;
}
}
return state;
}
- (void) _fillBlocks: (NSArray *) blocks
withEvent: (NSArray *) event
withNumber: (NSNumber *) number
@ -968,7 +1009,7 @@ static inline NSString* _userStateInEvent (NSArray *event)
NSMutableArray *currentDay;
NSMutableDictionary *eventBlock;
NSString *userState;
eventStart = [[event objectAtIndex: eventStartDateIndex] intValue];
if (eventStart < 0)
[self errorWithFormat: @"event '%@' has negative start: %d (skipped)",
@ -989,15 +1030,15 @@ static inline NSString* _userStateInEvent (NSArray *event)
[self warnWithFormat: @"event '%@' has end < start: %d < %d",
[event objectAtIndex: eventNameIndex], eventEnd, eventStart];
}
startSecs = (unsigned int) [startDate timeIntervalSince1970];
endsSecs = (unsigned int) [endDate timeIntervalSince1970];
if ([[event objectAtIndex: eventIsCycleIndex] boolValue])
recurrenceTime = [[event objectAtIndex: eventRecurrenceIdIndex] unsignedIntValue];
else
recurrenceTime = 0;
currentStart = eventStart;
if (currentStart < startSecs)
{
@ -1014,15 +1055,15 @@ static inline NSString* _userStateInEvent (NSArray *event)
{
currentDay = [blocks objectAtIndex: offset];
currentDayStart = startSecs + dayLength * offset;
if (eventEnd > endsSecs)
eventEnd = endsSecs;
if (eventEnd < startSecs)
// The event doesn't end in the covered period.
// This special case occurs with a DST change.
return;
userState = _userStateInEvent(event);
while (currentDayStart + dayLength < eventEnd)
{
@ -1038,14 +1079,14 @@ static inline NSString* _userStateInEvent (NSArray *event)
offset++;
currentDay = [blocks objectAtIndex: offset];
}
computedEventEnd = eventEnd;
// We add 5 mins to the end date of an event if the end date
// is equal or smaller than the event's start date.
if (eventEnd <= currentStart)
computedEventEnd = currentStart + (5*60);
eventBlock = [self _eventBlockWithStart: currentStart
end: computedEventEnd
number: number
@ -1063,7 +1104,7 @@ static inline NSString* _userStateInEvent (NSArray *event)
{
unsigned int count, nbrDays;
int seconds;
seconds = [endDate timeIntervalSinceDate: startDate];
if (seconds > 0)
{
@ -1089,11 +1130,11 @@ static inline NSString* _userStateInEvent (NSArray *event)
NSMutableArray *currentBlock, *blocks;
NSDictionary *currentEvent;
unsigned int count, max, qCount, qMax, qOffset;
blocks = [NSMutableArray array];
bzero (quarters, 96 * sizeof (NSMutableArray *));
max = [day count];
for (count = 0; count < max; count++)
{
@ -1112,7 +1153,7 @@ static inline NSString* _userStateInEvent (NSArray *event)
[currentBlock addObject: currentEvent];
}
}
return blocks;
}
@ -1120,7 +1161,7 @@ static inline unsigned int _computeMaxBlockSiblings (NSArray *block)
{
unsigned int count, max, maxSiblings, siblings;
NSNumber *nbrEvents;
max = [block count];
maxSiblings = max;
for (count = 0; count < max; count++)
@ -1133,7 +1174,7 @@ static inline unsigned int _computeMaxBlockSiblings (NSArray *block)
maxSiblings = siblings;
}
}
return maxSiblings;
}
@ -1142,7 +1183,7 @@ static inline void _propagateBlockSiblings (NSArray *block, NSNumber *maxSibling
unsigned int count, max;
NSMutableDictionary *event;
NSNumber *realSiblings;
max = [block count];
realSiblings = [NSNumber numberWithUnsignedInt: max];
for (count = 0; count < max; count++)
@ -1158,7 +1199,7 @@ static inline void _computeBlocksSiblings (NSArray *blocks)
{
NSArray *currentBlock;
unsigned int count, max, maxSiblings;
max = [blocks count];
for (count = 0; count < max; count++)
{
@ -1175,12 +1216,12 @@ static inline void _computeBlockPosition (NSArray *block)
NSNumber *position;
NSMutableDictionary *event;
NSMutableDictionary **positions;
max = [block count];
event = [block objectAtIndex: 0];
siblings = [[event objectForKey: @"siblings"] unsignedIntValue];
positions = NSZoneCalloc (NULL, siblings, sizeof (NSMutableDictionary *));
for (count = 0; count < max; count++)
{
event = [block objectAtIndex: count];
@ -1197,7 +1238,7 @@ static inline void _computeBlockPosition (NSArray *block)
forKey: @"position"];
}
}
NSZoneFree (NULL, positions);
}
@ -1242,7 +1283,7 @@ _computeBlocksPosition (NSArray *blocks)
NSArray *block;
unsigned int count, max;
// NSMutableDictionary **positions;
max = [blocks count];
for (count = 0; count < max; count++)
{
@ -1256,7 +1297,7 @@ _computeBlocksPosition (NSArray *blocks)
- (void) _addBlocksWidth: (NSMutableArray *) day
{
NSArray *blocks;
blocks = [self _horizontalBlocks: day];
_computeBlocksSiblings (blocks);
_computeBlocksSiblings (blocks);
@ -1273,7 +1314,7 @@ _computeBlocksPosition (NSArray *blocks)
NSString *fUID, *fName;
NSNumber *isActive;
unsigned int count, foldersCount;
co = [self clientObject];
folders = [co subFolders];
foldersCount = [folders count];
@ -1306,6 +1347,9 @@ _computeBlocksPosition (NSArray *blocks)
* @apiParam {Number} [ed] Period end date (YYYYMMDD)
* @apiParam {String} [view] Formatting view. Either dayview, multicolumndayview, weekview or monthview.
*
* @apiSuccess (Success 200) {Object[]} days
* @apiSuccess (Success 200) {String} days.date
* @apiSuccess (Success 200) {Number} days.number
* @apiSuccess (Success 200) {String[]} eventsFields List of fields for each event definition
* @apiSuccess (Success 200) {String[]} events List of events
* @apiSuccess (Success 200) {String} events.c_name Event UID
@ -1357,7 +1401,7 @@ _computeBlocksPosition (NSArray *blocks)
[self _setupContext];
events = [self _fetchFields: eventsFields forComponentOfType: @"vevent"];
if ([currentView isEqualToString: @"multicolumndayview"])
{
calendars = (NSMutableArray *)[self _selectedCalendars];
@ -1487,7 +1531,7 @@ _computeBlocksPosition (NSArray *blocks)
{
NSCalendarDate *taskDate;
NSString *statusClass;
if (statusCode == 1)
statusClass = @"completed";
else
@ -1509,7 +1553,7 @@ _computeBlocksPosition (NSArray *blocks)
else
statusClass = @"noduedate";
}
return statusClass;
}
@ -1558,29 +1602,29 @@ _computeBlocksPosition (NSArray *blocks)
SOGoUserSettings *us;
NSEnumerator *tasks;
NSArray *task;
unsigned int endDateStamp;
BOOL showCompleted;
int statusCode;
int startSecs;
filteredTasks = [NSMutableArray array];
[self _setupContext];
[self saveFilterValue: @"TasksFilterState"];
[self saveSortValue: @"TasksSortingState"];
startSecs = (unsigned int) [startDate timeIntervalSince1970];
tasksView = [request formValueForKey: @"filterpopup"];
showCompleted = [[request formValueForKey: @"show_completed"] intValue];
us = [[context activeUser] userSettings];
[us setBool: showCompleted forKey: @"ShowCompletedTasks"];
[us synchronize];
tasks = [[self _fetchFields: tasksFields
forComponentOfType: @"vtodo"] objectEnumerator];
while ((task = [tasks nextObject]))
{
statusCode = [[task objectAtIndex: taskStatusIndex] intValue];
@ -1635,7 +1679,7 @@ _computeBlocksPosition (NSArray *blocks)
[filteredTasks sortUsingSelector: @selector (compareTasksCalendarNameAscending:)];
else
[filteredTasks sortUsingSelector: @selector (compareTasksAscending:)];
ascending = [[context request] formValueForKey: @"asc"];
if (![ascending boolValue])
[filteredTasks reverseArray];
@ -1665,11 +1709,11 @@ _computeBlocksPosition (NSArray *blocks)
for (i = 0; i < [folders count]; i++)
{
folder = [folders objectAtIndex: i];
[activeTasksByCalendars setObject: [folder activeTasks]
forKey: [folder nameInContainer]];
}
return [self _responseWithData: activeTasksByCalendars];
}

View File

@ -383,40 +383,39 @@
<span ng-bind="list.filterpopup() | loc"><!-- active filter --></span>
</md-subheader>
<md-list class="sg-section-list"
ng-class="{ 'sg-list-selectable': list.mode.multiple }">
<md-list-item ng-repeat="event in list.component.$events"
aria-label="{{::event.c_title}}"
ng-click="list.openEvent($event, event)">
<div class="md-secondary sg-avatar-selectable"
label:aria-label="Toggle item"
ng-class="[event.getClassName('fg'), { 'sg-avatar-selected' : event.selected }]"
ng-click="list.toggleComponentSelection($event, event)">
<div class="sg-color-chip"
ng-class="::event.getClassName('bg')"><!-- calendar color --></div>
</div>
<div class="sg-category"
ng-repeat="category in ::event.categories | limitTo:5"
ng-class="::'bg-category' + category"
ng-style="::{ left: ($index * 3) + 'px' }"><!-- calendar color --></div>
<div class="sg-tile-content">
<div class="sg-md-subhead">
<div>
<span ng-show="::event.c_priority" class="sg-priority" ng-bind="::event.c_priority"><!-- priority --></span>
<span ng-bind="::event.c_title"><!-- title --></span>
</div>
</div>
<div class="sg-md-body">
<div ng-bind="::event.c_location"><!-- location --></div>
<div class="sg-tile-date" ng-bind="::event.formatted_startdate"><!-- start --></div>
</div>
</div>
<div class="sg-tile-icons">
<md-icon ng-show="::event.c_iscycle">repeat</md-icon>
<md-icon ng-show="::event.c_nextalarm">alarm</md-icon>
</div>
<md-divider><!-- divider --></md-divider>
ng-class="{ 'sg-list-selectable': list.mode.multiple }"
ng-repeat="(key, monthData) in list.component.$events">
<md-list-item aria-label="{{::monthData.month}}"
md-colors="::{ color: 'default-primary-800' }">
<span ng-bind="::monthData.month"><!-- month name --></span>
</md-list-item>
<md-list-item disabled="disabled" ng-if="list.component.$events.length == 0">
<md-list-item layout="row" layout-align="start start"
ng-repeat="dayData in monthData.days">
<div class="sg-calendar-date">
<span class="sg-calendar-day sg-md-display-1--narrow"
md-colors="::{ color: 'default-primary-800' }"
ng-bind="::dayData.monthDay"><!-- month day --></span>
<span>
<div md-colors="::{ color: 'default-primary-400' }"
ng-bind="::dayData.weekDay"><!-- week day --></div>
<div class="md-caption"
md-colors="::{ color: 'default-primary-800' }"
ng-bind="::dayData.month"><!-- month --></div>
</span>
</div>
<div class="sg-calendar-list md-flex">
<sg-calendar-list-event
ng-repeat="event in dayData.events"
sg-component="event"
sg-click="list.openEvent($event, clickComponent)">
<!-- directive -->
</sg-calendar-list-event>
<md-divider><!-- divider --></md-divider>
</div>
</md-list-item>
</md-list>
<md-list ng-if="!list.component.$events">
<md-list-item disabled="disabled">
<p class="sg-md-caption"><var:string label:value="No events for selected criteria"/></p>
</md-list-item>
</md-list>
@ -447,8 +446,8 @@
<div class="sg-tile-content">
<div class="sg-md-subhead">
<div>
<span ng-show="::task.c_priority" class="sg-priority">{{::task.c_priority}}</span>
{{::task.c_title}}
<span ng-show="::task.c_priority" class="sg-priority" ng-bind="::task.c_priority"><!-- priority --></span>
<span ng-bind="::task.c_title"><!-- title --></span>
</div>
</div>
<div class="sg-md-body">

View File

@ -419,12 +419,27 @@
fields.splice(_.indexOf(fields, 'c_recurrence_id'), 1, 'occurrenceId');
// Instanciate Component objects
_.reduce(data[type], function(components, componentData, i) {
var data = _.zipObject(fields, componentData), component;
component = new Component(data);
components.push(component);
return components;
}, components);
if (type == 'events') {
_.forEach(data[type], function(monthData, month) {
_.forEach(monthData.days, function(dayData, day) {
_.forEach(dayData.events, function(componentData, i) {
var data = _.zipObject(fields, componentData), component;
component = new Component(data);
dayData.events[i] = component;
});
});
});
components = data[type];
}
else if (type == 'tasks') {
_.reduce(data[type], function(components, componentData, i) {
var data = _.zipObject(fields, componentData), component;
component = new Component(data);
components.push(component);
return components;
}, components);
}
Component.$log.debug('list of ' + type + ' ready (' + components.length + ')');

View File

@ -0,0 +1,99 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
(function() {
'use strict';
/*
* sgCalendarListEvent - An event block to be displayed in a list
* @memberof SOGo.SchedulerUI
* @restrict element
* @param {object} sgComponent - the Component object.
* @param {function} sgClick - the function to call when clicking on the event.
* Two variables are available: clickEvent (the event that triggered the mouse click),
* and clickComponent (a Component object)
*
* @example:
<sg-calendar-list-event
ng-repeat="event in dayData.events"
sg-component="event"
sg-click="list.openEvent($event, clickComponent)" />
*/
sgCalendarListEvent.$inject = ['CalendarSettings'];
function sgCalendarListEvent(CalendarSettings) {
return {
restrict: 'E',
scope: {
component: '=sgComponent',
clickComponent: '&sgClick'
},
replace: true,
template: template,
link: link
};
function template(tElem, tAttrs) {
return [
'<div class="sg-event"',
' ng-click="clickComponent({clickEvent: $event, clickComponent: component})">',
// Categories color stripes
' <div class="sg-category" ng-repeat="category in ::component.categories"',
' ng-class="::(\'bg-category\' + category)"',
' ng-style="::{ right: ($index * 3) + \'px\' }"></div>',
// Priority
' <span ng-show="::component.c_priority" class="sg-priority" ng-bind="::component.c_priority"></span>',
// Summary
' {{ ::component.c_title }}',
' <span class="icons">',
// Component is reccurent
' <md-icon ng-if="::component.occurrenceId" class="material-icons icon-repeat"></md-icon>',
// Component has an alarm
' <md-icon ng-if="::component.c_nextalarm" class="material-icons icon-alarm"></md-icon>',
// Component is confidential
' <md-icon ng-if="::component.c_classification == 2" class="material-icons icon-visibility-off"></md-icon>',
// Component is private
' <md-icon ng-if="::component.c_classification == 1" class="material-icons icon-vpn-key"></md-icon>',
' </span>',
// Time
' <div class="secondary" ng-if="::!component.c_isallday">',
' <md-icon>access_time</md-icon> {{::component.starthour}}',
' </div>',
// Location
' <div class="secondary" ng-if="::component.c_location">',
' <md-icon>place</md-icon> {{::component.c_location}}',
' </div>',
'</div>'
].join('');
}
function link(scope, iElement, attrs) {
/**
* No data binding here since the view is completely redraw when
* a change is detected.
*/
if (scope.component.viewable)
iElement.addClass('md-clickable');
// Add class for user's participation state
if (scope.component.userstate)
iElement.addClass('sg-event--' + scope.component.userstate);
// Set background color
iElement.addClass('bg-folder' + scope.component.pid);
iElement.addClass('contrast-bdr-folder' + scope.component.pid);
// Add class for transparency
if (scope.component.c_isopaque === 0)
iElement.addClass('sg-event--transparent');
// Add class for cancelled event
if (scope.component.c_status === 0)
iElement.addClass('sg-event--cancelled');
}
}
angular
.module('SOGo.SchedulerUI')
.directive('sgCalendarListEvent', sgCalendarListEvent);
})();

View File

@ -337,6 +337,10 @@ html p {
line-height: $lineHeight;
font-weight: $sg-font-regular;
}
.#{$md}-display-1--narrow {
@extend .#{$md}-display-1;
letter-spacing: -0.1em;
}
.#{$md}-display-2 {
$lineHeight: $sg-line-height-7;
font-size: $sg-font-size-7;

View File

@ -494,6 +494,43 @@ $quarter_height: 10px;
.ghostEndHour {
bottom: -14px;
}
}
// Middle list view of events
.view-list {
.sg-calendar-date {
white-space: nowrap;
width: 72px;
min-width: 72px;
> * {
display: inline-block;
}
}
.sg-calendar-day {
font-weight: 200;
padding-right: 3px;
}
.sg-calendar-list {
padding-bottom: 16px;
md-divider {
margin-bottom: 8px;
}
}
.sg-event {
margin: 0 0 4px 0;
padding: $bl;
cursor: pointer;
position: relative;
.eventInside {
overflow: auto;
}
}
.text {
position: relative;
overflow: auto;
}
}
// Multicolumn day cell that contains the calendar name