Browse code

FEATBL-865 Feat: remote attach to events

Thomas Fricker authored on 04/09/2019 07:51:44
Showing 14 changed files
... ...
@@ -34,8 +34,9 @@ BMFileHostingAPI.prototype.store = function(aOnLoad, aOnError, aThis, aExtraHead
34 34
 };
35 35
 
36 36
 BMFileHostingAPI.prototype.share = function(aOnLoad, aOnError, aThis, path, downloadLimit, expirationDate) {
37
-    let url = this._baseUrl + "/filehosting/" + this._domainUid + "/" + encodeURIComponent(path) + "/share";
38
-    url += "?downloadLimit=" + encodeURIComponent(downloadLimit);
37
+    let url = this._baseUrl + "/filehosting/" + this._domainUid + "/_share";
38
+    url += "?path=" + encodeURIComponent(path);
39
+    url += "&downloadLimit=" + encodeURIComponent(downloadLimit);
39 40
     url += "&expirationDate=" + encodeURIComponent(expirationDate);
40 41
     return this._execute(aOnLoad, aOnError, aThis, url, "GET", null, null);
41 42
 };
... ...
@@ -1,10 +1,9 @@
1 1
 <#if attachments??>
2
-	<#list attachments as attachment>
3
-		<div id="cloudAttachmentListRoot" style="padding: 15px; background-color: #d9edff;">
4
-			<div id="cloudAttachmentList" style="background-color: #ffffff; padding: 15px;">
5
-			<div class="cloudAttachmentItem" style="border: 1px  solid  #cdcdcd; border-radius: 5px; margin-top: 10px; margin-bottom: 10px; padding: 15px;"><a style="color: #0f7edb ;" href="${attachment.uri}" target="_blank">${attachment.name}</a><span style="margin-left: 5px; font-size: small; color: grey;">(146 ko)</span><span style="display: block; font-size: small; color: grey;"></span></div>
6
-			</div>
2
+	<div id="cloudAttachmentListRoot" style="padding: 15px; background-color: #d9edff;">
3
+		<div id="cloudAttachmentList" style="background-color: #ffffff; padding: 15px;">
4
+			<#list attachments as attachment>
5
+				<div class="cloudAttachmentItem" style="border: 1px  solid  #cdcdcd; border-radius: 5px; margin-top: 10px; margin-bottom: 10px; padding: 15px;"><a style="color: #0f7edb ;" href="${attachment.uri}" target="_blank">${attachment.name}</a><span style="margin-left: 5px; font-size: small; color: grey;"></span><span style="display: block; font-size: small; color: grey;"></span></div>
6
+			</#list>
7 7
 		</div>
8
-		</br>
9
-	</#list>
8
+	</div>
10 9
 </#if>
11 10
\ No newline at end of file
... ...
@@ -22,8 +22,8 @@ import java.io.BufferedInputStream;
22 22
 import java.io.ByteArrayOutputStream;
23 23
 import java.io.IOException;
24 24
 import java.net.URL;
25
-import java.time.ZonedDateTime;
26 25
 import java.security.SecureRandom;
26
+import java.time.ZonedDateTime;
27 27
 import java.util.ArrayList;
28 28
 import java.util.Arrays;
29 29
 import java.util.Collections;
... ...
@@ -70,7 +70,11 @@ public class VEventUtil {
70 70
 		if (changed(oldEvent.classification, newEvent.classification)) {
71 71
 			return true;
72 72
 		}
73
-		
73
+
74
+		if (listChanged(oldEvent.attachments, newEvent.attachments)) {
75
+			return true;
76
+		}
77
+
74 78
 		return false;
75 79
 	}
76 80
 
... ...
@@ -31,4 +31,41 @@ public class AttachedFile {
31 31
 
32 32
 	}
33 33
 
34
+	@Override
35
+	public int hashCode() {
36
+		final int prime = 31;
37
+		int result = 1;
38
+		result = prime * result + ((expirationDate == null) ? 0 : expirationDate.hashCode());
39
+		result = prime * result + ((name == null) ? 0 : name.hashCode());
40
+		result = prime * result + ((publicUrl == null) ? 0 : publicUrl.hashCode());
41
+		return result;
42
+	}
43
+
44
+	@Override
45
+	public boolean equals(Object obj) {
46
+		if (this == obj)
47
+			return true;
48
+		if (obj == null)
49
+			return false;
50
+		if (getClass() != obj.getClass())
51
+			return false;
52
+		AttachedFile other = (AttachedFile) obj;
53
+		if (expirationDate == null) {
54
+			if (other.expirationDate != null)
55
+				return false;
56
+		} else if (!expirationDate.equals(other.expirationDate))
57
+			return false;
58
+		if (name == null) {
59
+			if (other.name != null)
60
+				return false;
61
+		} else if (!name.equals(other.name))
62
+			return false;
63
+		if (publicUrl == null) {
64
+			if (other.publicUrl != null)
65
+				return false;
66
+		} else if (!publicUrl.equals(other.publicUrl))
67
+			return false;
68
+		return true;
69
+	}
70
+
34 71
 }
