| 1 | /// crmFile: Manage file attachments |
| 2 | (function (angular, $, _) { |
| 3 | |
| 4 | angular.module('crmAttachment', ['angularFileUpload']); |
| 5 | |
| 6 | // crmAttachment manages the list of files which are attached to a given entity |
| 7 | angular.module('crmAttachment').factory('CrmAttachments', function (crmApi, crmStatus, FileUploader, $q) { |
| 8 | // @param target an Object(entity_table:'',entity_id:'') or function which generates an object |
| 9 | function CrmAttachments(target) { |
| 10 | var crmAttachments = this; |
| 11 | this._target = target; |
| 12 | this.files = []; |
| 13 | this.trash = []; |
| 14 | this.uploader = new FileUploader({ |
| 15 | url: CRM.url('civicrm/ajax/attachment'), |
| 16 | onAfterAddingFile: function onAfterAddingFile(item) { |
| 17 | item.crmData = { |
| 18 | description: '' |
| 19 | }; |
| 20 | }, |
| 21 | onSuccessItem: function onSuccessItem(item, response, status, headers) { |
| 22 | crmAttachments.files.push(response.file.values[response.file.id]); |
| 23 | crmAttachments.uploader.removeFromQueue(item); |
| 24 | }, |
| 25 | onErrorItem: function onErrorItem(item, response, status, headers) { |
| 26 | var msg = (response && response.file && response.file.error_message) ? response.file.error_message : ts('Unknown error'); |
| 27 | CRM.alert(item.file.name + ' - ' + msg, ts('Attachment failed')); |
| 28 | crmAttachments.uploader.removeFromQueue(item); |
| 29 | } |
| 30 | }); |
| 31 | } |
| 32 | |
| 33 | angular.extend(CrmAttachments.prototype, { |
| 34 | // @return Object(entity_table:'',entity_id:'') |
| 35 | getTarget: function () { |
| 36 | return (angular.isFunction(this._target) ? this._target() : this._target); |
| 37 | }, |
| 38 | // @return Promise<Attachment> |
| 39 | load: function load() { |
| 40 | var target = this.getTarget(); |
| 41 | var Attachment = this; |
| 42 | |
| 43 | if (target.entity_id) { |
| 44 | var params = { |
| 45 | entity_table: target.entity_table, |
| 46 | entity_id: target.entity_id |
| 47 | }; |
| 48 | return crmApi('Attachment', 'get', params).then(function (apiResult) { |
| 49 | Attachment.files = _.values(apiResult.values); |
| 50 | return Attachment; |
| 51 | }); |
| 52 | } |
| 53 | else { |
| 54 | var dfr = $q.defer(); |
| 55 | Attachment.files = []; |
| 56 | dfr.resolve(Attachment); |
| 57 | return dfr.promise; |
| 58 | } |
| 59 | }, |
| 60 | // @return Promise |
| 61 | save: function save() { |
| 62 | var crmAttachments = this; |
| 63 | var target = this.getTarget(); |
| 64 | if (!target.entity_table || !target.entity_id) { |
| 65 | throw "Cannot save attachments: unknown entity_table or entity_id"; |
| 66 | } |
| 67 | |
| 68 | var params = _.extend({}, target); |
| 69 | params.values = crmAttachments.files; |
| 70 | return crmApi('Attachment', 'replace', params) |
| 71 | .then(function () { |
| 72 | var dfr = $q.defer(); |
| 73 | |
| 74 | var newItems = crmAttachments.uploader.getNotUploadedItems(); |
| 75 | if (newItems.length > 0) { |
| 76 | _.each(newItems, function (item) { |
| 77 | item.formData = [_.extend({crm_attachment_token: CRM.crmAttachment.token}, target, item.crmData)]; |
| 78 | }); |
| 79 | crmAttachments.uploader.onCompleteAll = function onCompleteAll() { |
| 80 | delete crmAttachments.uploader.onCompleteAll; |
| 81 | dfr.resolve(crmAttachments); |
| 82 | }; |
| 83 | crmAttachments.uploader.uploadAll(); |
| 84 | } |
| 85 | else { |
| 86 | dfr.resolve(crmAttachments); |
| 87 | } |
| 88 | |
| 89 | return dfr.promise; |
| 90 | }); |
| 91 | }, |
| 92 | // Compute a digest over the list of files. The signature should change if the attachment list has changed |
| 93 | // (become dirty). |
| 94 | getAutosaveSignature: function getAutosaveSignature() { |
| 95 | var sig = []; |
| 96 | // Attachments have a special lifecycle, and attachments.queue is not properly serializable, so |
| 97 | // it takes some special effort to figure out a suitable signature. Issues which can cause gratuitous saving: |
| 98 | // - Files move from this.uploader.queue to this.files after upload. |
| 99 | // - File names are munged after upload. |
| 100 | // - Deletes are performed immediately (outside the save process). |
| 101 | angular.forEach(this.files, function(item) { |
| 102 | sig.push({f: item.name.replace(/[^a-zA0-Z0-9\.]/, '_'), d: item.description}); |
| 103 | }); |
| 104 | angular.forEach(this.uploader.queue, function(item) { |
| 105 | sig.push({f: item.file.name.replace(/[^a-zA0-Z0-9\.]/, '_'), d: item.crmData.description}); |
| 106 | }); |
| 107 | angular.forEach(this.trash, function(item) { |
| 108 | sig.push({f: item.name.replace(/[^a-zA0-Z0-9\.]/, '_'), d: item.description}); |
| 109 | }); |
| 110 | return _.sortBy(sig, 'name'); |
| 111 | }, |
| 112 | // @param Object file APIv3 attachment record (e.g. id, entity_table, entity_id, description) |
| 113 | deleteFile: function deleteFile(file) { |
| 114 | var crmAttachments = this; |
| 115 | |
| 116 | var idx = _.indexOf(this.files, file); |
| 117 | if (idx != -1) { |
| 118 | this.files.splice(idx, 1); |
| 119 | } |
| 120 | |
| 121 | this.trash.push(file); |
| 122 | |
| 123 | if (file.id) { |
| 124 | var p = crmApi('Attachment', 'delete', {id: file.id}).then( |
| 125 | function () { // success |
| 126 | }, |
| 127 | function (response) { // error; restore the file |
| 128 | var msg = angular.isObject(response) ? response.error_message : ''; |
| 129 | CRM.alert(msg, ts('Deletion failed')); |
| 130 | crmAttachments.files.push(file); |
| 131 | |
| 132 | var trashIdx = _.indexOf(crmAttachments.trash, file); |
| 133 | if (trashIdx != -1) { |
| 134 | crmAttachments.trash.splice(trashIdx, 1); |
| 135 | } |
| 136 | } |
| 137 | ); |
| 138 | return crmStatus({start: ts('Deleting...'), success: ts('Deleted')}, p); |
| 139 | } |
| 140 | } |
| 141 | }); |
| 142 | |
| 143 | return CrmAttachments; |
| 144 | }); |
| 145 | |
| 146 | // example: |
| 147 | // $scope.myAttachments = new CrmAttachments({entity_table: 'civicrm_mailing', entity_id: 123}); |
| 148 | // <div crm-attachments="myAttachments"/> |
| 149 | angular.module('crmAttachment').directive('crmAttachments', function ($parse, $timeout) { |
| 150 | return { |
| 151 | scope: { |
| 152 | crmAttachments: '@' |
| 153 | }, |
| 154 | template: '<div ng-if="ready" ng-include="inclUrl"></div>', |
| 155 | link: function (scope, elm, attr) { |
| 156 | var model = $parse(attr.crmAttachments); |
| 157 | scope.att = model(scope.$parent); |
| 158 | scope.ts = CRM.ts(null); |
| 159 | scope.inclUrl = '~/crmAttachment/attachments.html'; |
| 160 | |
| 161 | // delay rendering of child tree until after model has been populated |
| 162 | scope.ready = true; |
| 163 | } |
| 164 | }; |
| 165 | }); |
| 166 | |
| 167 | })(angular, CRM.$, CRM._); |