فهرست منبع

Merge remote-tracking branch 'upstream/devel' into devel

Xavier Priour 9 سال پیش
والد
کامیت
569f8d50ba

+ 2 - 2
.meteor/versions

@@ -63,7 +63,7 @@ id-map@1.0.4
 idmontie:migrations@1.0.1
 jquery@1.11.4
 kadira:blaze-layout@2.2.0
-kadira:dochead@1.3.2
+kadira:dochead@1.4.0
 kadira:flow-router@2.9.0
 kenton:accounts-sandstorm@0.1.8
 launch-screen@1.0.4
@@ -95,7 +95,7 @@ mquandalle:jade@0.4.5
 mquandalle:jade-compiler@0.4.5
 mquandalle:jquery-textcomplete@0.8.0_1
 mquandalle:jquery-ui-drag-drop-sort@0.1.0
-mquandalle:moment@1.0.0
+mquandalle:moment@1.0.1
 mquandalle:mousetrap-bindglobal@0.0.1
 mquandalle:perfect-scrollbar@0.6.5_2
 mquandalle:stylus@1.1.1

+ 7 - 5
History.md

@@ -2,14 +2,16 @@
 
 This release features:
 
-* Card import from Trello
+* Trello boards and cards importation, including card history, assigned members,
+  labels, comments, and attachments;
 * Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start the
-  a board member autocompletion, or <kbd>#</kbd> for a label.
+  a board member autocompletion, or <kbd>#</kbd> for a label;
 * Accelerate the initial page rendering by sending the data on the intial HTTP
-  response instead of waiting for the DDP connection to open.
+  response instead of waiting for the DDP connection to open;
+* Support images attachments copy pasting.
 
-Thanks to GitHub users AlexanderS, fisle, FuzzyWuzzie, ndarilek, and
-xavierpriour for their contributions.
+Thanks to GitHub users AlexanderS, fisle, floatinghotpot, FuzzyWuzzie, mnutt,
+ndarilek, SirCmpwn, and xavierpriour for their contributions.
 
 # v0.9
 

+ 3 - 3
client/components/boards/boardBody.js

