0012950: More attachment methods for mail
[tine20] / tine20 / Tinebase / js / widgets / grid / FileUploadGrid.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Philipp Schüle <p.schuele@metaways.de>
6  * @copyright   Copyright (c) 2010-2011 Metaways Infosystems GmbH (http://www.metaways.de)
7  * @version     $Id
8  */
9
10 /*global Ext, Tine*/
11  
12 Ext.ns('Tine.widgets.grid');
13
14 /**
15  * @namespace   Tine.widgets.grid
16  * @class       Tine.widgets.grid.FileUploadGrid
17  * @extends     Ext.grid.GridPanel
18  * 
19  * <p>FileUpload grid for dialogs</p>
20  * <p>
21  * </p>
22  * 
23  * @author      Philipp Schüle <p.schuele@metaways.de>
24  * @copyright   Copyright (c) 2010-2011 Metaways Infosystems GmbH (http://www.metaways.de)
25  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
26  * 
27  * @param       {Object} config
28  * 
29  * @constructor Create a new  Tine.widgets.grid.FileUploadGrid
30  */
31 Tine.widgets.grid.FileUploadGrid = Ext.extend(Ext.grid.EditorGridPanel, {
32
33     /**
34      * @cfg filesProperty
35      * @type String
36      */
37     filesProperty: 'files',
38     
39     /**
40      * @cfg showTopToolbar
41      * @type Boolean
42      * TODO     think about that -> when we deactivate the top toolbar, we lose the dropzone for files!
43      */
44     //showTopToolbar: null,
45
46     /**
47      * @cfg {Bool} readOnly
48      */
49     readOnly: false,
50
51     /**
52      * config values
53      * @private
54      */
55     header: false,
56     border: false,
57     deferredRender: false,
58     autoExpandColumn: 'name',
59     showProgress: true,
60     
61     i18nFileString: null,
62     
63     /**
64      * init
65      * @private
66      */
67     initComponent: function () {
68         this.i18nFileString = this.i18nFileString ? this.i18nFileString : i18n._('File');
69         
70         this.record = this.record || null;
71
72         // init actions
73         this.actionUpdater = new Tine.widgets.ActionUpdater({
74             containerProperty: 'container_id', 
75             evalGrants: false
76         });
77         
78         this.initToolbarAndContextMenu();
79         this.initStore();
80         this.initColumnModel();
81         this.initSelectionModel();
82
83
84         if (!this.plugins) {
85             this.plugins = [];
86         }
87
88         this.plugins.push(new Ext.ux.grid.GridViewMenuPlugin({}));
89
90         this.enableHdMenu = false;
91       
92         Tine.widgets.grid.FileUploadGrid.superclass.initComponent.call(this);
93
94         this.on('rowcontextmenu', function (grid, row, e) {
95             e.stopEvent();
96             var selModel = grid.getSelectionModel();
97             if (! selModel.isSelected(row)) {
98                 selModel.selectRow(row);
99             }
100             this.contextMenu.showAt(e.getXY());
101         }, this);
102
103         if (! this.record || this.record.id === 0) {
104             this.on('celldblclick', function (grid, rowIndex, columnIndex, e) {
105                 // Don't download if the cell has an editor, just go on with the event
106                 if (grid.getColumns()[columnIndex].hasOwnProperty('editor')) {
107                     return true;
108                 }
109
110                 // In case cell has no editor, just assume a download is intended
111                 e.stopEvent();
112                 this.onDownload()
113             }, this);
114         }
115     },
116
117     setReadOnly: function(readOnly) {
118         this.readOnly = readOnly;
119         this.action_add.setDisabled(readOnly);
120         this.action_remove.setDisabled(readOnly);
121         this.action_add_from_filemanager.setDisabled(readOnly);
122     },
123
124     /**
125      * on upload failure
126      * @private
127      */
128     onUploadFail: function (uploader, fileRecord) {
129         
130         var dataSize;
131         if (fileRecord.html5upload) {
132             dataSize = Tine.Tinebase.registry.get('maxPostSize');
133         }
134         else {
135             dataSize = Tine.Tinebase.registry.get('maxFileUploadSize');
136         }
137         
138         Ext.MessageBox.alert(
139             i18n._('Upload Failed'),
140             i18n._('Could not upload file. Filesize could be too big. Please notify your Administrator. Max upload size:') + ' ' + parseInt(dataSize, 10) / 1048576 + ' MB'
141         ).setIcon(Ext.MessageBox.ERROR);
142         
143         this.getStore().remove(fileRecord);
144         if(this.loadMask) this.loadMask.hide();
145     },
146     
147     /**
148      * on remove
149      * @param {} button
150      * @param {} event
151      */
152     onRemove: function (button, event) {
153         
154         var selectedRows = this.getSelectionModel().getSelections();
155         for (var i = 0; i < selectedRows.length; i += 1) {
156             this.store.remove(selectedRows[i]);
157             var upload = Tine.Tinebase.uploadManager.getUpload(selectedRows[i].get('uploadKey'));
158             if (upload) {
159                 upload.setPaused(true);
160             }
161         }
162     },
163     
164     
165     /**
166      * on pause
167      * @param {} button
168      * @param {} event
169      */
170     onPause: function (button, event) {
171  
172         var selectedRows = this.getSelectionModel().getSelections();
173         for(var i=0; i < selectedRows.length; i++) {
174             var upload = Tine.Tinebase.uploadManager.getUpload(selectedRows[i].get('uploadKey'));
175             upload.setPaused(true);
176         }       
177         this.getSelectionModel().deselectRange(0, this.getSelectionModel().getCount());
178     },
179
180     
181     /**
182      * on resume
183      * @param {} button
184      * @param {} event
185      */
186     onResume: function (button, event) {
187
188         var selectedRows = this.getSelectionModel().getSelections();
189         for(var i=0; i < selectedRows.length; i++) {
190             var upload = Tine.Tinebase.uploadManager.getUpload(selectedRows[i].get('uploadKey'));
191             upload.resumeUpload();
192         }
193         this.getSelectionModel().deselectRange(0, this.getSelectionModel().getCount());
194     },
195
196     
197     /**
198      * init toolbar and context menu
199      * @private
200      */
201     initToolbarAndContextMenu: function () {
202         var me = this;
203
204         this.action_add = new Ext.Action(this.getAddAction());
205
206         if (Tine.Tinebase.appMgr.isEnabled('Filemanager')) {
207             var filemanager = Tine.Tinebase.appMgr.get('Filemanager');
208
209             this.action_add_from_filemanager = new Ext.Action({
210                 text: String.format(filemanager.i18n._('Add {0} from Filemanager'), this.i18nFileString),
211                 iconCls: 'action_add',
212                 scope: this,
213                 handler: function () {
214                     var filePickerDialog = new Tine.Filemanager.FilePickerDialog({
215                         title: filemanager.i18n._('Select a file'),
216                         singleSelect: true,
217                         constraint: 'file'
218                     });
219
220                     filePickerDialog.openWindow();
221
222                     filePickerDialog.on('selected', function(node) {
223                         me.onFileSelectFromFilemanager.call(me, node);
224                     });
225                 }
226             });
227         }
228
229         this.action_remove = new Ext.Action({
230             text: String.format(i18n._('Remove {0}'), this.i18nFileString),
231             iconCls: 'action_remove',
232             scope: this,
233             disabled: true,
234             handler: this.onRemove
235         });
236         
237         this.action_pause = new Ext.Action({
238             text: i18n._('Pause upload'),
239             iconCls: 'action_pause',
240             scope: this,
241             handler: this.onPause,
242             actionUpdater: this.isPauseEnabled
243         });
244         
245         this.action_resume = new Ext.Action({
246             text: i18n._('Resume upload'),
247             iconCls: 'action_resume',
248             scope: this,
249             handler: this.onResume,
250             actionUpdater: this.isResumeEnabled
251         });
252         
253         this.tbar = [
254             this.action_add
255         ];
256
257         if (Tine.Tinebase.appMgr.isEnabled('Filemanager')) {
258             this.tbar.push(this.action_add_from_filemanager)
259         }
260
261         this.tbar.push(this.action_remove);
262         
263         this.contextMenu = new Ext.menu.Menu({
264             plugins: [{
265                 ptype: 'ux.itemregistry',
266                 key:   'Tinebase-MainContextMenu'
267             }],
268             items:  [
269                  this.action_remove,
270                  this.action_pause,
271                  this.action_resume
272             ]
273         });
274         
275         this.actionUpdater.addActions([
276             this.action_pause,
277             this.action_resume
278         ]);
279
280     },
281     
282     /**
283      * init store
284      * @private
285      */
286     initStore: function () {
287         this.store = new Ext.data.SimpleStore({
288             fields: Ext.ux.file.Upload.file
289         });
290
291         this.store.on('add', this.onStoreAdd, this);
292
293         this.loadRecord(this.record);
294     },
295
296     onStoreAdd: function(store, records, idx) {
297         Ext.each(records, function (attachment) {
298             if (attachment.get('url')) {
299                 // we can't use Ext.data.connection here as we can't control xhr obj. directly :-(
300                 var me = this,
301                     url = attachment.get('url'),
302                     name = url.split('/').pop(),
303                     xhr = new XMLHttpRequest();
304
305                 xhr.open('GET', url, true);
306                 xhr.responseType = 'blob';
307
308                 store.suspendEvents();
309                 attachment.set('name', name);
310                 attachment.set('type', name.split('.').pop());
311                 store.resumeEvents();
312
313                 xhr.onprogress = function (e) {
314                     var progress = Math.floor(100 * e.loaded / e.total) + '% loaded';
315                     console.log(e);
316                 };
317
318
319                 xhr.onload = function (e) {
320 //                    attachment.set('type', xhr.response.type);
321 //                    attachment.set('size', xhr.response.size);
322
323                     var upload = new Ext.ux.file.Upload({
324                         file: new File([xhr.response], name),
325                         type: xhr.response.type,
326                         size: xhr.response.size
327                     });
328                     // work around chrome bug which dosn't take type from blob
329                     upload.file.fileType = xhr.response.type;
330
331                     var uploadKey = Tine.Tinebase.uploadManager.queueUpload(upload);
332                     var fileRecord = Tine.Tinebase.uploadManager.upload(uploadKey);
333
334                     upload.on('uploadfailure', me.onUploadFail, me);
335                     upload.on('uploadcomplete', me.onUploadComplete, fileRecord);
336                     upload.on('uploadstart', Tine.Tinebase.uploadManager.onUploadStart, me);
337
338                     store.remove(attachment);
339                     store.add(fileRecord);
340
341                 };
342
343                 xhr.send();
344
345             }
346         }, this);
347     },
348
349     /**
350      * On download attached file from panel
351      */
352     onDownload: function() {
353         var selectedRows = this.getSelectionModel().getSelections();
354
355         selectedRows.forEach(function (attachement) {
356             new Ext.ux.file.Download({
357                 params: {
358                     requestType: 'HTTP',
359                     method: 'Tinebase.downloadTempfile',
360                     tmpFileId: attachement.get('id')
361                 }
362             }).start();
363         });
364     },
365
366     /**
367      * returns add action
368      * 
369      * @return {Object} add action config
370      */
371     getAddAction: function () {
372         return {
373             text: String.format(i18n._('Add {0}'), this.i18nFileString),
374             iconCls: 'action_add',
375             scope: this,
376             plugins: [{
377                 ptype: 'ux.browseplugin',
378                 multiple: true,
379                 dropElSelector: 'div[id=' + this.id + ']'
380             }],
381             handler: this.onFilesSelect
382         };
383     },
384     
385     /**
386      * populate grid store
387      *
388      * @param {} record
389      */
390     loadRecord: function (record) {
391         if (record && record.get(this.filesProperty)) {
392             var files = record.get(this.filesProperty);
393             for (var i = 0; i < files.length; i += 1) {
394                 var file = new Ext.ux.file.Upload.file(files[i]);
395                 file.data.status = 'complete';
396                 this.store.add(file);
397             }
398         }
399     },
400     
401     /**
402      * init cm
403      */
404     initColumnModel: function () {
405         this.cm = new Ext.grid.ColumnModel(this.getColumns());
406     },
407     
408     getColumns: function() {
409         var columns = [{
410             resizable: true,
411             id: 'name',
412             dataIndex: 'name',
413             width: 300,
414             header: i18n._('name'),
415             renderer: Ext.ux.PercentRendererWithName
416         }, {
417             resizable: true,
418             id: 'size',
419             dataIndex: 'size',
420             width: 70,
421             header: i18n._('size'),
422             renderer: Ext.util.Format.fileSize
423         }, {
424             resizable: true,
425             id: 'type',
426             dataIndex: 'type',
427             width: 70,
428             header: i18n._('type')
429             // TODO show type icon?
430             //renderer: Ext.util.Format.fileSize
431         }];
432         
433         return columns;
434     },
435
436     /**
437      * init sel model
438      * @private
439      */
440     initSelectionModel: function () {
441         this.selModel = new Ext.grid.RowSelectionModel({multiSelect: true});
442         
443         this.selModel.on('selectionchange', function (selModel) {
444             var rowCount = selModel.getCount();
445             this.action_remove.setDisabled(this.readOnly || rowCount === 0);
446             this.actionUpdater.updateActions(selModel);
447
448         }, this);
449     },
450     
451     /**
452      * upload new file and add to store
453      * 
454      * @param {} btn
455      * @param {} e
456      */
457     onFilesSelect: function (fileSelector, e) {
458         var files = fileSelector.getFileList();
459         Ext.each(files, function (file) {
460
461             var upload = new Ext.ux.file.Upload({
462                 file: file,
463                 fileSelector: fileSelector
464             });
465
466             var uploadKey = Tine.Tinebase.uploadManager.queueUpload(upload);
467             var fileRecord = Tine.Tinebase.uploadManager.upload(uploadKey);
468             
469             upload.on('uploadfailure', this.onUploadFail, this);
470             upload.on('uploadcomplete', this.onUploadComplete, fileRecord);
471             upload.on('uploadstart', Tine.Tinebase.uploadManager.onUploadStart, this);
472
473             if(fileRecord.get('status') !== 'failure' ) {
474                 this.store.add(fileRecord);
475             }
476
477             
478         }, this);
479         
480     },
481
482     /**
483      * Add one or more files from filemanager
484      *
485      * @param nodes
486      */
487     onFileSelectFromFilemanager: function (nodes) {
488         var me = this;
489
490         Ext.each(nodes, function(node) {
491             var record = new Tine.Filemanager.Model.Node(node);
492
493             if (me.store.find('name', record.get('name')) === -1) {
494                 me.store.add(record);
495             } else {
496                 Ext.MessageBox.show({
497                     title: i18n._('Failure'),
498                     msg: i18n._('This file is already attached to this record.'),
499                     buttons: Ext.MessageBox.OK,
500                     icon: Ext.MessageBox.ERROR
501                 });
502             }
503         });
504     },
505     
506     onUploadComplete: function(upload, fileRecord) {
507         fileRecord.beginEdit();
508         fileRecord.set('status', 'complete');
509         fileRecord.set('progress', 100);
510         fileRecord.commit(false);
511         Tine.Tinebase.uploadManager.onUploadComplete();
512     },
513     
514     /**
515      * returns true if files are uploading atm
516      * 
517      * @return {Boolean}
518      */
519     isUploading: function () {
520         var uploadingFiles = this.store.query('status', 'uploading');
521         return (uploadingFiles.getCount() > 0);
522     },
523     
524     isPauseEnabled: function(action, grants, records) {
525          
526         for(var i=0; i<records.length; i++) {
527             if(records[i].get('type') === 'folder') {
528                 action.hide();
529                 return;
530             }
531         }
532         
533         for(var i=0; i < records.length; i++) {
534             if(!records[i].get('status') || (records[i].get('type ') !== 'folder' && records[i].get('status') !== 'paused'
535                     &&  records[i].get('status') !== 'uploading' && records[i].get('status') !== 'pending')) {
536                 action.hide();
537                 return;
538             }
539         }
540         
541         action.show();
542         
543         for(var i=0; i < records.length; i++) {
544             if(records[i].get('status')) {
545                 action.setDisabled(false);
546             }
547             else {
548                 action.setDisabled(true);
549             }
550             if(records[i].get('status') && records[i].get('status') !== 'uploading'){
551                 action.setDisabled(true);
552             }
553             
554         }             
555     },
556
557     isResumeEnabled: function(action, grants, records) {
558         for(var i=0; i<records.length; i++) {
559             if(records[i].get('type') === 'folder') {
560                 action.hide();
561                 return;
562             }
563         }
564        
565         for(var i=0; i < records.length; i++) {
566             if(!records[i].get('status') || (records[i].get('type ') !== 'folder' &&  records[i].get('status') !== 'uploading' 
567                     &&  records[i].get('status') !== 'paused' && records[i].get('status') !== 'pending')) {
568                 action.hide();
569                 return;
570             }
571         }
572         
573         action.show();
574         
575         for(var i=0; i < records.length; i++) {
576             if(records[i].get('status')) {
577                 action.setDisabled(false);
578             }
579             else {
580                 action.setDisabled(true);
581             }
582             if(records[i].get('status') && records[i].get('status') !== 'paused'){
583                 action.setDisabled(true);
584             }
585             
586         }   
587     }
588 });