From 77f9f6027654c4036ec539c77f68ed3834cdda19 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Fri, 8 Sep 2017 13:24:58 -0400 Subject: [PATCH] New events list view --- NEWS | 1 + UI/SOGoUI/SOGoDirectAction.h | 3 + UI/SOGoUI/SOGoDirectAction.m | 28 ++ UI/Scheduler/UIxCalListingActions.m | 340 ++++++++++-------- UI/Templates/SchedulerUI/UIxCalMainView.wox | 69 ++-- .../js/Scheduler/Component.service.js | 27 +- .../sgCalendarListEvent.directive.js | 99 +++++ .../scss/core/typography.scss | 4 + .../scss/views/SchedulerUI.scss | 37 ++ 9 files changed, 419 insertions(+), 189 deletions(-) create mode 100644 UI/WebServerResources/js/Scheduler/sgCalendarListEvent.directive.js diff --git a/NEWS b/NEWS index 0913af4a0..f403f884f 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/UI/SOGoUI/SOGoDirectAction.h b/UI/SOGoUI/SOGoDirectAction.h index 27a0c65ae..1e1e46595 100644 --- a/UI/SOGoUI/SOGoDirectAction.h +++ b/UI/SOGoUI/SOGoDirectAction.h @@ -27,6 +27,9 @@ @class WOResponse, WOResourceManager; @interface SOGoDirectAction : WODirectAction +{ + NSDictionary *locale; +} - (WOResponse *) responseWithStatus: (unsigned int) status; - (WOResponse *) responseWithStatus: (unsigned int) status diff --git a/UI/SOGoUI/SOGoDirectAction.m b/UI/SOGoUI/SOGoDirectAction.m index 90bd6723a..99cca44fe 100644 --- a/UI/SOGoUI/SOGoDirectAction.m +++ b/UI/SOGoUI/SOGoDirectAction.m @@ -19,6 +19,7 @@ */ #import +#import /* for locale strings */ #import #import @@ -30,7 +31,9 @@ #import #import #import +#import #import +#import #import @@ -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; diff --git a/UI/Scheduler/UIxCalListingActions.m b/UI/Scheduler/UIxCalListingActions.m index b49ef9e05..9ef83f95a 100644 --- a/UI/Scheduler/UIxCalListingActions.m +++ b/UI/Scheduler/UIxCalListingActions.m @@ -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 Full month and year + * @apiSuccess (Success 200) {Object} events..days List of days + * @apiSuccess (Success 200) {String[]} events..days..month Abbreviated month name + * @apiSuccess (Success 200) {String[]} events..days..monthDay Day of month + * @apiSuccess (Success 200) {String[]} events..days..weekDay Abbreviated weeday name + * @apiSuccess (Success 200) {String[]} events..days..events List of events + * @apiSuccess (Success 200) {String} events..days..events.c_name Event UID + * @apiSuccess (Success 200) {String} events..days..events.c_folder Calendar ID + * @apiSuccess (Success 200) {String} events..days..events.calendarName Human readable name of calendar + * @apiSuccess (Success 200) {Number} events..days..events.c_status 0: Cancelled, 1: Normal, 2: Tentative + * @apiSuccess (Success 200) {Number} events..days..events.c_isopaque 1 if event is opaque (not transparent) + * @apiSuccess (Success 200) {String} events..days..events.c_title Title + * @apiSuccess (Success 200) {String} events..days..events.c_startdate Epoch time of start date + * @apiSuccess (Success 200) {String} events..days..events.c_enddate Epoch time of end date + * @apiSuccess (Success 200) {String} events..days..events.c_location Event's location + * @apiSuccess (Success 200) {Number} events..days..events.c_isallday 1 if event lasts all day + * @apiSuccess (Success 200) {Number} events..days..events.c_classification 0: Public, 1: Private, 2: Confidential + * @apiSuccess (Success 200) {String} events..days..events.c_category Category + * @apiSuccess (Success 200) {Number} events..days..events.c_priority Priority (0 to 9) + * @apiSuccess (Success 200) {String[]} events..days..events.c_partmails Participants email addresses + * @apiSuccess (Success 200) {String[]} events..days..events.c_partstates Participants states + * @apiSuccess (Success 200) {String} events..days..events.c_owner Event's owner + * @apiSuccess (Success 200) {Number} events..days..events.c_iscycle 1 if the event is cyclic/recurring + * @apiSuccess (Success 200) {Number} events..days..events.c_nextalarm Epoch time of next alarm + * @apiSuccess (Success 200) {String} [events..days..events.c_recurrence_id] Recurrence ID if event is cyclic + * @apiSuccess (Success 200) {Number} events..days..events.isException 1 if recurrence is an exception + * @apiSuccess (Success 200) {Number} events..days..events.viewable 1 if active user can view the event + * @apiSuccess (Success 200) {Number} events..days..events.editable 1 if active user can edit the event + * @apiSuccess (Success 200) {Number} events..days..events.erasable 1 if active user can erase the event + * @apiSuccess (Success 200) {Number} events..days..events.ownerIsOrganizer 1 if owner is also the organizer + * @apiSuccess (Success 200) {String} events..days..events.formatted_startdate Localized start date + * @apiSuccess (Success 200) {String} events..days..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]; } diff --git a/UI/Templates/SchedulerUI/UIxCalMainView.wox b/UI/Templates/SchedulerUI/UIxCalMainView.wox index b4ab6d7db..f068a7df5 100644 --- a/UI/Templates/SchedulerUI/UIxCalMainView.wox +++ b/UI/Templates/SchedulerUI/UIxCalMainView.wox @@ -383,40 +383,39 @@ - -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
- repeat - alarm -
- + ng-class="{ 'sg-list-selectable': list.mode.multiple }" + ng-repeat="(key, monthData) in list.component.$events"> + + - + +
+ + +
+
+
+
+
+ + + + +
+
+
+ +

