diff options
Diffstat (limited to 'client')
28 files changed, 471 insertions, 173 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index 5be953b6..deb73072 100644 --- a/client/components/activities/activities.jade +++ b/client/components/activities/activities.jade @@ -201,6 +201,7 @@ template(name="cardActivities") .activity-checklist(href="{{ card.absoluteUrl }}") +viewer = checklistItem.title + if(currentData.timeKey) | {{{_ activityType }}} = ' ' @@ -215,6 +216,10 @@ template(name="cardActivities") | {{{_ activityType currentData.timeValue}}} + if($eq activityType 'deleteComment') + | {{{_ 'activity-deleteComment' currentData.commentId}}}. + if($eq activityType 'editComment') + | {{{_ 'activity-editComment' currentData.commentId}}}. if($eq activityType 'addComment') +inlinedForm(classNames='js-edit-comment') +editor(autofocus=true) diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index 05149826..b082273a 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -85,7 +85,7 @@ BlazeComponent.extendComponent({ const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById( lastLabelId, ); - if (lastLabel.name === undefined || lastLabel.name === '') { + if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) { return lastLabel.color; } else { return lastLabel.name; diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js index 8289b628..95084646 100644 --- a/client/components/activities/comments.js +++ b/client/components/activities/comments.js @@ -38,6 +38,7 @@ BlazeComponent.extendComponent({ resetCommentInput(input); Tracker.flush(); autosize.update(input); + input.trigger('submitted'); } evt.preventDefault(); }, @@ -54,7 +55,7 @@ BlazeComponent.extendComponent({ // XXX This should be a static method of the `commentForm` component function resetCommentInput(input) { - input.val('').trigger('input'); // without manually trigger, input event won't be fired + input.val(''); // without manually trigger, input event won't be fired input.blur(); commentFormIsOpen.set(false); } diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index fd094a93..76a85d87 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -7,8 +7,8 @@ template(name="board") +boardBody else //-- XXX We need a better error message in case the board has been archived - //-- +message(label="board-not-found") - | {{goHome}} + +message(label="board-not-found") + //-- | {{goHome}} else +spinner diff --git a/client/components/boards/boardColors.styl b/client/components/boards/boardColors.styl index efd4367e..3be9c0c3 100644 --- a/client/components/boards/boardColors.styl +++ b/client/components/boards/boardColors.styl @@ -241,6 +241,7 @@ setBoardColor(color) background-color #ffffff !important padding 15px !important border 1px solid #000000 !important + word-wrap: break-word // When card has comment, emphasis on minicard: // bigger red comment icon and number of comments, diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade index 85f47963..79bae502 100644 --- a/client/components/boards/boardsList.jade +++ b/client/components/boards/boardsList.jade @@ -31,12 +31,28 @@ template(name="boardList") i.fa.js-has-spenttime-cards( class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}" title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}") - i.fa.js-clone-board( - class="fa-clone" - title="{{_ 'duplicate-board'}}") - i.fa.js-archive-board( - class="fa-archive" - title="{{_ 'archive-board'}}") + unless isMiniScreen + if isSandstorm + i.fa.js-clone-board( + class="fa-clone" + title="{{_ 'duplicate-board'}}") + i.fa.js-archive-board( + class="fa-archive" + title="{{_ 'archive-board'}}") + else if currentUser.isBoardAdmin + i.fa.js-clone-board( + class="fa-clone" + title="{{_ 'duplicate-board'}}") + i.fa.js-archive-board( + class="fa-archive" + title="{{_ 'archive-board'}}") + else if currentUser.isAdmin + i.fa.js-clone-board( + class="fa-clone" + title="{{_ 'duplicate-board'}}") + i.fa.js-archive-board( + class="fa-archive" + title="{{_ 'archive-board'}}") template(name="boardListHeaderBar") h1 {{_ 'my-boards'}} diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index b1371747..3918af82 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -8,10 +8,10 @@ Template.boardListHeaderBar.events({ Template.boardListHeaderBar.helpers({ templatesBoardId() { - return Meteor.user().getTemplatesBoardId(); + return Meteor.user() && Meteor.user().getTemplatesBoardId(); }, templatesBoardSlug() { - return Meteor.user().getTemplatesBoardSlug(); + return Meteor.user() && Meteor.user().getTemplatesBoardSlug(); }, }); diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index f536a655..843f1eb7 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -55,24 +55,12 @@ Template.cardAttachmentsPopup.events({ 'change .js-attach-file'(event) { const card = this; const processFile = f => { - const file = new FS.File(f); - if (card.isLinkedCard()) { - file.boardId = Cards.findOne(card.linkedId).boardId; - file.cardId = card.linkedId; - } else { - file.boardId = card.boardId; - file.swimlaneId = card.swimlaneId; - file.listId = card.listId; - file.cardId = card._id; - } - file.userId = Meteor.userId(); - const attachment = Attachments.insert(file); - - if (attachment && attachment._id && attachment.isImage()) { - card.setCover(attachment._id); - } - - Popup.close(); + Utils.processUploadedAttachment(card, f, attachment => { + if (attachment && attachment._id && attachment.isImage()) { + card.setCover(attachment._id); + } + Popup.close(); + }); }; FS.Utility.eachFile(event, f => { @@ -86,7 +74,7 @@ Template.cardAttachmentsPopup.events({ reader.onload = function(e) { const dataurl = e && e.target && e.target.result; if (dataurl !== undefined) { - shrinkImage({ + Utils.shrinkImage({ dataurl, maxSize: MAX_IMAGE_PIXEL, ratio: COMPRESS_RATIO, @@ -118,59 +106,9 @@ Template.cardAttachmentsPopup.events({ 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'), }); -const MAX_IMAGE_PIXEL = Meteor.settings.public.MAX_IMAGE_PIXEL; -const COMPRESS_RATIO = Meteor.settings.public.IMAGE_COMPRESS_RATIO; +const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL; +const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO; let pastedResults = null; -const shrinkImage = function(options) { - // shrink image to certain size - const dataurl = options.dataurl, - callback = options.callback, - toBlob = options.toBlob; - let canvas = document.createElement('canvas'), - image = document.createElement('img'); - const maxSize = options.maxSize || 1024; - const ratio = options.ratio || 1.0; - const next = function(result) { - image = null; - canvas = null; - if (typeof callback === 'function') { - callback(result); - } - }; - image.onload = function() { - let width = this.width, - height = this.height; - let changed = false; - if (width > height) { - if (width > maxSize) { - height *= maxSize / width; - width = maxSize; - changed = true; - } - } else if (height > maxSize) { - width *= maxSize / height; - height = maxSize; - changed = true; - } - canvas.width = width; - canvas.height = height; - canvas.getContext('2d').drawImage(this, 0, 0, width, height); - if (changed === true) { - const type = 'image/jpeg'; - if (toBlob) { - canvas.toBlob(next, type, ratio); - } else { - next(canvas.toDataURL(type, ratio)); - } - } else { - next(changed); - } - }; - image.onerror = function() { - next(false); - }; - image.src = dataurl; -}; Template.previewClipboardImagePopup.onRendered(() => { // we can paste image from clipboard @@ -182,7 +120,7 @@ Template.previewClipboardImagePopup.onRendered(() => { }; if (MAX_IMAGE_PIXEL) { // if has size limitation on image we shrink it before uploading - shrinkImage({ + Utils.shrinkImage({ dataurl: results.dataURL, maxSize: MAX_IMAGE_PIXEL, ratio: COMPRESS_RATIO, diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 781967ae..cd8813f5 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -117,6 +117,37 @@ BlazeComponent.extendComponent({ }, onRendered() { + if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) { + // Send Webhook but not create Activities records --- + const card = this.currentData(); + const userId = Meteor.userId(); + //console.log(`userId: ${userId}`); + //console.log(`cardId: ${card._id}`); + //console.log(`boardId: ${card.boardId}`); + //console.log(`listId: ${card.listId}`); + //console.log(`swimlaneId: ${card.swimlaneId}`); + const params = { + userId, + cardId: card._id, + boardId: card.boardId, + listId: card.listId, + user: Meteor.user().username, + url: '', + }; + //console.log('looking for integrations...'); + const integrations = Integrations.find({ + boardId: card.boardId, + type: 'outgoing-webhooks', + enabled: true, + activities: { $in: ['CardDetailsRendered', 'all'] }, + }).fetch(); + //console.log(`Investigation length: ${integrations.length}`); + if (integrations.length > 0) { + Meteor.call('outgoingWebhooks', integrations, 'CardSelected', params); + } + //------------- + } + if (!Utils.isMiniScreen()) { Meteor.setTimeout(() => { $('.card-details').mCustomScrollbar({ diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl index 4bba2d4d..cd475072 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -99,7 +99,9 @@ &.card-details-item-end, &.card-details-item-customfield, &.card-details-item-name - max-width: 50% + display: block + word-wrap: break-word + max-width: 48% flex-grow: 1 .card-details-item-title diff --git a/client/components/cards/checklists.styl b/client/components/cards/checklists.styl index d48c1851..8ac37a15 100644 --- a/client/components/cards/checklists.styl +++ b/client/components/cards/checklists.styl @@ -128,6 +128,9 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item & .viewer p margin-bottom: 2px + display: block + word-wrap: break-word + max-width: 420px .js-delete-checklist-item margin: 0 0 0.5em 1.33em diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl index 3b481d93..9d7c7553 100644 --- a/client/components/cards/labels.styl +++ b/client/components/cards/labels.styl @@ -10,9 +10,10 @@ margin-right: 4px margin-bottom: 5px padding: 3px 8px - max-width: 100% + max-width: 210px min-width: 8px overflow: ellipsis + word-wrap: break-word height: 18px vertical-align: bottom diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index 242367b4..c4172572 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -81,6 +81,7 @@ .minicard-labels float: right display: flex + flex-wrap: wrap .minicard-label width: 11px @@ -92,8 +93,11 @@ .minicard-custom-field display:flex; .minicard-custom-field-item - max-width:50%; - flex-grow:1; + flex-grow: 1 + display: block + word-wrap: break-word + max-width: 100px + margin-right: 4px .handle width: 20px; height: 20px; @@ -111,7 +115,9 @@ p:last-child margin-bottom: 0 .viewer - display: inline-block + display: block + word-wrap: break-word + max-width: 230px .dates display: flex; flex-direction: row; diff --git a/client/components/import/import.js b/client/components/import/import.js index 62c7e525..6368885b 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -211,22 +211,20 @@ BlazeComponent.extendComponent({ this.parentComponent().nextStep(); }, - onMapMember(evt) { - const memberToMap = this.currentData(); - if (memberToMap.wekan) { - // todo xxx ask for confirmation? - this.unmapMember(memberToMap.id); - } else { - this.setSelectedMember(memberToMap.id); - Popup.open('importMapMembersAdd')(evt); - } - }, - events() { return [ { submit: this.onSubmit, - 'click .js-select-member': this.onMapMember, + 'click .js-select-member'(evt) { + const memberToMap = this.currentData(); + if (memberToMap.wekan) { + // todo xxx ask for confirmation? + this.unmapMember(memberToMap.id); + } else { + this.setSelectedMember(memberToMap.id); + Popup.open('importMapMembersAdd')(evt); + } + }, }, ]; }, diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 7d9e358b..c8e41a0b 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -701,12 +701,31 @@ BlazeComponent.extendComponent({ this.listId = this.parentComponent().data()._id; this.swimlaneId = ''; - const boardView = (Meteor.user().profile || {}).boardView; - if (boardView === 'board-view-swimlanes') - this.swimlaneId = this.parentComponent() - .parentComponent() - .parentComponent() - .data()._id; + const isSandstorm = + Meteor.settings && + Meteor.settings.public && + Meteor.settings.public.sandstorm; + + if (isSandstorm) { + const user = Meteor.user(); + if (user) { + const boardView = (Meteor.user().profile || {}).boardView; + if (boardView === 'board-view-swimlanes') { + this.swimlaneId = this.parentComponent() + .parentComponent() + .parentComponent() + .data()._id; + } + } + } else { + const boardView = (Meteor.user().profile || {}).boardView; + if (boardView === 'board-view-swimlanes') { + this.swimlaneId = this.parentComponent() + .parentComponent() + .parentComponent() + .data()._id; + } + } }, onRendered() { diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 98461c4f..91403086 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -1,7 +1,89 @@ +import _sanitizeXss from 'xss'; +const ASIS = 'asis'; +const sanitizeXss = (input, options) => { + const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i; + const allowedIframeSrcRegex = (function() { + let reg = defaultAllowedIframeSrc; + const SAFE_IFRAME_SRC_PATTERN = + Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN; + try { + if (SAFE_IFRAME_SRC_PATTERN !== undefined) { + reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i'); + } + } catch (e) { + /*eslint no-console: ["error", { allow: ["warn", "error"] }] */ + + console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e); + } + return reg; + })(); + const targetWindow = '_blank'; + const getHtmlDOM = html => { + const i = document.createElement('i'); + i.innerHTML = html; + return i.firstChild; + }; + options = { + onTag(tag, html, options) { + const htmlDOM = getHtmlDOM(html); + const getAttr = attr => { + return htmlDOM && attr && htmlDOM.getAttribute(attr); + }; + if (tag === 'iframe') { + const clipCls = 'note-vide-clip'; + if (!options.isClosing) { + const iframeCls = getAttr('class'); + let safe = iframeCls.indexOf(clipCls) > -1; + const src = getAttr('src'); + if (allowedIframeSrcRegex.exec(src)) { + safe = true; + } + if (safe) + return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`; + } else { + // remove </iframe> tag + return ''; + } + } else if (tag === 'a') { + if (!options.isClosing) { + if (getAttr(ASIS) === 'true') { + // if has a ASIS attribute, don't do anything, it's a member id + return html; + } else { + const href = getAttr('href'); + if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) { + // a valid url + return `<a href=${href} target=${targetWindow}>`; + } + } + } + } else if (tag === 'img') { + if (!options.isClosing) { + const src = getAttr('src'); + if (src) { + return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`; + } + } + } + return undefined; + }, + onTagAttr(tag, name, value) { + if (tag === 'img' && name === 'src') { + if (value && value.substr(0, 5) === 'data:') { + // allow image with dataURI src + return `${name}='${value}'`; + } + } else if (tag === 'a' && name === 'target') { + return `${name}='${targetWindow}'`; // always change a href target to a new window + } + return undefined; + }, + ...options, + }; + return _sanitizeXss(input, options); +}; Template.editor.onRendered(() => { const textareaSelector = 'textarea'; - const enableRicherEditor = - Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR || true; const mentions = [ // User mentions { @@ -32,7 +114,7 @@ Template.editor.onRendered(() => { autosize($textarea); $textarea.escapeableTextComplete(mentions); }; - if (enableRicherEditor) { + if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) { const isSmall = Utils.isMiniScreen(); const toolbar = isSmall ? [ @@ -50,47 +132,11 @@ Template.editor.onRendered(() => { ['color', ['color']], ['para', ['ul', 'ol', 'paragraph']], ['table', ['table']], - //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled + ['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled //['insert', ['link', 'picture']], // modal popup has issue somehow :( ['view', ['fullscreen', 'help']], ]; - const cleanPastedHTML = function(input) { - const badTags = [ - 'style', - 'script', - 'applet', - 'embed', - 'noframes', - 'noscript', - 'meta', - 'link', - 'button', - 'form', - ].join('|'); - const badPatterns = new RegExp( - `(?:${[ - `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`, - `<(${badTags})[^>]*?\\/>`, - ].join('|')})`, - 'gi', - ); - let output = input; - // remove bad Tags - output = output.replace(badPatterns, ''); - // remove attributes ' style="..."' - const badAttributes = new RegExp( - `(?:${[ - 'on\\S+=([\'"]?).*?\\1', - 'href=([\'"]?)javascript:.*?\\2', - 'style=([\'"]?).*?\\3', - 'target=\\S+', - ].join('|')})`, - 'gi', - ); - output = output.replace(badAttributes, ''); - output = output.replace(/(<a )/gi, '$1target=_ '); // always to new target - return output; - }; + const cleanPastedHTML = sanitizeXss; const editor = '.editor'; const selectors = [ `.js-new-comment-form ${editor}`, @@ -116,8 +162,8 @@ Template.editor.onRendered(() => { callbacks: { onInit(object) { const originalInput = this; - $(originalInput).on('input', function() { - // when comment is submitted, the original textarea will be set to '', so shall we + $(originalInput).on('submitted', function() { + // resetCommentInput has been called if (!this.value) { const sn = getSummernote(this); sn && sn.summernote('reset'); @@ -134,10 +180,83 @@ Template.editor.onRendered(() => { fBtn.on('click', function() { const $this = $(this), isActive = $this.hasClass('active'); - $('.minicards').toggle(!isActive); // mini card is still showing when editor is in fullscreen mode, we hide here manually + $('.minicards,#header-quick-access').toggle(!isActive); // mini card is still showing when editor is in fullscreen mode, we hide here manually }); } }, + onImageUpload(files) { + const $summernote = getSummernote(this); + if (files && files.length > 0) { + const image = files[0]; + const currentCard = Cards.findOne(Session.get('currentCard')); + const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL; + const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO; + const insertImage = src => { + const img = document.createElement('img'); + img.src = src; + img.setAttribute('width', '100%'); + $summernote.summernote('insertNode', img); + }; + const processData = function(fileObj) { + Utils.processUploadedAttachment( + currentCard, + fileObj, + attachment => { + if ( + attachment && + attachment._id && + attachment.isImage() + ) { + attachment.one('uploaded', function() { + const maxTry = 3; + const checkItvl = 500; + let retry = 0; + const checkUrl = function() { + // even though uploaded event fired, attachment.url() is still null somehow //TODO + const url = attachment.url(); + if (url) { + insertImage( + `${location.protocol}//${location.host}${url}`, + ); + } else { + retry++; + if (retry < maxTry) { + setTimeout(checkUrl, checkItvl); + } + } + }; + checkUrl(); + }); + } + }, + ); + }; + if (MAX_IMAGE_PIXEL) { + const reader = new FileReader(); + reader.onload = function(e) { + const dataurl = e && e.target && e.target.result; + if (dataurl !== undefined) { + // need to shrink image + Utils.shrinkImage({ + dataurl, + maxSize: MAX_IMAGE_PIXEL, + ratio: COMPRESS_RATIO, + toBlob: true, + callback(blob) { + if (blob !== false) { + blob.name = image.name; + processData(blob); + } + }, + }); + } + }; + reader.readAsDataURL(image); + } else { + processData(image); + } + } + }, onPaste() { // clear up unwanted tag info when user pasted in text const thisNote = this; @@ -185,8 +304,6 @@ Template.editor.onRendered(() => { } }); -import sanitizeXss from 'xss'; - // XXX I believe we should compute a HTML rendered field on the server that // would handle markdown and user mentions. We can simply have two // fields, one source, and one compiled version (in HTML) and send only the @@ -231,38 +348,42 @@ Blaze.Template.registerHelper( // `userId` to the popup as usual, and we need to store it in the DOM // using a data attribute. 'data-userId': knowedUser.userId, + [ASIS]: 'true', }, linkValue, ); content = content.replace(fullMention, Blaze.toHTML(link)); } - return HTML.Raw(sanitizeXss(content)); }), ); - Template.viewer.events({ // Viewer sometimes have click-able wrapper around them (for instance to edit // the corresponding text). Clicking a link shouldn't fire these actions, stop // we stop these event at the viewer component level. 'click a'(event, templateInstance) { - event.stopPropagation(); - - // XXX We hijack the build-in browser action because we currently don't have - // `_blank` attributes in viewer links, and the transformer function is - // handled by a third party package that we can't configure easily. Fix that - // by using directly `_blank` attribute in the rendered HTML. - event.preventDefault(); - + let prevent = true; const userId = event.currentTarget.dataset.userid; if (userId) { Popup.open('member').call({ userId }, event, templateInstance); } else { const href = event.currentTarget.href; - if (href) { + const child = event.currentTarget.firstElementChild; + if (child && child.tagName === 'IMG') { + prevent = false; + } else if (href) { window.open(href, '_blank'); } } + if (prevent) { + event.stopPropagation(); + + // XXX We hijack the build-in browser action because we currently don't have + // `_blank` attributes in viewer links, and the transformer function is + // handled by a third party package that we can't configure easily. Fix that + // by using directly `_blank` attribute in the rendered HTML. + event.preventDefault(); + } }, }); diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl index 672e4520..56c35284 100644 --- a/client/components/main/layouts.styl +++ b/client/components/main/layouts.styl @@ -232,6 +232,7 @@ kbd background: darken(white, 2%) border-radius: 3px border: 1px solid darken(white, 10%) + color: unset box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15) .clear @@ -377,6 +378,8 @@ a .viewer min-height: 18px + display: block + word-wrap: break-word ol list-style-type: decimal @@ -424,6 +427,9 @@ a height: 100% margin: 0px + .panel-default + width: 83vw + .inline-input height: 37px margin: 8px 10px 0 0 diff --git a/client/components/settings/informationBody.jade b/client/components/settings/informationBody.jade index feb7c0dc..2c615ffd 100644 --- a/client/components/settings/informationBody.jade +++ b/client/components/settings/informationBody.jade @@ -20,9 +20,21 @@ template(name='statistics') th Wekan {{_ 'info'}} td {{statistics.version}} tr + th {{_ 'Meteor_version'}} + td {{statistics.meteor.meteorVersion}} + tr th {{_ 'Node_version'}} td {{statistics.process.nodeVersion}} tr + th {{_ 'MongoDB_version'}} + td {{statistics.mongo.mongoVersion}} + tr + th {{_ 'MongoDB_storage_engine'}} + td {{statistics.mongo.mongoStorageEngine}} + tr + th {{_ 'MongoDB_Oplog_enabled'}} + td {{statistics.mongo.mongoOplogEnabled}} + tr th {{_ 'OS_Type'}} td {{statistics.os.type}} tr diff --git a/client/components/settings/settingBody.styl b/client/components/settings/settingBody.styl index b9300782..bcbd2ea1 100644 --- a/client/components/settings/settingBody.styl +++ b/client/components/settings/settingBody.styl @@ -52,10 +52,10 @@ .main-body padding: 0.1em 1em - -webkit-user-select: auto // Safari 3.1+ - -moz-user-select: auto // Firefox 2+ - -ms-user-select: auto // IE 10+ - user-select: auto // Standard syntax + -webkit-user-select: text // Safari 3.1+ + -moz-user-select: text // Firefox 2+ + -ms-user-select: text // IE 10+ + user-select: text // Standard syntax ul li diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 8b98fd7e..f7efb1e8 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -47,8 +47,11 @@ BlazeComponent.extendComponent({ }, calculateNextPeak() { - const altitude = this.find('.js-board-sidebar-content').scrollHeight; - this.callFirstWith(this, 'setNextPeak', altitude); + const sidebarElement = this.find('.js-board-sidebar-content'); + if (sidebarElement) { + const altitude = sidebarElement.scrollHeight; + this.callFirstWith(this, 'setNextPeak', altitude); + } }, reachNextPeak() { diff --git a/client/components/sidebar/sidebarArchives.js b/client/components/sidebar/sidebarArchives.js index 53fc29b9..a4846561 100644 --- a/client/components/sidebar/sidebarArchives.js +++ b/client/components/sidebar/sidebarArchives.js @@ -1,3 +1,4 @@ +archivedRequested = false; const subManager = new SubsManager(); BlazeComponent.extendComponent({ @@ -12,6 +13,7 @@ BlazeComponent.extendComponent({ const currentBoardId = Session.get('currentBoard'); if (!currentBoardId) return; const handle = subManager.subscribe('board', currentBoardId, true); + archivedRequested = true; Tracker.nonreactive(() => { Tracker.autorun(() => { this.isArchiveReady.set(handle.ready()); diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index f11528b1..55ab213a 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -56,6 +56,22 @@ template(name="filterSidebar") if Filter.customFields.isSelected _id i.fa.fa-check hr + ul.sidebar-list + li(class="{{#if Filter.archive.isSelected _id}}active{{/if}}") + a.name.js-toggle-archive-filter + span.sidebar-list-item-description + | {{_ 'filter-show-archive'}} + if Filter.archive.isSelected _id + i.fa.fa-check + hr + ul.sidebar-list + li(class="{{#if Filter.hideEmpty.isSelected _id}}active{{/if}}") + a.name.js-toggle-hideEmpty-filter + span.sidebar-list-item-description + | {{_ 'filter-hide-empty'}} + if Filter.hideEmpty.isSelected _id + i.fa.fa-check + hr span {{_ 'advanced-filter-label'}} input.js-field-advanced-filter(type="text") span {{_ 'advanced-filter-description'}} diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 88438a7a..3483d00c 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -1,3 +1,5 @@ +const subManager = new SubsManager(); + BlazeComponent.extendComponent({ events() { return [ @@ -12,6 +14,23 @@ BlazeComponent.extendComponent({ Filter.members.toggle(this.currentData()._id); Filter.resetExceptions(); }, + 'click .js-toggle-archive-filter'(evt) { + evt.preventDefault(); + Filter.archive.toggle(this.currentData()._id); + Filter.resetExceptions(); + const currentBoardId = Session.get('currentBoard'); + if (!currentBoardId) return; + subManager.subscribe( + 'board', + currentBoardId, + Filter.archive.isSelected(), + ); + }, + 'click .js-toggle-hideEmpty-filter'(evt) { + evt.preventDefault(); + Filter.hideEmpty.toggle(this.currentData()._id); + Filter.resetExceptions(); + }, 'click .js-toggle-custom-fields-filter'(evt) { evt.preventDefault(); Filter.customFields.toggle(this.currentData()._id); diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 485b2ffc..3ad43777 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -33,7 +33,8 @@ template(name="listsGroup") +addListForm else each lists - +list(this) + if visible this + +list(this) if currentCardIsInThisList _id null +cardDetails(currentCard) if currentUser.isBoardMember diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index 568c0bbe..e0857003 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -246,6 +246,24 @@ BlazeComponent.extendComponent({ currentCardIsInThisList(listId, swimlaneId) { return currentCardIsInThisList(listId, swimlaneId); }, + visible(list) { + if (list.archived) { + // Show archived list only when filter archive is on or archive is selected + if (!(Filter.archive.isSelected() || archivedRequested)) { + return false; + } + } + if (Filter.hideEmpty.isSelected()) { + const swimlaneId = this.parentComponent() + .parentComponent() + .data()._id; + const cards = list.cards(swimlaneId); + if (cards.count() === 0) { + return false; + } + } + return true; + }, onRendered() { const boardComponent = this.parentComponent(); const $listsDom = this.$('.js-lists'); diff --git a/client/lib/filter.js b/client/lib/filter.js index f19dc617..1ca3a280 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -451,10 +451,12 @@ Filter = { // before changing the schema. labelIds: new SetFilter(), members: new SetFilter(), + archive: new SetFilter(), + hideEmpty: new SetFilter(), customFields: new SetFilter('_id'), advanced: new AdvancedFilter(), - _fields: ['labelIds', 'members', 'customFields'], + _fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'], // We don't filter cards that have been added after the last filter change. To // implement this we keep the id of these cards in this `_exceptions` fields diff --git a/client/lib/popup.js b/client/lib/popup.js index 6c294d32..8095fbd2 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -67,7 +67,7 @@ window.Popup = new (class { title: self._getTitle(popupName), depth: self._stack.length, offset: self._getOffset(openerElement), - dataContext: (this.currentData && this.currentData()) || this, + dataContext: (this && this.currentData && this.currentData()) || this, }); // If there are no popup currently opened we use the Blaze API to render diff --git a/client/lib/utils.js b/client/lib/utils.js index 5681273e..81835929 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -24,6 +24,83 @@ Utils = { ); }, + MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL, + COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO, + processUploadedAttachment(card, fileObj, callback) { + const next = attachment => { + if (typeof callback === 'function') { + callback(attachment); + } + }; + if (!card) { + return next(); + } + const file = new FS.File(fileObj); + if (card.isLinkedCard()) { + file.boardId = Cards.findOne(card.linkedId).boardId; + file.cardId = card.linkedId; + } else { + file.boardId = card.boardId; + file.swimlaneId = card.swimlaneId; + file.listId = card.listId; + file.cardId = card._id; + } + file.userId = Meteor.userId(); + if (file.original) { + file.original.name = fileObj.name; + } + return next(Attachments.insert(file)); + }, + shrinkImage(options) { + // shrink image to certain size + const dataurl = options.dataurl, + callback = options.callback, + toBlob = options.toBlob; + let canvas = document.createElement('canvas'), + image = document.createElement('img'); + const maxSize = options.maxSize || 1024; + const ratio = options.ratio || 1.0; + const next = function(result) { + image = null; + canvas = null; + if (typeof callback === 'function') { + callback(result); + } + }; + image.onload = function() { + let width = this.width, + height = this.height; + let changed = false; + if (width > height) { + if (width > maxSize) { + height *= maxSize / width; + width = maxSize; + changed = true; + } + } else if (height > maxSize) { + width *= maxSize / height; + height = maxSize; + changed = true; + } + canvas.width = width; + canvas.height = height; + canvas.getContext('2d').drawImage(this, 0, 0, width, height); + if (changed === true) { + const type = 'image/jpeg'; + if (toBlob) { + canvas.toBlob(next, type, ratio); + } else { + next(canvas.toDataURL(type, ratio)); + } + } else { + next(changed); + } + }; + image.onerror = function() { + next(false); + }; + image.src = dataurl; + }, capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1); }, |