@@ -134,7 +134,7 @@ Template.boardBody.onRendered(function() {
   if (!Meteor.user() || !Meteor.user().isBoardMember())
     return;
 
-  self.$(self.listsDom).sortable({
+  $(self.listsDom).sortable({
     tolerance: 'pointer',
     helper: 'clone',
     handle: '.js-list-header',
@@ -146,7 +146,7 @@ Template.boardBody.onRendered(function() {
       Popup.close();
     },
     stop() {
-      self.$('.js-lists').find('.js-list:not(.js-list-composer)').each(
+      $(self.listsDom).find('.js-list:not(.js-list-composer)').each(
         (i, list) => {
           const data = Blaze.getData(list);
           Lists.update(data._id, {
@@ -161,7 +161,7 @@ Template.boardBody.onRendered(function() {
 
   // Disable drag-dropping while in multi-selection mode
   self.autorun(() => {
-    self.$(self.listsDom).sortable('option', 'disabled',
+    $(self.listsDom).sortable('option', 'disabled',
       MultiSelection.isActive());
   });
 

+ 11 - 1
client/components/cards/attachments.jade

@@ -3,6 +3,16 @@ template(name="cardAttachmentsPopup")
     li
       input.js-attach-file.hide(type="file" name="file" multiple)
       a.js-computer-upload {{_ 'computer'}}
+    li
+      a.js-upload-clipboard-image {{_ 'clipboard'}}
+
+template(name="previewClipboardImagePopup")
+  p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}}
+  img.preview-clipboard-image()
+  button.primary.js-upload-pasted-image {{_ 'upload'}}
+
+template(name="previewAttachedImagePopup")
+  img.preview-large-image.js-large-image-clicked(src="{{pathFor url}}")
 
 template(name="attachmentDeletePopup")
   p {{_ "attachment-delete-pop"}}
@@ -15,7 +25,7 @@ template(name="attachmentsGalery")
         .attachment-thumbnail
           if isUploaded
             if isImage
-              img.attachment-thumbnail-img(src="{{pathFor url}}")
+              img.attachment-thumbnail-img.js-preview-image(src="{{pathFor url}}")
             else
               span.attachment-thumbnail-ext= extension
           else

+ 78 - 1
client/components/cards/attachments.js

@@ -20,6 +20,39 @@ Template.attachmentsGalery.events({
   'click .js-remove-cover'() {
     Cards.findOne(this.cardId).unsetCover();
   },
+  'click .js-preview-image'(evt) {
+    Popup.open('previewAttachedImage').call(this, evt);
+    // when multiple thumbnails, if click one then another very fast, 
+    // we might get a wrong width from previous img.
+    // when popup reused, onRendered() won't be called, so we cannot get there.
+    // here make sure to get correct size when this img fully loaded.
+    const img = $('img.preview-large-image')[0];
+    if (!img) return;
+    const rePosPopup = () => {
+      const w = img.width;
+      const h = img.height;
+      // if the image is too large, we resize & center the popup.
+      if (w > 300) {
+        $('div.pop-over').css({
+          width: (w + 20),
+          position: 'absolute',
+          left: (window.innerWidth - w)/2,
+          top: (window.innerHeight - h)/2,
+        });
+      }
+    };
+    const url = $(evt.currentTarget).attr('src');
+    if (img.src === url && img.complete)
+      rePosPopup();
+    else
+      img.onload = rePosPopup;
+  },
+});
+
+Template.previewAttachedImagePopup.events({
+  'click .js-large-image-clicked'(){
+    Popup.close();
+  },
 });
 
 Template.cardAttachmentsPopup.events({
@@ -28,7 +61,7 @@ Template.cardAttachmentsPopup.events({
     FS.Utility.eachFile(evt, (f) => {
       const file = new FS.File(f);
       file.boardId = card.boardId;
-      file.cardId  = card._id;
+      file.cardId = card._id;
 
       Attachments.insert(file);
       Popup.close();
@@ -38,4 +71,48 @@ Template.cardAttachmentsPopup.events({
     tpl.find('.js-attach-file').click();
     evt.preventDefault();
   },
+  'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
+});
+
+let pastedResults = null;
+
+Template.previewClipboardImagePopup.onRendered(() => {
+  // we can paste image from clipboard
+  $(document.body).pasteImageReader((results) => {
+    if (results.dataURL.startsWith('data:image/')) {
+      $('img.preview-clipboard-image').attr('src', results.dataURL);
+      pastedResults = results;
+    }
+  });
+
+  // we can also drag & drop image file to it
+  $(document.body).dropImageReader((results) => {
+    if (results.dataURL.startsWith('data:image/')) {
+      $('img.preview-clipboard-image').attr('src', results.dataURL);
+      pastedResults = results;
+    }
+  });
+});
+
+Template.previewClipboardImagePopup.events({
+  'click .js-upload-pasted-image'() {
+    const results = pastedResults;
+    if (results && results.file) {
+      const card = this;
+      const file = new FS.File(results.file);
+      if (!results.name) {
+        // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type
+        if (typeof results.file.type === 'string') {
+          file.name(results.file.type.replace('image/', 'clipboard.'));
+        }
+      }
+      file.updatedAt(new Date());
+      file.boardId = card.boardId;
+      file.cardId = card._id;
+      Attachments.insert(file);
+      pastedResults = null;
+      $(document.body).pasteImageReader(() => {});
+      Popup.close();
+    }
+  },
 });

+ 11 - 0
client/components/cards/attachments.styl

@@ -45,3 +45,14 @@
   display: block
   box-shadow: 0 1px 2px rgba(0,0,0,.2)
 
+.preview-large-image
+  max-width: 1000px
+  display: block
+  box-shadow: 0 1px 2px rgba(0,0,0,.2)
+
+.preview-clipboard-image
+  width: 280px
+  height: 200px
+  display: block
+  border: 1px solid black
+  box-shadow: 0 1px 2px rgba(0,0,0,.2)

+ 0 - 5
client/components/main/keyboardShortcuts.styl

@@ -14,11 +14,6 @@
         padding: 5px 8px
         margin: 5px
         font-size: 18px
-        font-weight: bold
-        background: white
-        border-radius: 3px
-        border: 1px solid darken(white, 10%)
-        box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
 
     .shortcuts-list-item-action
       font-size: 1.4em

+ 9 - 0
client/components/main/layouts.styl

@@ -172,6 +172,15 @@ dl, dt
 dd
   margin: 0 0 16px 24px
 
+kbd
+  padding: 1px 3px
+  margin: 3px
+  font-weight: bold
+  background: darken(white, 2%)
+  border-radius: 3px
+  border: 1px solid darken(white, 10%)
+  box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
+
 .clear
   clear: both
 

+ 62 - 0
client/lib/dropImage.js

@@ -0,0 +1,62 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/drop.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+  $.event.fix = (function(originalFix) {
+    return function(event) {
+      event = originalFix.apply(this, arguments);
+      if (event.type.indexOf('drag') === 0 || event.type.indexOf('drop') === 0) {
+        event.dataTransfer = event.originalEvent.dataTransfer;
+      }
+      return event;
+    };
+  })($.event.fix);
+
+  const defaults = {
+    callback: $.noop,
+    matchType: /image.*/,
+  };
+
+  return $.fn.dropImageReader = function(options) {
+    if (typeof options === 'function') {
+      options = {
+        callback: options,
+      };
+    }
+    options = $.extend({}, defaults, options);
+    const stopFn = function(event) {
+      event.stopPropagation();
+      return event.preventDefault();
+    };
+    return this.each(function() {
+      const element = this;
+      $(element).bind('dragenter dragover dragleave', stopFn);
+      return $(element).bind('drop', function(event) {
+        stopFn(event);
+        const files = event.dataTransfer.files;
+        for(let i=0; i<files.length; i++) {
+          const f = files[i];
+          if(f.type.match(options.matchType)) {
+            const reader = new FileReader();
+            reader.onload = function(evt) {
+              return options.callback.call(element, {
+                dataURL: evt.target.result,
+                event: evt,
+                file: f,
+                name: f.name,
+              });
+            };
+            reader.readAsDataURL(f);
+            return;
+          }
+        }
+      });
+    });
+  };
+})(jQuery);

+ 57 - 0
client/lib/pasteImage.js

@@ -0,0 +1,57 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/paste.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+  $.event.fix = (function(originalFix) {
+    return function(event) {
+      event = originalFix.apply(this, arguments);
+      if (event.type.indexOf('copy') === 0 || event.type.indexOf('paste') === 0) {
+        event.clipboardData = event.originalEvent.clipboardData;
+      }
+      return event;
+    };
+  })($.event.fix);
+
+  const defaults = {
+    callback: $.noop,
+    matchType: /image.*/,
+  };
+
+  return $.fn.pasteImageReader = function(options) {
+    if (typeof options === 'function') {
+      options = {
+        callback: options,
+      };
+    }
+    options = $.extend({}, defaults, options);
+    return this.each(function() {
+      const element = this;
+      return $(element).bind('paste', function(event) {
+        const types = event.clipboardData.types;
+        const items = event.clipboardData.items;
+        for(let i=0; i<types.length; i++) {
+          if(types[i].match(options.matchType) || items[i].type.match(options.matchType)) {
+            const f = items[i].getAsFile();
+            const reader = new FileReader();
+            reader.onload = function(evt) {
+              return options.callback.call(element, {
+                dataURL: evt.target.result,
+                event: evt,
+                file: f,
+                name: f.name,
+              });
+            };
+            reader.readAsDataURL(f);
+            return;
+          }
+        }
+      });
+    });
+  };
+})(jQuery);

+ 6 - 0
config/accounts.js

@@ -46,3 +46,9 @@ AccountsTemplates.configureRoute('changePwd', {
     Popup.back();
   },
 });
+
+if (Meteor.isServer) {
+  if (process.env.MAIL_FROM) {
+    Accounts.emailTemplates.from = process.env.MAIL_FROM;
+  }
+}

+ 6 - 0
i18n/en.i18n.json

@@ -87,6 +87,7 @@
     "changePermissionsPopup-title": "Change Permissions",
     "click-to-star": "Click to star this board.",
     "click-to-unstar": "Click to unstar this board.",
+    "clipboard" : "Clipboard or drag & drop",
     "close": "Close",
     "close-board": "Close Board",
     "close-board-pop": "You can re-open the board by clicking the “Boards” menu from the header, selecting “View Closed Boards”, finding the board and clicking “Re-open”.",
@@ -189,6 +190,10 @@
     "page-maybe-private": "This page may be private. You may be able to view it by <a href='%s'>logging in</a>.",
     "page-not-found": "Page not found.",
     "password": "Password",
+    "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+    "preview": "Preview",
+    "previewClipboardImagePopup-title": "Preview",
+    "previewAttachedImagePopup-title": "Preview",
     "private": "Private",
     "private-desc": "This board is private. Only people added to the board can view and edit it.",
     "profile": "Profile",
@@ -228,6 +233,7 @@
     "title": "Title",
     "unassign-member": "Unassign member",
     "unsaved-description": "You have an unsaved description.",
+    "upload": "Upload",
     "upload-avatar": "Upload an avatar",
     "uploaded-avatar": "Uploaded an avatar",
     "username": "Username",

+ 18 - 2
sandstorm.js

@@ -54,10 +54,10 @@ if (isSandstorm && Meteor.isServer) {
     // XXX If this routing scheme changes, this will break. We should generate
     // the location URL using the router, but at the time of writing, the
     // it is only accessible on the client.
-    const path = `/boards/${sandstormBoard._id}/${sandstormBoard.slug}`;
+    const boardPath = `/b/${sandstormBoard._id}/${sandstormBoard.slug}`;
 
     res.writeHead(301, {
-      Location: base + path,
+      Location: base + boardPath,
     });
     res.end();
 
@@ -126,6 +126,22 @@ if (isSandstorm && Meteor.isServer) {
 }
 
 if (isSandstorm && Meteor.isClient) {
+  // Since the Sandstorm grain is displayed in an iframe of the Sandstorm shell,
+  // we need to explicitly expose meta data like the page title or the URL path
+  // so that they could appear in the browser window.
+  // See https://docs.sandstorm.io/en/latest/developing/path/
+  function updateSandstormMetaData(msg) {
+    return window.parent.postMessage(msg, '*');
+  }
+
+  FlowRouter.triggers.enter([({ path }) => {
+    updateSandstormMetaData({ setPath: path });
+  }]);
+
+  Tracker.autorun(() => {
+    updateSandstormMetaData({ setTitle: DocHead.getTitle() });
+  });
+
   // XXX Hack. `Meteor.absoluteUrl` doesn't work in Sandstorm, since every
   // session has a different URL whereas Meteor computes absoluteUrl based on
   // the ROOT_URL environment variable. So we overwrite this function on a