@@ -447,8 +446,8 @@
- {{::task.c_priority}} - {{::task.c_title}} + +
diff --git a/UI/WebServerResources/js/Scheduler/Component.service.js b/UI/WebServerResources/js/Scheduler/Component.service.js index 7737c080a..69c66dc09 100644 --- a/UI/WebServerResources/js/Scheduler/Component.service.js +++ b/UI/WebServerResources/js/Scheduler/Component.service.js @@ -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 + ')'); diff --git a/UI/WebServerResources/js/Scheduler/sgCalendarListEvent.directive.js b/UI/WebServerResources/js/Scheduler/sgCalendarListEvent.directive.js new file mode 100644 index 000000000..3f6237f65 --- /dev/null +++ b/UI/WebServerResources/js/Scheduler/sgCalendarListEvent.directive.js @@ -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: + + + */ + 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 [ + '
', + // Categories color stripes + '
', + // Priority + ' ', + // Summary + ' {{ ::component.c_title }}', + ' ', + // Component is reccurent + ' ', + // Component has an alarm + ' ', + // Component is confidential + ' ', + // Component is private + ' ', + ' ', + // Time + '
', + ' access_time {{::component.starthour}}', + '
', + // Location + '
', + ' place {{::component.c_location}}', + '
', + '
' + ].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); +})(); diff --git a/UI/WebServerResources/scss/core/typography.scss b/UI/WebServerResources/scss/core/typography.scss index a113587cd..d4bf7a255 100644 --- a/UI/WebServerResources/scss/core/typography.scss +++ b/UI/WebServerResources/scss/core/typography.scss @@ -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; diff --git a/UI/WebServerResources/scss/views/SchedulerUI.scss b/UI/WebServerResources/scss/views/SchedulerUI.scss index 87574afbb..c2b318d81 100644 --- a/UI/WebServerResources/scss/views/SchedulerUI.scss +++ b/UI/WebServerResources/scss/views/SchedulerUI.scss @@ -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