mirror of
https://github.com/wekan/wekan.git
synced 2025-04-23 21:47:10 -04:00
Merge remote-tracking branch 'upstream/devel' into devel
This commit is contained in:
commit
569f8d50ba
13 changed files with 270 additions and 19 deletions
|
@ -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
|
||||
|
|
12
History.md
12
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
|
||||
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
client/lib/dropImage.js
Normal file
62
client/lib/dropImage.js
Normal file
|
@ -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
client/lib/pasteImage.js
Normal file
57
client/lib/pasteImage.js
Normal file
|
@ -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);
|
|
@ -46,3 +46,9 @@ AccountsTemplates.configureRoute('changePwd', {
|
|||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
if (process.env.MAIL_FROM) {
|
||||
Accounts.emailTemplates.from = process.env.MAIL_FROM;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
20
sandstorm.js
20
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue