From dc91be3c0d3e68cc1bd140ba76f9df3f6483c1c5 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Wed, 20 May 2015 11:11:01 -0400 Subject: [PATCH] Initial recurrence editor for appointments --- .../English.lproj/Localizable.strings | 7 + UI/Scheduler/UIxCalMainView.m | 5 +- UI/Scheduler/UIxComponentEditor.h | 1 - UI/Scheduler/UIxComponentEditor.m | 67 ------- UI/Scheduler/UIxRecurrenceEditor.m | 82 +++++++-- .../SchedulerUI/UIxRecurrenceEditor.wox | 163 ++++++++++++++---- .../js/Scheduler/Component.service.js | 118 ++++++++++--- .../js/Scheduler/ComponentController.js | 14 +- .../scss/components/content/content.scss | 6 + .../scss/components/input/_extends.scss | 2 + .../scss/components/input/input.scss | 99 +---------- .../components/pseudo-input/pseudo-input.scss | 4 - .../scss/components/select/select.scss | 7 + 13 files changed, 330 insertions(+), 245 deletions(-) create mode 100644 UI/WebServerResources/scss/components/input/_extends.scss diff --git a/UI/Scheduler/English.lproj/Localizable.strings b/UI/Scheduler/English.lproj/Localizable.strings index 2ea884eb6..f7c274d25 100644 --- a/UI/Scheduler/English.lproj/Localizable.strings +++ b/UI/Scheduler/English.lproj/Localizable.strings @@ -128,6 +128,8 @@ "Attach" = "Attach"; "Update" = "Update"; "Cancel" = "Cancel"; +"Reset" = "Reset"; +"Save" = "Save"; "show_rejected_apts" = "Show rejected appointments"; "hide_rejected_apts" = "Hide rejected appointments"; @@ -337,6 +339,11 @@ "appointment(s)" = "appointment(s)"; "Repeat until" = "Repeat until"; +"End Repeat" = "End Repeat"; +"Never" = "Never"; +"After" = "After"; +"On Date" = "On Date"; + "First" = "First"; "Second" = "Second"; "Third" = "Third"; diff --git a/UI/Scheduler/UIxCalMainView.m b/UI/Scheduler/UIxCalMainView.m index 5c70e6263..33894af98 100644 --- a/UI/Scheduler/UIxCalMainView.m +++ b/UI/Scheduler/UIxCalMainView.m @@ -359,10 +359,9 @@ if (!repeatItems) { - repeatItems = [NSArray arrayWithObjects: @"daily", + repeatItems = [NSArray arrayWithObjects: @"never", + @"daily", @"weekly", - @"bi-weekly", - @"every_weekday", @"monthly", @"yearly", nil]; diff --git a/UI/Scheduler/UIxComponentEditor.h b/UI/Scheduler/UIxComponentEditor.h index a9744ef30..5e984faef 100644 --- a/UI/Scheduler/UIxComponentEditor.h +++ b/UI/Scheduler/UIxComponentEditor.h @@ -29,7 +29,6 @@ @interface UIxComponentEditor : UIxComponent { - id item; iCalRepeatableEntityObject *component; SOGoAppointmentFolder *componentCalendar; } diff --git a/UI/Scheduler/UIxComponentEditor.m b/UI/Scheduler/UIxComponentEditor.m index f0cb47377..eb01fe635 100644 --- a/UI/Scheduler/UIxComponentEditor.m +++ b/UI/Scheduler/UIxComponentEditor.m @@ -242,59 +242,6 @@ static NSArray *reminderValues = nil; // return [comment stringByReplacingString: @"\n" withString: @"\r\n"]; //} -// TODO: Expose this method to the JSON API or centralize in UIxPreferences -- (NSArray *) categoryList -{ - NSMutableArray *categoryList; - NSArray *categoryLabels; - SOGoUserDefaults *defaults; - - defaults = [[context activeUser] userDefaults]; - categoryLabels = [defaults calendarCategories]; - if (!categoryLabels) - categoryLabels = [[self labelForKey: @"category_labels"] - componentsSeparatedByString: @","]; - categoryList = [NSMutableArray arrayWithCapacity: [categoryLabels count] + 1]; - [categoryList addObjectsFromArray: - [categoryLabels sortedArrayUsingSelector: - @selector (localizedCaseInsensitiveCompare:)]]; - - return categoryList; -} - -//- (NSArray *) repeatList -//{ -// static NSArray *repeatItems = nil; -// -// if (!repeatItems) -// { -// repeatItems = [NSArray arrayWithObjects: @"DAILY", -// @"WEEKLY", -// @"BI-WEEKLY", -// @"EVERY WEEKDAY", -// @"MONTHLY", -// @"YEARLY", -// @"-", -// @"CUSTOM", -// nil]; -// [repeatItems retain]; -// } -// -// return repeatItems; -//} - -//- (NSString *) repeatLabel -//{ -// NSString *rc; -// -// if ([self repeat]) -// rc = [self labelForKey: [NSString stringWithFormat: @"repeat_%@", [self repeat]]]; -// else -// rc = [self labelForKey: @"repeat_NEVER"]; -// -// return rc; -//} - //- (NSString *) reminder //{ // if ([[self clientObject] isNew]) @@ -394,20 +341,6 @@ static NSArray *reminderValues = nil; // return priorities; //} -//- (NSArray *) classificationClasses -//{ -// static NSArray *classes = nil; -// -// if (!classes) -// { -// classes = [NSArray arrayWithObjects: @"PUBLIC", -// @"CONFIDENTIAL", @"PRIVATE", nil]; -// [classes retain]; -// } -// -// return classes; -//} - /* helpers */ //- (BOOL) isWriteableClientObject diff --git a/UI/Scheduler/UIxRecurrenceEditor.m b/UI/Scheduler/UIxRecurrenceEditor.m index d4449b40a..f423fbbe5 100644 --- a/UI/Scheduler/UIxRecurrenceEditor.m +++ b/UI/Scheduler/UIxRecurrenceEditor.m @@ -1,8 +1,6 @@ /* UIxRecurrenceEditor.m - this file is part of SOGo * - * Copyright (C) 2008 Inverse inc. - * - * Author: Ludovic Marcotte + * Copyright (C) 2015 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,11 +19,14 @@ */ #import +#import #import #import /* for locale string constants */ #import +#import + #import "UIxRecurrenceEditor.h" @implementation UIxRecurrenceEditor @@ -64,7 +65,7 @@ if (!monthlyDayList) { monthlyDayList = [NSMutableArray arrayWithArray: [locale objectForKey: NSWeekDayNameArray]]; - [monthlyDayList addObject: @"DayOfTheMonth"]; + [monthlyDayList addObject: [self labelForKey: @"DayOfTheMonth"]]; [monthlyDayList retain]; } @@ -77,7 +78,7 @@ if (!yearlyMonthList) { - yearlyMonthList = [locale objectForKey: NSMonthNameArray]; + yearlyMonthList = [locale objectForKey: NSShortMonthNameArray]; [yearlyMonthList retain]; } @@ -102,18 +103,27 @@ // - (NSArray *) repeatList { - static NSArray *repeatList = nil; + static NSArray *repeatItems = nil; - if (!repeatList) + if (!repeatItems) { - repeatList = [NSArray arrayWithObjects: @"Daily", @"Weekly", - @"Monthly", @"Yearly", nil]; - [repeatList retain]; + repeatItems = [NSArray arrayWithObjects: @"daily", + @"weekly", + @"bi-weekly", + @"every_weekday", + @"monthly", + @"yearly", + nil]; + [repeatItems retain]; } - return repeatList; + return repeatItems; } +- (NSString *) itemRepeatText +{ + return [self labelForKey: [NSString stringWithFormat: @"repeat_%@", [item uppercaseString]]]; +} // // Accessors @@ -158,4 +168,54 @@ return item; } +- (NSString *) valueForWeekDay +{ + unsigned int i; + + i = [[self shortWeekDaysList] indexOfObject: item]; + + return iCalWeekDayString[i]; +} + +- (NSString *) valueForMonthlyRepeat +{ + static NSArray *monthlyRepeatValues = nil; + unsigned int i; + + if (!monthlyRepeatValues) + { + monthlyRepeatValues = [NSArray arrayWithObjects: @"1", @"2", @"3", + @"4", @"5", @"-1", nil]; + [monthlyRepeatValues retain]; + } + i = [[self monthlyRepeatList] indexOfObject: item]; + + return [monthlyRepeatValues objectAtIndex: i]; +} + +- (NSString *) valueForMonthlyDay +{ + unsigned int i; + + i = [[self monthlyDayList] indexOfObject: item]; + if (i % 7 != i) + return @""; + else + return iCalWeekDayString[i]; +} + +- (unsigned int) valueForYearlyMonth +{ + return [[self yearlyMonthList] indexOfObject: item] + 1; +} + +- (NSString *) valueForYearlyDay +{ + unsigned int i; + + i = [[self yearlyDayList] indexOfObject: item]; + + return iCalWeekDayString[i]; +} + @end diff --git a/UI/Templates/SchedulerUI/UIxRecurrenceEditor.wox b/UI/Templates/SchedulerUI/UIxRecurrenceEditor.wox index c58733c8e..9d602e931 100644 --- a/UI/Templates/SchedulerUI/UIxRecurrenceEditor.wox +++ b/UI/Templates/SchedulerUI/UIxRecurrenceEditor.wox @@ -1,28 +1,136 @@ - - + + +
+ + + + + +
- -
+ +
+
+ + + + + +
+ + + + + +
+ + +
+
+ + + + + +
+ + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + +
+ + + + + + + + + + + + + +
+
+
+ + +
+
+ + + + + +
+ + + + + +
+ + + + + + + + + + + + + +
+
+ + - -
- - -
-
+ diff --git a/UI/WebServerResources/js/Scheduler/Component.service.js b/UI/WebServerResources/js/Scheduler/Component.service.js index 6b7c8f6f3..d662f3c59 100644 --- a/UI/WebServerResources/js/Scheduler/Component.service.js +++ b/UI/WebServerResources/js/Scheduler/Component.service.js @@ -48,14 +48,13 @@ * @desc Factory registration of Component in Angular module. */ angular.module('SOGo.SchedulerUI') - /* Factory registration in Angular module */ .factory('Component', Component.$factory); /** * @function $filter * @memberof Component.prototype * @desc Search for components matching some criterias - * @param {string} type - Either 'events' or 'tasks' + * @param {string} type - either 'events' or 'tasks' * @param {object} [options] - additional options to the query * @returns a collection of Components instances */ @@ -82,11 +81,11 @@ }; /** - * @memberof Card - * @desc Fetch a card from a specific addressbook. - * @param {string} addressbook_id - the addressbook ID - * @param {string} card_id - the card ID - * @see {@link AddressBook.$getCard} + * @function $find + * @desc Fetch a component from a specific calendar. + * @param {string} calendarId - the calendar ID + * @param {string} componentId - the component ID + * @see {@link Calendar.$getComponent} */ Component.$find = function(calendarId, componentId) { var futureComponentData = this.$$resource.fetch([calendarId, componentId].join('/'), 'view'); @@ -96,7 +95,6 @@ /** * @function filterCategories - * @memberof Component.prototype * @desc Search for categories matching some criterias * @param {string} search - the search string to match * @returns a collection of strings @@ -110,7 +108,6 @@ /** * @function $eventsBlocksForView - * @memberof Component.prototype * @desc Events blocks for a specific week * @param {string} view - Either 'day' or 'week' * @param {Date} type - Date of any day of the desired period @@ -146,7 +143,6 @@ /** * @function $eventsBlocks - * @memberof Component.prototype * @desc Events blocks for a specific view and period * @param {string} view - Either 'day' or 'week' * @param {Date} startDate - period's start date @@ -197,9 +193,9 @@ }; /** - * @function $unwrap - * @memberof Comonent.prototype + * @function $unwrapCollection * @desc Unwrap a promise and instanciate new Component objects using received data. + * @param {string} type - either 'events' or 'tasks' * @param {promise} futureComponentData - a promise of the components' metadata * @returns a promise of the HTTP operation */ @@ -241,10 +237,59 @@ */ Component.prototype.init = function(data) { this.categories = []; + this.repeat = {}; angular.extend(this, data); + + // Parse recurrence rule definition and initialize default values + if (this.repeat.days) { + var byDayMask = _.find(this.repeat.days, function(o) { + return angular.isDefined(o.occurrence); + }); + if (byDayMask) + if (this.repeat.frequency == 'yearly') + this.repeat.year = { byday: true }; + this.repeat.month = { + type: 'byday', + occurrence: byDayMask.occurrence.toString(), + day: byDayMask.day + }; + } + else { + this.repeat.days = []; + } + if (angular.isUndefined(this.repeat.interval)) + this.repeat.interval = 1; + if (angular.isUndefined(this.repeat.month)) + this.repeat.month = { occurrence: '1', day: 'SU', type: 'bymonthday' }; + if (angular.isUndefined(this.repeat.monthdays)) + this.repeat.monthdays = []; + if (angular.isUndefined(this.repeat.months)) + this.repeat.months = []; + if (angular.isUndefined(this.repeat.year)) + this.repeat.year = {}; + if (this.repeat.count) + this.repeat.end = 'count'; + else if (this.repeat.until) { + this.repeat.end = 'until'; + this.repeat.until = this.repeat.until.substring(0,10).asDate(); + } + else + this.repeat.end = 'never'; + this.$hasCustomRepeat = this.hasCustomRepeat(); + + // Allow the event to be moved to a different calendar this.destinationCalendar = this.pid; }; + Component.prototype.hasCustomRepeat = function() { + var b = angular.isDefined(this.repeat) && + (this.repeat.interval > 1 || + this.repeat.days && this.repeat.days.length > 0 || + this.repeat.monthdays && this.repeat.monthdays.length > 0 || + this.repeat.months && this.repeat.months.length > 0); + return b; + }; + /** * @function getClassName * @memberof Component.prototype @@ -260,8 +305,8 @@ /** * @function $reset - * @memberof Card.prototype - * @desc Reset the original state the card's data. + * @memberof Component.prototype + * @desc Reset the original state the component's data. */ Component.prototype.$reset = function() { var _this = this; @@ -300,29 +345,21 @@ * @param {promise} futureComponentData - a promise of some of the Component's data */ Component.prototype.$unwrap = function(futureComponentData) { - var _this = this, - deferred = Component.$q.defer(); + var _this = this; // Expose the promise this.$futureComponentData = futureComponentData; // Resolve the promise this.$futureComponentData.then(function(data) { - // Calling $timeout will force Angular to refresh the view - Component.$timeout(function() { - _this.init(data); - // Make a copy of the data for an eventual reset - _this.$shadowData = _this.$omit(); - deferred.resolve(_this); - }); + _this.init(data); + // Make a copy of the data for an eventual reset + _this.$shadowData = _this.$omit(); }, function(data) { angular.extend(_this, data); _this.isError = true; Component.$log.error(_this.error); - deferred.reject(); }); - - return deferred.promise; }; /** @@ -335,13 +372,39 @@ var component = {}, date; angular.forEach(this, function(value, key) { if (key != 'constructor' && key[0] != '$') { - component[key] = value; + component[key] = angular.copy(value); } }); + // Format times component.startTime = component.startDate ? formatTime(component.startDate) : ''; component.endTime = component.endDate ? formatTime(component.endDate) : ''; + // Update recurrence definition depending on selections + if (this.$hasCustomRepeat) { + if (this.repeat.frequency == 'monthly' && this.repeat.month.type && this.repeat.month.type == 'byday' + || this.repeat.frequency == 'yearly' && this.repeat.year.byday) { + // BYDAY mask for a monthly or yearly recurrence + delete component.repeat.monthdays; + component.repeat.days = [{ day: this.repeat.month.day, occurrence: this.repeat.month.occurrence.toString() }]; + } + else if (this.repeat.month.type) { + // montly recurrence by month days or yearly by month + delete component.repeat.days; + } + } + else { + component.repeat = { frequency: this.repeat.frequency }; + } + if (this.repeat.end == 'until' && this.repeat.until) + component.repeat.until = this.repeat.until.stringWithSeparator('-'); + else if (this.repeat.end == 'count' && this.repeat.count) + component.repeat.count = this.repeat.count; + else { + delete component.repeat.until; + delete component.repeat.count; + } + function formatTime(dateString) { // YYYY-MM-DDTHH:MM-05:00 var date = new Date(dateString.substring(0,10) + ' ' + dateString.substring(11,16)), @@ -353,7 +416,6 @@ return hours + ':' + minutes; } - return component; }; diff --git a/UI/WebServerResources/js/Scheduler/ComponentController.js b/UI/WebServerResources/js/Scheduler/ComponentController.js index 94f4b8ac3..35bbc6081 100644 --- a/UI/WebServerResources/js/Scheduler/ComponentController.js +++ b/UI/WebServerResources/js/Scheduler/ComponentController.js @@ -13,7 +13,8 @@ vm.calendars = stateCalendars; vm.event = stateComponent; vm.categories = {}; - vm.editRecurrence = editRecurrence; + vm.showRecurrenceEditor = vm.event.$hasCustomRepeat; + vm.toggleRecurrenceEditor = toggleRecurrenceEditor; vm.cancel = cancel; vm.save = save; @@ -35,14 +36,9 @@ }, 100); // don't ask why }); - function editRecurrence($event) { - $mdDialog.show({ - templateUrl: 'editRecurrence', // UI/Templates/SchedulerUI/UIxRecurrenceEditor.wox - controller: RecurrenceController - }); - function RecurrenceController() { - - } + function toggleRecurrenceEditor() { + vm.showRecurrenceEditor = !vm.showRecurrenceEditor; + vm.event.$hasCustomRepeat = vm.showRecurrenceEditor; } function save(form) { diff --git a/UI/WebServerResources/scss/components/content/content.scss b/UI/WebServerResources/scss/components/content/content.scss index 56ee09019..ffc423b29 100644 --- a/UI/WebServerResources/scss/components/content/content.scss +++ b/UI/WebServerResources/scss/components/content/content.scss @@ -32,3 +32,9 @@ md-content { padding: $mg; } } + +.sg-subcontent { + border-left: $baseline-grid solid sg-color($sogoGreen, 100); + margin-left: ($baseline-grid / 2); + padding-left: $baseline-grid; +} \ No newline at end of file diff --git a/UI/WebServerResources/scss/components/input/_extends.scss b/UI/WebServerResources/scss/components/input/_extends.scss new file mode 100644 index 000000000..9eae5fe4e --- /dev/null +++ b/UI/WebServerResources/scss/components/input/_extends.scss @@ -0,0 +1,2 @@ +/*! input/_extends.scss - */ +@import '../../../angular-material/src/components/input/input.scss'; \ No newline at end of file diff --git a/UI/WebServerResources/scss/components/input/input.scss b/UI/WebServerResources/scss/components/input/input.scss index 38382a639..55f55bc21 100644 --- a/UI/WebServerResources/scss/components/input/input.scss +++ b/UI/WebServerResources/scss/components/input/input.scss @@ -1,97 +1,14 @@ -/// input.scss -*- Mode: text; indent-tabs-mode: nil; basic-offset: 2 -*- -$input-container-padding: 2px !default; +/// input.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*- +@import 'extends'; -$input-label-default-offset: 24px !default; -$input-label-default-scale: 1.0 !default; -$input-label-float-offset: 4px !default; -$input-label-float-scale: 0.75 !default; - -$input-border-width-default: 1px !default; -$input-border-width-focused: 2px !default; -$input-line-height: 26px !default; -$input-padding-top: 2px !default; - -md-input-container { - display: flex; - position: relative; - flex-direction: column; - padding: $input-container-padding; - - textarea, - input[type="text"], - input[type="password"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="week"], - input[type="number"], - input[type="email"], - input[type="url"], - input[type="search"], - input[type="tel"], - input[type="color"] { - /* remove default appearance from all input/textarea */ - -moz-appearance: none; - -webkit-appearance: none; - } - textarea { - resize: none; - overflow: hidden; - } - - label { - order: 1; - pointer-events: none; - -webkit-font-smoothing: antialiased; - z-index: 1; - transform: translate3d(0, $input-label-default-offset, 0) scale($input-label-default-scale); - transform-origin: left top; - transition: all $swift-ease-out-timing-function 0.2s; - } - - /* - * The .md-input class is added to the input/textarea - */ - .md-input { - flex: 1; - order: 2; - display: block; - - background: none; - padding-top: $input-padding-top; - padding-bottom: $input-border-width-focused - $input-border-width-default; - border-width: 0 0 $input-border-width-default 0; - line-height: $input-line-height; - -ms-flex-preferred-size: $input-line-height; //IE fix - - &:focus { - outline: none; - } - } - - &.md-input-focused, - &.md-input-has-value { - label { - transform: translate3d(0,$input-label-float-offset,0) scale($input-label-float-scale); - } - } - &.md-input-focused { - .md-input { - padding-bottom: 0px; // Increase border width by 1px, decrease padding by 1 - border-width: 0 0 $input-border-width-focused 0; - } - } - - .md-input[disabled] { - background-position: 0 bottom; - // This background-size is coordinated with a linear-gradient set in input-theme.scss - // to create a dotted line under the input. - background-size: 3px 1px; - background-repeat: repeat-x; +md-input-container.md-input-number { + flex-grow: 0; + width: 4em; + input { + text-align: center; } } + md-input-container .bgroup { display: block; } diff --git a/UI/WebServerResources/scss/components/pseudo-input/pseudo-input.scss b/UI/WebServerResources/scss/components/pseudo-input/pseudo-input.scss index e917b3ba9..0c1eb8d79 100644 --- a/UI/WebServerResources/scss/components/pseudo-input/pseudo-input.scss +++ b/UI/WebServerResources/scss/components/pseudo-input/pseudo-input.scss @@ -68,10 +68,6 @@ $input-padding-top: 2px !default; .pseudo-input-field { display: block; - margin-bottom: $line; - padding: $line 0 0 0; - font-size: sg-size(subhead); - line-height: 1; } .pseudo-input-field--underline { diff --git a/UI/WebServerResources/scss/components/select/select.scss b/UI/WebServerResources/scss/components/select/select.scss index 0fb2b3821..8e0abefcd 100644 --- a/UI/WebServerResources/scss/components/select/select.scss +++ b/UI/WebServerResources/scss/components/select/select.scss @@ -5,6 +5,13 @@ md-select { margin-right: $bl; } +// Try to align select labels with other input components +[layout="row"] { + .md-select-label { + padding-top: 4px; + } +} + // angular material overqualifies, so we are md-select.md-default-theme.sg-toolbar-sort { margin: 0 $bl 4px 0;