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

@ -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
@ -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];
}
@ -927,38 +1000,6 @@ static inline void _feedBlockWithDayBasedData (NSMutableDictionary *block, unsig
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
@ -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

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