... ...
@@ -40,7 +40,8 @@ public interface IFileHosting {
40 40
 	 * Retrieves the configuration
41 41
 	 * 
42 42
 	 * @return the Filehosting service configuration
43
-	 * @throws ServerFault common error object
43
+	 * @throws ServerFault
44
+	 *                         common error object
44 45
 	 */
45 46
 	@GET
46 47
 	@Path("_config")
... ...
@@ -50,9 +51,11 @@ public interface IFileHosting {
50 51
 	/**
51 52
 	 * Lists files and folders. The listing contains only non-recursive items
52 53
 	 * 
53
-	 * @param path the folder path
54
+	 * @param path
55
+	 *                 the folder path
54 56
 	 * @return the files and folders found under this path
55
-	 * @throws ServerFault common error object
57
+	 * @throws ServerFault
58
+	 *                         common error object
56 59
 	 */
57 60
 	@GET
58 61
 	@Path("_list")
... ...
@@ -62,9 +65,11 @@ public interface IFileHosting {
62 65
 	/**
63 66
 	 * Finds items in the file hosting repository
64 67
 	 * 
65
-	 * @param query the query. The format of the query is repository dependent
68
+	 * @param query
69
+	 *                  the query. The format of the query is repository dependent
66 70
 	 * @return all items matching the query
67
-	 * @throws ServerFault common error object
71
+	 * @throws ServerFault
72
+	 *                         common error object
68 73
 	 */
69 74
 	@GET
70 75
 	@Path("_find")
... ...
@@ -74,9 +79,11 @@ public interface IFileHosting {
74 79
 	/**
75 80
 	 * Retrieves a document from the file hosting repository
76 81
 	 * 
77
-	 * @param path the relative path to the document
82
+	 * @param path
83
+	 *                 the relative path to the document
78 84
 	 * @return the document data
79
-	 * @throws ServerFault common error object
85
+	 * @throws ServerFault
86
+	 *                         common error object
80 87
 	 */
81 88
 	@GET
82 89
 	@Path("{path}/_content")
... ...
@@ -86,25 +93,31 @@ public interface IFileHosting {
86 93
 	/**
87 94
 	 * Retrieves a public URL to the document in the file hosting repository
88 95
 	 * 
89
-	 * @param path           the relative path to the document
90
-	 * @param downloadLimit  the number of times the file can be downloaded, <= 0 if
91
-	 *                       unlimited
92
-	 * @param expirationDate a ISO-8601 compliant date, null otherwise
96
+	 * @param path
97
+	 *                           the relative path to the document
98
+	 * @param downloadLimit
99
+	 *                           the number of times the file can be downloaded, <=
100
+	 *                           0 if unlimited
101
+	 * @param expirationDate
102
+	 *                           a ISO-8601 compliant date, null otherwise
93 103
 	 * @return the URL pointing to this document
94
-	 * @throws ServerFault common error object
104
+	 * @throws ServerFault
105
+	 *                         common error object
95 106
 	 */
96 107
 	@GET
97
-	@Path("{path}/share")
108
+	@Path("_share")
98 109
 	@RequiredRoles(value = { "canUseFilehosting", "canRemoteAttach" })
99
-	public FileHostingPublicLink share(@PathParam(value = "path") String path,
110
+	public FileHostingPublicLink share(@QueryParam(value = "path") String path,
100 111
 			@QueryParam(value = "downloadLimit") Integer downloadLimit,
101 112
 			@QueryParam(value = "expirationDate") String expirationDate) throws ServerFault;
102 113
 
103 114
 	/**
104 115
 	 * Remove a public link
105 116
 	 * 
106
-	 * @param url the share url
107
-	 * @throws ServerFault common error object
117
+	 * @param url
118
+	 *                the share url
119
+	 * @throws ServerFault
120
+	 *                         common error object
108 121
 	 */
109 122
 	@DELETE
110 123
 	@Path("{url}/unshare")
... ...
@@ -113,9 +126,12 @@ public interface IFileHosting {
113 126
 	/**
114 127
 	 * Update/insert a document
115 128
 	 * 
116
-	 * @param path     the relative path in the file hosting repository
117
-	 * @param document the document data
118
-	 * @throws ServerFault common error object
129
+	 * @param path
130
+	 *                     the relative path in the file hosting repository
131
+	 * @param document
132
+	 *                     the document data
133
+	 * @throws ServerFault
134
+	 *                         common error object
119 135
 	 */
120 136
 	@PUT
121 137
 	@Path("{path}")
... ...
@@ -125,8 +141,10 @@ public interface IFileHosting {
125 141
 	/**
126 142
 	 * Deletes a document
127 143
 	 * 
128
-	 * @param path the relative path in the file hosting repository
129
-	 * @throws ServerFault common error object
144
+	 * @param path
145
+	 *                 the relative path in the file hosting repository
146
+	 * @throws ServerFault
147
+	 *                         common error object
130 148
 	 */
131 149
 	@DELETE
132 150
 	@Path("{path}")
... ...
@@ -136,7 +154,8 @@ public interface IFileHosting {
136 154
 	/**
137 155
 	 * Retrieves informations about the filehosting implementation
138 156
 	 * 
139
-	 * @throws ServerFault common error object
157
+	 * @throws ServerFault
158
+	 *                         common error object
140 159
 	 */
141 160
 	@GET
142 161
 	@Path("_info")
... ...
@@ -145,9 +164,11 @@ public interface IFileHosting {
145 164
 	/**
146 165
 	 * Retrieves an entity from the file hosting repository
147 166
 	 * 
148
-	 * @param uid the entity uid
167
+	 * @param uid
168
+	 *                the entity uid
149 169
 	 * @return the document data
150
-	 * @throws ServerFault common error object
170
+	 * @throws ServerFault
171
+	 *                         common error object
151 172
 	 */
152 173
 	@GET
153 174
 	@Path("{uid}/_complete")
... ...
@@ -156,9 +177,11 @@ public interface IFileHosting {
156 177
 	/**
157 178
 	 * Retrieves a document from the file hosting repository by its public uid
158 179
 	 * 
159
-	 * @param uid the document uid
180
+	 * @param uid
181
+	 *                the document uid
160 182
 	 * @return the document data
161
-	 * @throws ServerFault common error object
183
+	 * @throws ServerFault
184
+	 *                         common error object
162 185
 	 */
163 186
 	@GET
164 187
 	@Path("{uid}/_public")
... ...
@@ -25,6 +25,7 @@ Require-Bundle: net.bluemind.authentication.closure;bundle-version="1.0.0",
25 25
  net.bluemind.ui.banner;bundle-version="1.0.0",
26 26
  net.bluemind.todolist.ui.closure;bundle-version="1.0.0",
27 27
  net.bluemind.history.ui.closure;bundle-version="4.1.0",
28
- net.bluemind.resource.closure;bundle-version="4.1.0"
28
+ net.bluemind.resource.closure;bundle-version="4.1.0",
29
+ net.bluemind.filehosting.api.closure;bundle-version="4.1.0"
29 30
 Bundle-ClassPath: css.jar, js.jar
30 31
 Closure-Bundle-Type: Library
... ...
@@ -184,7 +184,7 @@ net.bluemind.calendar.vevent.VEventActions.prototype.participation = function(e)
184 184
 };
185 185
 
186 186
 net.bluemind.calendar.vevent.VEventActions.prototype.collectAttendees_ = function(attendees) {
187
-  if (attendees == null || attendees.length == 0){
187
+  if (attendees == null || attendees.length == 0) {
188 188
     return;
189 189
   }
190 190
 
... ...
@@ -196,33 +196,43 @@ net.bluemind.calendar.vevent.VEventActions.prototype.collectAttendees_ = functio
196 196
     return;
197 197
   }
198 198
 
199
-
200
-  goog.array.forEach(toCollect, function(c) {
201
-    var q = '(_exists_:value.communications.emails.value OR value.kind:group) AND (value.identification.formatedName.value:'
202
-        + c['mailto'] + ' OR value.communications.emails.value:' + c['mailto'] + ')';
203
-
204
-    this.ctx_
205
-        .service('addressbooks')
206
-        .search(c['mailto'], 0, 1, 'Pertinance', q).then(function(res) {
207
-          if (res.count == 0) {
208
-            var vcard = {
209
-              'container' : 'book:CollectedContacts_' + this.ctx_.user['uid'],
210
-              'uid' : net.bluemind.mvp.UID.generate(),
211
-              'value' : {
212
-                'identification' : {'name': {'familyNames' : c['commonName']}},
213
-                'organizational' : {},
214
-                'related' : {},
215
-                'explanatory' : {},
216
-                'communications' : {
217
-                  'emails': [{'parameters' : [{'label' : 'TYPE', 'value' : 'work'}], 'value' : c['mailto']}]
218
-                }
199
+  goog.array
200
+      .forEach(
201
+          toCollect,
202
+          function(c) {
203
+            var q = '(_exists_:value.communications.emails.value OR value.kind:group) AND (value.identification.formatedName.value:'
204
+                + c['mailto'] + ' OR value.communications.emails.value:' + c['mailto'] + ')';
205
+
206
+            this.ctx_.service('addressbooks').search(c['mailto'], 0, 1, 'Pertinance', q).then(function(res) {
207
+              if (res.count == 0) {
208
+                var vcard = {
209
+                  'container' : 'book:CollectedContacts_' + this.ctx_.user['uid'],
210
+                  'uid' : net.bluemind.mvp.UID.generate(),
211
+                  'value' : {
212
+                    'identification' : {
213
+                      'name' : {
214
+                        'familyNames' : c['commonName']
215
+                      }
216
+                    },
217
+                    'organizational' : {},
218
+                    'related' : {},
219
+                    'explanatory' : {},
220
+                    'communications' : {
221
+                      'emails' : [ {
222
+                        'parameters' : [ {
223
+                          'label' : 'TYPE',
224
+                          'value' : 'work'
225
+                        } ],
226
+                        'value' : c['mailto']
227
+                      } ]
228
+                    }
229
+                  }
230
+                };
231
+
232
+                this.ctx_.service('addressbook').create(vcard);
219 233
               }
220
-            };
221
-
222
-            this.ctx_.service('addressbook').create(vcard);
223
-          }
224
-        }, null, this);
225
-    }, this);
234
+            }, null, this);
235
+          }, this);
226 236
 };
227 237
 
228 238
 /**
... ...
@@ -233,15 +243,16 @@ net.bluemind.calendar.vevent.VEventActions.prototype.collectAttendees_ = functio
233 243
 
234 244
 net.bluemind.calendar.vevent.VEventActions.prototype.save = function(e) {
235 245
   var model = e.vevent;
236
-  return this.ctx_.service('calendar').getItem(model.initalContainer || model.calendar, model.uid).then(function(existing) {
237
-    if (existing && model.initalContainer && model.calendar != model.initalContainer) {
238
-      this.move_(e, existing);
239
-    } else if (!existing) {
240
-      this.create_(e);
241
-    } else {
242
-      this.update_(e, existing);
243
-    }
244
-  }, null, this);
246
+  return this.ctx_.service('calendar').getItem(model.initalContainer || model.calendar, model.uid).then(
247
+      function(existing) {
248
+        if (existing && model.initalContainer && model.calendar != model.initalContainer) {
249
+          this.move_(e, existing);
250
+        } else if (!existing) {
251
+          this.create_(e);
252
+        } else {
253
+          this.update_(e, existing);
254
+        }
255
+      }, null, this);
245 256
 };
246 257
 
247 258
 /**
... ...
@@ -511,7 +522,7 @@ net.bluemind.calendar.vevent.VEventActions.prototype.goToForm_ = function(model,
511 522
     uri.getQueryData().set('recurrence-id', model.recurrenceId.toIsoString(true, true))
512 523
   }
513 524
   uri.getQueryData().set('container', model.calendar);
514
-  //FIXME
525
+  // FIXME
515 526
   if (model.states.updatable) {
516 527
     var storage = bluemind.storage.StorageHelper.getExpiringStorage();
517 528
     var vseries = this.adaptor_.fromVEventModelView(model, opt_vseries);
... ...
@@ -665,7 +676,7 @@ net.bluemind.calendar.vevent.VEventActions.prototype.showReccurringFormDialog_ =
665 676
 
666 677
 net.bluemind.calendar.vevent.VEventActions.prototype.needNotification = function(oldValue, actual) {
667 678
   var compareOrganizer = function(old, actual) {
668
-    if (!old && actual || !actual && old ) {
679
+    if (!old && actual || !actual && old) {
669 680
       return false;
670 681
     }
671 682
     return old == actual || old['dir'] == actual['dir'];
... ...
@@ -687,6 +698,22 @@ net.bluemind.calendar.vevent.VEventActions.prototype.needNotification = function
687 698
 
688 699
   };
689 700
 
701
+  var compareAttachments = function(old, actual) {
702
+    if (old == null && actual == null) {
703
+      return true;
704
+    } else if ((old == null && actual != null) || (old != null && actual == null)) {
705
+      return false;
706
+    } else if (old.length != actual.length) {
707
+      return false;
708
+    } else {
709
+      return goog.array.equals(old, actual, function(oldAttachment, actualAttachment) {
710
+        return oldAttachment['name'] == actualAttachment['name']
711
+            && oldAttachment['publicUrl'] == actualAttachment['publicUrl'];
712
+      });
713
+    }
714
+
715
+  };
716
+
690 717
   var compareExdate = function(old, actual) {
691 718
     var oldLength = goog.isDefAndNotNull(old) && goog.isArray(old) ? old.length : 0;
692 719
     var actualLength = goog.isDefAndNotNull(actual) && goog.isArray(actual) ? actual.length : 0;
... ...
@@ -700,6 +727,7 @@ net.bluemind.calendar.vevent.VEventActions.prototype.needNotification = function
700 727
   ret |= oldValue['priority'] != actual['priority'];
701 728
   ret |= !compareExdate(oldValue['exdate'], actual['exdate']);
702 729
   ret |= !compareAttendees(oldValue['attendees'], actual['attendees']);
730
+  ret |= !compareAttachments(oldValue['attachments'], actual['attachments']);
703 731
   ret |= !compareOrganizer(oldValue['organizer'], actual['organizer']);
704 732
 
705 733
   ret |= this.adaptor_.recurrenceHasChanged(oldValue, actual);
... ...
@@ -198,24 +198,25 @@
198 198
             <div class="{css bm-ui-form-entry} {css bm-clearfix}">
199 199
               <label id="attachment-label" for="bm-ui-form-owner">{msg meaning="calendar.event.attachments" desc="Attachments"}Attachments{/msg}</label>
200 200
               <div class="{css bm-ui-form-input}">
201
-                
201
+                &nbsp;
202 202
                 <div id="bm-attachment-list">
203 203
                 	{foreach $attachment in $event.attachments}
204 204
                   		{call .attachmentEntry}
205 205
                   			{param attachment : $attachment /}
206 206
                   		{/call}
207 207
                   	{/foreach}	
208
-                </div>  
209
-                  
210
-                <br>
211
-                <div class="{css bm-ui-form-inline}" id="bm-ui-form-no-attachment-block">
212
-                  <label id="attachment-label-local" for="bm-ui-form-owner">{msg meaning="calendar.event.addAttachmentLocal" desc="Button to add an attachment"}Add{/msg}</label>
213
-                  <input type="file" id="localAttachmentFile">&nbsp;&nbsp;<br>
214
-                  <p><div class="goog-inline-block goog-link-button" role="button" id="bm-ui-form-add-attachment-server" style="-webkit-user-select: none;" tabindex="0">{msg meaning="calendar.event.addAttachmentServer" desc="Button to add an attachment"}Add from Server{/msg}</div></p>
215 208
                 </div>
216
-                
217 209
               </div>
218 210
             </div>
211
+            <div class="{css bm-ui-form-entry} {css bm-clearfix}">
212
+              <label id="add-attachment-label" for="bm-ui-form-owner">{msg meaning="calendar.event.add_attachments" desc="Attachments"}Add Attachments{/msg}</label>
213
+              <div class="{css bm-ui-form-inline}" id="bm-ui-form-no-attachment-block">
214
+                  <input type="file" id="localAttachmentFile" style="visibility:hidden; width:0px;" />
215
+				  <label for="localAttachmentFile" class="goog-link-button">{msg meaning="calendar.event.addAttachmentLocal" desc="Attachments"}From local PC{/msg}</label>
216
+                  <div class="goog-inline-block goog-link-button" role="button" id="bm-ui-form-add-attachment-server" style="-webkit-user-select: none;" tabindex="0">{msg meaning="calendar.event.addAttachmentServer" desc="Button to add an attachment"}From Server{/msg}</div>
217
+                  &nbsp;&nbsp;<progress id="local-att-progress" max="100" value="0" style="visibility:hidden"></progress>
218
+              </div>
219
+            </div>  
219 220
           </fieldset>
220 221
           <fieldset id="bm-ui-form-fieldset-repeat" class="{css goog-tab-content}" style='display: none'>
221 222
           <div class="{css bm-ui-form-entry} {css bm-clearfix}">
... ...
@@ -79,6 +79,7 @@ goog.require("bluemind.ui.style.DangerousActionButtonRenderer");
79 79
 goog.require("bluemind.ui.style.PrimaryActionButtonRenderer");
80 80
 goog.require("net.bluemind.calendar.vevent.VEventAdaptor");
81 81
 goog.require("net.bluemind.history.HistoryDialog");
82
+goog.require('net.bluemind.filehosting.api.FileHostingClient');
82 83
 
83 84
 /**
84 85
  * BlueMind Calendar form
... ...
@@ -472,11 +473,13 @@ net.bluemind.calendar.vevent.ui.Form.prototype.enterDocument = function() {
472 473
   var canRemoteAttach = goog.global['bmcSessionInfos']['roles'].split(',').includes('canRemoteAttach');
473 474
   if (!canRemoteAttach){
474 475
     this.getDomHelper().removeNode(dom.getElement('bm-ui-form-no-attachment-block'));
476
+    this.getDomHelper().removeNode(dom.getElement('add-attachment-label'));
475 477
     if (this.getModel().attachments.length == 0){
476 478
       this.getDomHelper().removeNode(dom.getElement('attachment-label'));
477 479
     }
478 480
   } else {
479 481
     handler.listen(dom.getElement('localAttachmentFile'), goog.events.EventType.CHANGE, function() {
482
+      this.resetError_('details');
480 483
       var fileInput = document.getElementById('localAttachmentFile');
481 484
       var file = fileInput.files[0];
482 485
       var formData = new FormData();
... ...
@@ -489,29 +492,29 @@ net.bluemind.calendar.vevent.ui.Form.prototype.enterDocument = function() {
489 492
       xhr.open('PUT', url, true);
490 493
       xhr.setRequestHeader('X-BM-ApiKey', sid);
491 494
       var that = this;  
495
+      that.getDomHelper().getElement('local-att-progress').style.visibility = 'visible';
496
+      xhr.upload.onprogress = function(e){
497
+        console.log(e.loaded + "  - > " + e.total);
498
+        var p = Math.min((e.loaded/e.total)*100, 80);
499
+        that.getDomHelper().getElement('local-att-progress').value = p;
500
+      }
492 501
       xhr.onload = function () {
493
-          var ret = JSON.parse(this.response);
494
-          var index = 0;
495
-          for (i = 0; i < that.getModel().attachments.length; i++) { 
496
-            index = Math.max(that.getModel().attachments[i].index, index);
502
+          that.getDomHelper().getElement('local-att-progress').style.visibility = 'hidden';
503
+          that.getDomHelper().getElement('local-att-progress').value = 0;
504
+          if(this.status == 413){
505
+            /** @meaning calendar.form.error.attachment.size */
506
+            var MSG_ATTACHMENT_SIZE = goog.getMsg('The selected attachment exceeds the configured max size');
507
+            that.addError_('details', that.getDomHelper().getElement('add-attachment-label'), MSG_ATTACHMENT_SIZE);
508
+            return;
497 509
           } 
498
-          index++;
499
-          var publicUrl = ret['publicUrl'];
500
-          var name = ret['name'];
501
-          var newAttachment  = {
502
-              publicUrl : publicUrl,
503
-              name : name,
504
-              index : index
510
+          if(this.status != 200){
511
+            /** @meaning calendar.form.error.attachment */
512
+            var MSG_ATTACHMENT = goog.getMsg('an unknown error occurred while uploading the document');
513
+            that.addError_('details', that.getDomHelper().getElement('add-attachment-label'), MSG_ATTACHMENT);
514
+            return;
505 515
           }
506
-          
507
-          that.getModel().attachments.push(newAttachment);
508
-          var entry = soy.renderAsFragment(net.bluemind.calendar.vevent.templates.attachmentEntry, {
509
-            attachment : newAttachment
510
-          });
511
-
512
-          that.getDomHelper().appendChild(dom.getElement('bm-attachment-list'), entry);
513
-          handler.listen(dom.getElement('bm-ui-form-delete-attachment-'+newAttachment.index), goog.events.EventType.CLICK, that.delAttachment(newAttachment));
514
-          
516
+          var ret = JSON.parse(this.response);
517
+          that.addAttachment(that, ret, dom);
515 518
           dom.getElement('localAttachmentFile').value = "";
516 519
       };
517 520
       xhr.send(formData);
... ...
@@ -519,7 +522,33 @@ net.bluemind.calendar.vevent.ui.Form.prototype.enterDocument = function() {
519 522
     });
520 523
    
521 524
     handler.listen(dom.getElement('bm-ui-form-add-attachment-server'), goog.events.EventType.CLICK, function() {
522
-      
525
+        var that = this; 
526
+        var options = {
527
+          'success': function(links) {
528
+            goog.array.forEach(links, function(link) {
529
+              var client = new net.bluemind.filehosting.api.FileHostingClient(that.ctx.rpc, '', that.ctx.user.domainUid);
530
+              var ret = client.share(link.path, 0, null).then(function(linkInfo) {
531
+                 linkInfo['publicUrl'] = linkInfo['url'];
532
+                 linkInfo['name'] = link['name'];
533
+                 that.addAttachment(that, linkInfo, dom);
534
+              });
535
+            });
536
+          },
537
+          'multi': true,
538
+          'close': true
539
+        };
540
+        var w = 640, h = 512;
541
+        var t = (window.screenY || window.screenTop) + ((window.outerHeight || document.documentElement.offsetHeight) - h) / 2;
542
+        var l = (window.screenX || window.screenLeft) + ((window.outerWidth || document.documentElement.offsetWidth) - w) / 2;
543
+        var child = window.open('/chooser/#', 'chooser', "width=" + w + ",height=" + h + ",left=" + l + ",top=" + t)
544
+        var setOptions = function() {
545
+          if (child['application']) {
546
+            child['application']['setOptions'](options);
547
+          } else {
548
+            setTimeout(setOptions, 50);
549
+          };
550
+        }
551
+        setOptions();
523 552
     });
524 553
   }
525 554
 
... ...
@@ -617,6 +646,33 @@ net.bluemind.calendar.vevent.ui.Form.prototype.enterDocument = function() {
617 646
 };
618 647
 
619 648
 /**
649
+ * private
650
+ */
651
+net.bluemind.calendar.vevent.ui.Form.prototype.addAttachment = function(that, ret, dom){
652
+  var index = 0;
653
+  for (var i = 0; i < that.getModel().attachments.length; i++) { 
654
+    index = Math.max(that.getModel().attachments[i].index, index);
655
+  } 
656
+  index++;
657
+  var publicUrl = ret['publicUrl'];
658
+  var name = ret['name'];
659
+  var newAttachment  = {
660
+      publicUrl : publicUrl,
661
+      name : name,
662
+      index : index
663
+  }
664
+  
665
+  that.getModel().attachments.push(newAttachment);
666
+  var entry = soy.renderAsFragment(net.bluemind.calendar.vevent.templates.attachmentEntry, {
667
+    attachment : newAttachment
668
+  });
669
+
670
+  that.getDomHelper().appendChild(dom.getElement('bm-attachment-list'), entry);
671
+  that.getHandler().listen(dom.getElement('bm-ui-form-delete-attachment-'+newAttachment.index), goog.events.EventType.CLICK, that.delAttachment(newAttachment));
672
+
673
+}
674
+
675
+/**
620 676
  * @private
621 677
  */
622 678
 net.bluemind.calendar.vevent.ui.Form.prototype.delAttachment = function(attachment){
... ...
@@ -13,6 +13,8 @@ calendar.event.detail=Detail
13 13
 calendar.event.freeBusy.busy=Besch\u00E4ftigt
14 14
 calendar.event.freeBusy.noInformation=Keine Information
15 15
 calendar.event.freeBusy.outOfWork=Ausserhalb der Arbeitszeit
16
+calendar.form.error.attachment.size=Der Anhang \u00FCberschreitet die maximale Gr\u00F6sse
17
+calendar.form.error.attachment=Ein Fehler ist beim Upload aufgetreten
16 18
 calendar.event.location=Ort
17 19
 calendar.event.modifyDetails=Details \u00E4ndern
18 20
 calendar.event.occupation.available=Verf\u00FCgbar
... ...
@@ -109,8 +111,9 @@ general.yes=Ja
109 111
 search.results.prior=Zeige Resultate vor dem 
110 112
 search.results.after=Zeige Resultate nach dem
111 113
 calendar.addCalendar=Kalender hinzuf\u00FCgen...
112
-calendar.event.addAttachmentLocal=Lokalen Anhang hinzuf\u00FCgen
113
-calendar.event.addAttachmentServer=Anhang hinzuf\u00FCgen (Server)
114
+calendar.event.add_attachments=Anhang hinzufügen
115
+calendar.event.addAttachmentLocal=Computer
116
+calendar.event.addAttachmentServer=Server
114 117
 calendar.event.addAttendee=Teilnehmer hinzuf\u00FCgen...
115 118
 calendar.event.position.dayOfMonth.first=am ersten {$day} im {$month} 
116 119
 calendar.event.position.dayOfMonth.fourth=am vierten {$day} im {$month} 
... ...
@@ -5,8 +5,11 @@ calendar.back=Back to calendar
5 5
 calendar.calendar=Calendar
6 6
 calendar.event.addAttendee=Add an attendee or a resource...
7 7
 # or ['Add an attendee ...']
8
-calendar.event.addAttachmentLocal=Add local attachment
9
-calendar.event.addAttachmentServer=Add an attachment (Server)
8
+calendar.event.add_attachments=Add attachment
9
+calendar.event.addAttachmentLocal=From local PC
10
+calendar.event.addAttachmentServer=From Server
11
+calendar.form.error.attachment.size=The attachment exceeds the configured maximal size
12
+calendar.form.error.attachment=Unknown error occurred while uploading the document
10 13
 calendar.event.addReminder=Add a reminder
11 14
 calendar.event.allDay=All day
12 15
 calendar.event.attendees=Attendees
... ...
@@ -5,7 +5,7 @@ calendar.event.addReminder=Ajouter une alerte
5 5
 calendar.event.allDay=Toute la journ\u00E9e
6 6
 calendar.event.attendees=Participants
7 7
 calendar.event.date=Date
8
-calendar.event.dateEnd=jusqu’au
8
+calendar.event.dateEnd=jusqu\u2019au
9 9
 calendar.event.delete=Supprimer
10 10
 calendar.event.delete.caption=Voulez-vous supprimer cet \u00E9v\u00E9nement ?
11 11
 calendar.event.description=Description
... ...
@@ -19,11 +19,14 @@ calendar.event.occupation.available=Disponible
19 19
 calendar.event.occupation.busy=Occup\u00E9
20 20
 calendar.event.occupation.label=Disponibilit\u00E9
21 21
 calendar.event.organizer=Organisateur
22
-calendar.event.attachments=Pièces jointes 
22
+calendar.event.attachments=Pi\u00e8ces jointes 
23 23
 calendar.event.participation=Participation
24
-calendar.event.addAttachmentLocal=Ajouter une pièces jointe depuis  l'ordinateur
25
-calendar.event.addAttachmentServer=Ajouter une pièce jointes depuis le serveur
26
-calendar.event.participation.addNote=Ajouter une note \u00E0 la r\u00E9ponse avant de l’envoyer
24
+calendar.event.add_attachments=Ajouter des pi\u00e8ces jointes
25
+calendar.event.addAttachmentLocal=Depuis l'ordinateur
26
+calendar.event.addAttachmentServer=Depuis le serveur
27
+calendar.form.error.attachment.size=La pi\u00e8ce jointe d\u00e9passe la taille maximale autoris\u00e9e
28
+calendar.form.error.attachment=Erreur inconnue lors de l'upload de la pi\u00e8ce jointe  
29
+calendar.event.participation.addNote=Ajouter une note \u00E0 la r\u00E9ponse avant de l\u2019envoyer
27 30
 calendar.event.participation.maybe=Peut-\u00EAtre
28 31
 calendar.event.participation.notSendResponse=Ne pas envoyer de r\u00E9ponse
29 32
 calendar.event.participation.sendResponse=Envoyer une r\u00E9ponse maintenant
... ...
@@ -45,19 +48,19 @@ calendar.event.recurringEvent.action.allFollowing=Tous les \u00E9v\u00E9nements
45 48
 calendar.event.recurringEvent.action.allOtherRemainSame=Les autres \u00E9v\u00E9nements de la s\u00E9rie resteront identiques.
46 49
 calendar.event.recurringEvent.action.onlyThisOne=Uniquement cet \u00E9v\u00E9nement
47 50
 calendar.event.recurringEvent.delete.content=Voulez-vous supprimer uniquement cet \u00E9v\u00E9nement, tous les \u00E9v\u00E9nements de la s\u00E9rie, ou bien cet \u00E9v\u00E9nement et tous les \u00E9v\u00E9nements ult\u00E9rieurs de la s\u00E9rie ?
48
-calendar.event.recurringEvent.delete.title=Supprimer l’\u00E9v\u00E9nement p\u00E9riodique
51
+calendar.event.recurringEvent.delete.title=Supprimer l\u2019\u00E9v\u00E9nement p\u00E9riodique
49 52
 calendar.event.recurringEvent.info.allFollowingWillBeChanged=Cet \u00E9v\u00E9nement et tous les \u00E9v\u00E9nements suivants seront modifi\u00E9s.
50 53
 calendar.event.recurringEvent.info.allWillBeChanged=Tous les \u00E9v\u00E9nements de la s\u00E9rie seront modifi\u00E9s.
51 54
 calendar.event.recurringEvent.info.allWillBeDeleted=Tous les \u00E9v\u00E9nements de la s\u00E9rie seront supprim\u00E9s.
52 55
 calendar.event.recurringEvent.update.content=Voulez-vous modifier uniquement cet \u00E9v\u00E9nement, tous les \u00E9v\u00E9nements de la s\u00E9rie, ou bien cet \u00E9v\u00E9nement et tous les \u00E9v\u00E9nements ult\u00E9rieurs de la s\u00E9rie ?
53 56
 calendar.event.recurringEvent.updateWithoutFollowing.content=Voulez-vous modifier uniquement cet \u00E9v\u00E9nement ou tous les \u00E9v\u00E9nements de la s\u00E9rie ?
54
-calendar.event.recurringEvent.update.title=Modifier l’\u00E9v\u00E9nement p\u00E9riodique
57
+calendar.event.recurringEvent.update.title=Modifier l\u2019\u00E9v\u00E9nement p\u00E9riodique
55 58
 calendar.event.reminder=Rappel
56 59
 calendar.event.repeat.daily=Tous les jours
57 60
 calendar.event.repeat.label=R\u00E9currence
58 61
 calendar.event.repeat.monthly=Tous les mois
59 62
 calendar.event.repeat.none=Aucune
60
-calendar.event.repeat.until=jusqu’au
63
+calendar.event.repeat.until=jusqu\u2019au
61 64
 calendar.event.repeat.times=fois
62 65
 calendar.event.repeat.weekly=Toutes les semaines
63 66
 calendar.event.repeat.yearly=Tous les ans
... ...
@@ -70,7 +73,7 @@ calendar.list.allDay=Toute la journ\u00E9e
70 73
 calendar.list.hourColumn=Heure
71 74
 calendar.list.detailColumn=D\u00E9tail
72 75
 calendar.list.dateColumn=Date
73
-calendar.list.noPendingEvents=Pas d’\u00E9venement en attente !
76
+calendar.list.noPendingEvents=Pas d\u2019\u00E9venement en attente !
74 77
 calendar.newEvent=Nouvel \u00E9v\u00E9nement
75 78
 calendar.toolbar.PDFprint=Impression PDF
76 79
 calendar.toolbar.all=Tous mes \u00E9v\u00E9nements
... ...
@@ -91,7 +94,7 @@ calendar.toolbar.print.orientation.portrait=Portrait
91 94
 calendar.toolbar.print.preview=Aper\u00E7u
92 95
 calendar.toolbar.print.showDetails=Afficher les d\u00E9tails
93 96
 calendar.toolbar.refresh=Rafra\u00EEchir
94
-calendar.toolbar.today=Aujourd’hui
97
+calendar.toolbar.today=Aujourd\u2019hui
95 98
 calendar.toolbar.week=Semaine
96 99
 calendar.updatePopup.delete.content=Voulez-vous supprimer cet \u00E9v\u00E9nement ?
97 100
 calendar.updatePopup.delete.title=Supprimer
... ...
@@ -143,7 +146,7 @@ calendar.event.repeat.yearly.everyYear=Tous les ans,
143 146
 calendar.event.repeat.yearly.onDayOfYear=le {$rrulebydate}
144 147
 calendar.privateChanges.title=Ces changements resteront priv\u00E9s
145 148
 calendar.sendNotification.toAttendees=En cliquant sur Non, l'\u00E9v\u00E9nement est modifi\u00E9 uniquement dans votre agenda.
146
-calendar.sendNotification.toOrganizer=Voulez-vous notifier l’organisateur de vos changements ?
149
+calendar.sendNotification.toOrganizer=Voulez-vous notifier l\u2019organisateur de vos changements ?
147 150
 calendar.sendNotification.title=Informer les participants ?
148 151
 calendar.sendNotificationCreation.toAttendees=En cliquant sur Non, l'\u00E9v\u00E9nement appara\u00eet uniquement dans votre agenda.
149 152
 calendar.eventOccursInPast=Cet \u00E9v\u00E9nement a lieu \u00E0 une date r\u00E9volue
... ...
@@ -158,19 +161,19 @@ general.msg.withNotificationPopup=Avec une popup de notification *
158 161
 general.remove=Supprimer
159 162
 calendar.event.participation.accept=Accepter
160 163
 calendar.event.participation.decline=Decliner
161
-calendar.banner.noPendingEvents=Pas d’\u00E9v\u00E9nement en attente !
164
+calendar.banner.noPendingEvents=Pas d\u2019\u00E9v\u00E9nement en attente !
162 165
 calendar.banner.pendingEvents={$count} \u00E9v\u00E9nements en attente.
163 166
 general.todolists=Listes de t\u00E2ches
164 167
 calendar.todos.toggle=Montrer/Cacher les t\u00E2ches
165 168
 tasks.section.late=En retard
166
-tasks.section.today=Aujourd’hui
169
+tasks.section.today=Aujourd\u2019hui
167 170
 tasks.section.tomorrow=Demain
168 171
 tasks.section.week=Cette semaine
169 172
 tasks.section.month=Ce mois
170 173
 tasks.section.noDueDate=Sans \u00E9ch\u00E9ance
171 174
 search.results.prior=Voir r\u00E9sultats avant
172 175
 search.results.after=Voir r\u00E9sultats apr\u00E8s
173
-calendar.responseComment=Note \u00E0 l’organisateur
176
+calendar.responseComment=Note \u00E0 l\u2019organisateur
174 177
 calendar.update.success=\u00C9v\u00E9nement sauvegard\u00E9
175 178
 calendar.create.success=\u00C9v\u00E9nement cr\u00E9\u00E9
176 179
 generic.createError=Erreur lors de la cr\u00E9ation: {$message}
... ...
@@ -194,7 +197,7 @@ calendar.view.delete.success=Vue supprim\u00E9e
194 197
 calendar.toolbar.pending.plural=Vous avez {$pending} invitations
195 198
 calendar.toolbar.pending=Vous avez une invitation
196 199
 calendar.popup.updateParticipation=Envoyer une r\u00E9ponse maintenant
197
-calendar.popup.updateParticipation.note=Ajouter une note \u00E0 la r\u00E9ponse avant de l’envoyer
200
+calendar.popup.updateParticipation.note=Ajouter une note \u00E0 la r\u00E9ponse avant de l\u2019envoyer
198 201
 calendar.popup.updateParticipation.silent=Ne pas envoyer de r\u00E9ponse
199 202
 general.history=Historique
200 203
 calendar.popup.note.title=Ajouter une note
... ...
@@ -29,7 +29,6 @@ net.bluemind.api.BlueMindClient.prototype.rpc;
29 29
 net.bluemind.api.BlueMindClient.prototype.base;
30 30
 
31 31
 /**
32
- * 
33 32
  * @param {relief.rpc.Command} cmd Command to execute
34 33
  * @param {Object} data Post data
35 34
  * @returns {goog.Thenable}