From 6db8bfc343a52a0ff7a306b150ab19950f5f6c55 Mon Sep 17 00:00:00 2001 From: hack1m Date: Mon, 5 Oct 2015 16:25:27 +0800 Subject: updated to Meteor 1.2.0.2 --- .meteor/release | 2 +- .meteor/versions | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.meteor/release b/.meteor/release index 238bbcca..5684262a 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.2.0.1 +METEOR@1.2.0.2 diff --git a/.meteor/versions b/.meteor/versions index 2bdd251a..0a423853 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -15,7 +15,7 @@ blaze@2.1.3 blaze-tools@1.0.4 boilerplate-generator@1.0.4 caching-compiler@1.0.0 -caching-html-compiler@1.0.1 +caching-html-compiler@1.0.2 callback-hook@1.0.4 cfs:access-point@0.1.49 cfs:base-package@0.0.30 @@ -35,7 +35,7 @@ cfs:tempstore@0.1.5 cfs:upload-http@0.0.20 cfs:worker@0.1.4 check@1.0.6 -coffeescript@1.0.9 +coffeescript@1.0.10 cosmos:browserify@0.5.1 dburles:collection-helpers@1.0.3 ddp@1.2.2 @@ -45,7 +45,7 @@ ddp-rate-limiter@1.0.0 ddp-server@1.2.1 deps@1.0.9 diff-sequence@1.0.1 -ecmascript@0.1.4 +ecmascript@0.1.5 ecmascript-collections@0.1.6 ejson@1.0.7 email@1.0.7 @@ -64,13 +64,13 @@ kadira:blaze-layout@2.1.0 kadira:flow-router@2.6.1 kenton:accounts-sandstorm@0.1.4 launch-screen@1.0.4 -less@2.5.0_2 +less@2.5.0_3 livedata@1.0.15 localstorage@1.0.5 logging@1.0.8 matb33:collection-hooks@0.8.0 matteodem:easy-search@1.6.3 -meteor@1.1.7 +meteor@1.1.9 meteor-base@1.0.1 meteor-platform@1.2.3 meteorhacks:aggregate@1.3.0 @@ -79,9 +79,9 @@ meteorhacks:picker@1.0.3 meteorhacks:subs-manager@1.6.2 meteorspark:util@0.2.0 minifiers@1.1.7 -minimongo@1.0.9 +minimongo@1.0.10 mobile-status-bar@1.0.6 -mongo@1.1.1 +mongo@1.1.2 mongo-id@1.0.1 mongo-livedata@1.0.9 mousetrap:mousetrap@1.4.6_1 @@ -104,12 +104,12 @@ peerlibrary:assert@0.2.5 peerlibrary:base-component@0.10.0 peerlibrary:blaze-components@0.13.0 perak:markdown@1.0.5 -promise@0.4.8 +promise@0.5.0 raix:eventemitter@0.1.3 raix:handlebar-helpers@0.2.4 random@1.0.4 rate-limit@1.0.0 -reactive-dict@1.1.1 +reactive-dict@1.1.2 reactive-var@1.0.6 reload@1.1.4 retry@1.0.4 @@ -123,12 +123,12 @@ softwarerero:accounts-t9n@1.1.4 spacebars@1.0.7 spacebars-compiler@1.0.7 srp@1.0.4 -standard-minifiers@1.0.0 +standard-minifiers@1.0.1 tap:i18n@1.6.1 templates:tabs@2.2.0 -templating@1.1.3 +templating@1.1.4 templating-tools@1.0.0 -tracker@1.0.8 +tracker@1.0.9 ui@1.0.8 underscore@1.0.4 url@1.0.5 -- cgit v1.2.3-1-g7c22 From a78debc461944f55de246db15a1dd29353dec4ae Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 8 Oct 2015 23:25:04 +0200 Subject: Support app deployment under a path prefix Fixes #133 --- .meteor/versions | 2 +- client/components/activities/activities.js | 4 ++-- client/components/cards/attachments.jade | 2 +- client/components/cards/minicard.jade | 2 +- client/components/main/header.jade | 4 ++-- client/components/main/layouts.jade | 6 +++++- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.meteor/versions b/.meteor/versions index 0a423853..672a85e8 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -61,7 +61,7 @@ id-map@1.0.4 idmontie:migrations@1.0.0 jquery@1.11.4 kadira:blaze-layout@2.1.0 -kadira:flow-router@2.6.1 +kadira:flow-router@2.7.0 kenton:accounts-sandstorm@0.1.4 launch-screen@1.0.4 less@2.5.0_3 diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index 5c5d8370..9a1435ef 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -55,7 +55,7 @@ BlazeComponent.extendComponent({ cardLink() { const card = this.currentData().card(); return card && Blaze.toHTML(HTML.A({ - href: card.absoluteUrl(), + href: FlowRouter.path(card.absoluteUrl()), 'class': 'action-card', }, card.title)); }, @@ -69,7 +69,7 @@ BlazeComponent.extendComponent({ attachmentLink() { const attachment = this.currentData().attachment(); return attachment && Blaze.toHTML(HTML.A({ - href: attachment.url({ download: true }), + href: FlowRouter.path(attachment.url({ download: true })), target: '_blank', }, attachment.name())); }, diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index 59eaf077..e519650b 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -15,7 +15,7 @@ template(name="attachmentsGalery") .attachment-thumbnail if isUploaded if isImage - img.attachment-thumbnail-img(src=url) + img.attachment-thumbnail-img(src="{{pathfor url}}") else span.attachment-thumbnail-ext= extension else diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 660b0fa5..573b3da1 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -2,7 +2,7 @@ template(name="minicard") .minicard if cover .minicard-cover - img(src=cover.url) + img(src="{{pathFor cover.url}}") if labels .minicard-labels each labels diff --git a/client/components/main/header.jade b/client/components/main/header.jade index 4715bfc8..86dfd6a7 100644 --- a/client/components/main/header.jade +++ b/client/components/main/header.jade @@ -43,10 +43,10 @@ template(name="header") the list of all boards. if isSandstorm .wekan-logo - img(src="/wekan-logo-header.png" alt="Wekan") + img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan") else a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}") - img(src="/wekan-logo-header.png" alt="Wekan") + img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan") template(name="headerTitle") h1 {{_ 'my-boards'}} diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade index f5a8db59..166f143a 100644 --- a/client/components/main/layouts.jade +++ b/client/components/main/layouts.jade @@ -2,12 +2,16 @@ head title Wekan meta(name="viewport" content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0") + //- XXX We should use pathFor in the following `href` to support the case + where the application is deployed with a path prefix, but it seems to be + difficult to do that cleanly with Blaze -- at least without adding extra + packages. link(rel="shortcut icon" href="/wekan-favicon.png") template(name="userFormsLayout") section.auth-layout h1.at-form-landing-logo - img(src="/wekan-logo.png" alt="Wekan") + img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan") +Template.dynamic(template=content) template(name="defaultLayout") -- cgit v1.2.3-1-g7c22 From 21dcd162f507bfdc98e38d8ede111a0f324c6984 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 12 Oct 2015 17:41:09 +0200 Subject: Add editorconfig support --- .editorconfig | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4ba559c2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig is awesome: http://EditorConfig.org +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 -- cgit v1.2.3-1-g7c22 From bf46a214050cf468d2b08436a8d070ed5c24c852 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 13 Oct 2015 14:10:51 +0200 Subject: Fix a typo in a function call introduced in a78debc --- client/components/cards/attachments.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade index e519650b..168fc2c8 100644 --- a/client/components/cards/attachments.jade +++ b/client/components/cards/attachments.jade @@ -15,7 +15,7 @@ template(name="attachmentsGalery") .attachment-thumbnail if isUploaded if isImage - img.attachment-thumbnail-img(src="{{pathfor url}}") + img.attachment-thumbnail-img(src="{{pathFor url}}") else span.attachment-thumbnail-ext= extension else -- cgit v1.2.3-1-g7c22 From e504ac28940f614652f8e414aa0a67f60985bb89 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 13 Oct 2015 14:17:53 +0200 Subject: Enforce "public" visibility for Sandstorm boards Fixes #346 --- sandstorm.js | 9 +++++++++ server/migrations.js | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/sandstorm.js b/sandstorm.js index 97d42bdf..b0039d63 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -101,6 +101,15 @@ if (isSandstorm && Meteor.isServer) { updateUserPermissions(doc._id, doc.services.sandstorm.permissions); }); + + // LibreBoard v0.8 didn’t implement the Sandstorm sharing model and instead + // kept the visibility setting (“public” or “private”) in the UI as does the + // main Meteor application. We need to enforce “public” visibility has the + // sharing is now handled by Sandstorm. + // See https://github.com/wekan/wekan/issues/346 + Migrations.add('enforce-public-visibility-for-sandstorm', () => { + Boards.update('sandstorm', { $set: { permission: 'public' }}); + }); } if (isSandstorm && Meteor.isClient) { diff --git a/server/migrations.js b/server/migrations.js index 05f5ff7d..509d8510 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -4,6 +4,12 @@ // // Migrations.add(name, migrationCallback, optionalOrder); +// Note that we have extra migrations defined in `sandstorm.js` that are +// exclusive to Sandstorm and shouldn’t be executed in the general case. +// XXX I guess if we had ES6 modules we could +// `import { isSandstorm } from sandstorm.js` and define the migration here as +// well, but for now I want to avoid definied too many globals. + // In the context of migration functions we don't want to validate database // mutation queries against the current (ie, latest) collection schema. Doing // that would work at the time we write the migration but would break in the -- cgit v1.2.3-1-g7c22 From 8bbc69616f91e9d09302d8a00f845b74e37d535f Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 13 Oct 2015 18:36:58 +0200 Subject: Abstract the jquery-textcomplete integration with EscapeActions We now can re-use this integration in multiple places, this will be useful for #342 for instance. --- .meteor/versions | 2 +- client/components/main/editor.js | 26 +------------------------- client/lib/textComplete.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 client/lib/textComplete.js diff --git a/.meteor/versions b/.meteor/versions index 672a85e8..f9e968c1 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -89,7 +89,7 @@ mquandalle:autofocus@1.0.0 mquandalle:collection-mutations@0.1.0 mquandalle:jade@0.4.3_1 mquandalle:jade-compiler@0.4.3 -mquandalle:jquery-textcomplete@0.3.9_1 +mquandalle:jquery-textcomplete@0.8.0_1 mquandalle:jquery-ui-drag-drop-sort@0.1.0 mquandalle:moment@1.0.0 mquandalle:mousetrap-bindglobal@0.0.1 diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 1d88fe74..67b65bec 100644 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -1,11 +1,9 @@ -let dropdownMenuIsOpened = false; - Template.editor.onRendered(() => { const $textarea = this.$('textarea'); autosize($textarea); - $textarea.textcomplete([ + $textarea.escapeableTextComplete([ // Emojies { match: /\B:([\-+\w]*)$/, @@ -44,30 +42,8 @@ Template.editor.onRendered(() => { index: 1, }, ]); - - // Since commit d474017 jquery-textComplete automatically closes a potential - // opened dropdown menu when the user press Escape. This behavior conflicts - // with our EscapeActions system, but it's too complicated and hacky to - // monkey-pach textComplete to disable it -- I tried. Instead we listen to - // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown - // is opened (and rely on textComplete to execute the actual action). - $textarea.on({ - 'textComplete:show'() { - dropdownMenuIsOpened = true; - }, - 'textComplete:hide'() { - Tracker.afterFlush(() => { - dropdownMenuIsOpened = false; - }); - }, - }); }); -EscapeActions.register('textcomplete', - () => {}, - () => dropdownMenuIsOpened -); - // XXX I believe we should compute a HTML rendered field on the server that // would handle markdown, emojies and user mentions. We can simply have two // fields, one source, and one compiled version (in HTML) and send only the diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js new file mode 100644 index 00000000..e50d7cbc --- /dev/null +++ b/client/lib/textComplete.js @@ -0,0 +1,30 @@ +// We “inherit” the jquery-textcomplete plugin to integrate with our +// EscapeActions system. You should always use `escapeableTextComplete` instead +// of the vanilla `textcomplete`. +let dropdownMenuIsOpened = false; + +$.fn.escapeableTextComplete = function(...args) { + this.textcomplete(...args); + + // Since commit d474017 jquery-textComplete automatically closes a potential + // opened dropdown menu when the user press Escape. This behavior conflicts + // with our EscapeActions system, but it's too complicated and hacky to + // monkey-pach textComplete to disable it -- I tried. Instead we listen to + // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown + // is opened (and rely on textComplete to execute the actual action). + this.on({ + 'textComplete:show'() { + dropdownMenuIsOpened = true; + }, + 'textComplete:hide'() { + Tracker.afterFlush(() => { + dropdownMenuIsOpened = false; + }); + }, + }); +}; + +EscapeActions.register('textcomplete', + () => {}, + () => dropdownMenuIsOpened +); -- cgit v1.2.3-1-g7c22 From 1bf9e65571e465b8a8c8a0a9a78edbb04f5b59c2 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 13 Oct 2015 18:44:46 +0200 Subject: Remove the Meteor._wrapAsync depreciation warning This warning was raised by the `cfs:storage-adapter` package and fixed in https://github.com/CollectionFS/Meteor-CollectionFS/pull/663. --- .meteor/versions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.meteor/versions b/.meteor/versions index f9e968c1..5ced1e20 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -30,7 +30,7 @@ cfs:power-queue@0.9.11 cfs:reactive-list@0.0.9 cfs:reactive-property@0.0.4 cfs:standard-packages@0.5.9 -cfs:storage-adapter@0.2.2 +cfs:storage-adapter@0.2.3 cfs:tempstore@0.1.5 cfs:upload-http@0.0.20 cfs:worker@0.1.4 -- cgit v1.2.3-1-g7c22 From 3f7df340e1dea4a48bd17c189ccf61e53c47330a Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 13 Oct 2015 19:46:37 +0200 Subject: Set some DOM transformation to fix some of the accessibility issues See #337 for the complete rationale by @ndarilek -- thank you :) Closes #338 --- client/lib/accessibility.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 client/lib/accessibility.js diff --git a/client/lib/accessibility.js b/client/lib/accessibility.js new file mode 100644 index 00000000..64195b81 --- /dev/null +++ b/client/lib/accessibility.js @@ -0,0 +1,41 @@ +// In this file we define a set of DOM transformations that are specifically +// intended for blind screen readers. +// +// See https://github.com/wekan/wekan/issues/337 for the general accessibility +// considerations. + +// Without an href, links are non-keyboard-focusable and are not presented on +// blind screen readers. We default to the empty anchor `#` href. +function enforceHref(attributes) { + if (! _.has(attributes, 'href')) { + attributes.href = '#'; + } + return attributes; +} + +// `title` is inconsistently used on the web, and is thus inconsistently +// presented by screen readers. `aria-label`, on the other hand, is specific to +// accessibility and is presented in ways that title shouldn't be. +function copyTitleInAriaLabel(attributes) { + if (! _.has(attributes, 'aria-label') && _.has(attributes, 'title')) { + attributes['aria-label'] = attributes.title; + } + return attributes; +} + +// XXX Our implementation relies on overwriting Blaze virtual DOM functions, +// which is a little bit hacky -- but still reasonable with our ES6 usage. If we +// end up switching to React we will probably create lower level small +// components to handle that without overwriting any build-in function. +const { + A: superA, + I: superI, +} = HTML; + +HTML.A = (attributes, ...others) => { + return superA(copyTitleInAriaLabel(enforceHref(attributes)), ...others); +} + +HTML.I = (attributes, ...others) => { + return superI(copyTitleInAriaLabel(attributes), ...others); +} -- cgit v1.2.3-1-g7c22 From 758be3356cc3fb05de0a29787059b6b34aac08a2 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 13 Oct 2015 19:56:04 +0200 Subject: Use a better API to retrieve the new card position Fixes #347 --- client/components/lists/listBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 2e00cb4f..0b69b50a 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -27,7 +27,7 @@ BlazeComponent.extendComponent({ const lastCardDom = this.find('.js-minicard:last'); const textarea = $(evt.currentTarget).find('textarea'); const title = textarea.val(); - const position = Blaze.getData(evt.currentTarget).position; + const position = this.currentData().position; let sortIndex; if (position === 'top') { sortIndex = Utils.calculateIndex(null, firstCardDom).base; -- cgit v1.2.3-1-g7c22 From bc79b83140ec2e120f4050ac78f6f8baa0483137 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Tue, 13 Oct 2015 20:29:25 +0200 Subject: Add card import UI --- client/components/lists/listHeader.jade | 9 +++++++++ client/components/lists/listHeader.js | 2 ++ i18n/en.i18n.json | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 7d01f1ba..597e2bbd 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -25,11 +25,20 @@ template(name="listActionPopup") li: a.js-archive-cards {{_ 'list-archive-cards'}} hr ul.pop-over-list + li: a.js-import-card {{_ 'import-card'}} li: a.js-close-list {{_ 'archive-list'}} template(name="listMoveCardsPopup") +boardLists +template(name="listImportCardPopup") + form + label + | {{_ 'card-json'}} + //+editor(class="js-card-json" autofocus=true) + textarea.js-card-json(placeholder="{{_ 'card-json-placeholder'}}" autofocus) + input.primary.wide(type="submit" value="{{_ 'import'}}") + template(name="boardLists") ul.pop-over-list each currentBoard.lists diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index a893e873..4f5fc3a0 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -33,6 +33,7 @@ Template.listActionPopup.events({ MultiSelection.add(cardIds); Popup.close(); }, + 'click .js-import-card': Popup.open('listImportCard'), 'click .js-move-cards': Popup.open('listMoveCards'), 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() { this.allCards().forEach((card) => { @@ -40,6 +41,7 @@ Template.listActionPopup.events({ }); Popup.close(); }), + 'click .js-close-list'(evt) { evt.preventDefault(); this.archive(); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index a8c2c5d9..436db890 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -65,6 +65,8 @@ "card-edit-attachments": "Edit attachments", "card-edit-labels": "Edit labels", "card-edit-members": "Edit members", + "card-json": "Card data", + "card-json-placeholder": "Paste your valid JSON data here", "card-labels-title": "Change the labels for the card.", "card-members-title": "Add or remove members of the board from the card.", "cardAttachmentsPopup-title": "Attach From", @@ -118,6 +120,8 @@ "fullname": "Full Name", "header-logo-title": "Go back to your boards page.", "home": "Home", + "import": "Import", + "import-card": "Import as new card", "info": "Infos", "initials": "Initials", "joined": "joined", @@ -136,6 +140,7 @@ "list-select-cards": "Select all cards in this list", "listActionPopup-title": "List Actions", "listArchiveCardsPopup-title": "Archive All Cards in this List?", + "listImportCardPopup-title": "Import as new card", "listMoveCardsPopup-title": "Move All Cards in List", "lists": "Lists", "log-out": "Log Out", -- cgit v1.2.3-1-g7c22 From 1b9df0049259438d001abd9c81f15ce8f7d25b14 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 00:32:44 +0200 Subject: Import single card: title and proper sort index --- client/components/lists/listHeader.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 4f5fc3a0..28e7ab89 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -49,6 +49,32 @@ Template.listActionPopup.events({ }, }); +Template.listImportCardPopup.events({ + submit(evt, template) { + // 1. get the json data out of the form and parse it + evt.preventDefault(); + const jsonData = $(evt.currentTarget).find('textarea').val(); + const data = JSON.parse(jsonData); + // 2. map all fields for the card to create + const firstCardDom = $(`#js-list-${this._id} .js-minicard:first`).get(0); + sortIndex = Utils.calculateIndex(null, firstCardDom).base; + const cardToCreate = { + title: data.name, + listId: this._id, + boardId: this.boardId, + userId: Meteor.userId(), + sort: sortIndex, + } + // 3. finally, insert new card into list + const _id = Cards.insert(cardToCreate); + // In case the filter is active we need to add the newly inserted card in + // the list of exceptions -- cards that are not filtered. Otherwise the + // card will disappear instantly. + // See https://github.com/wekan/wekan/issues/80 + Filter.addException(_id); + } +}); + Template.listMoveCardsPopup.events({ 'click .js-select-list'() { const fromList = Template.parentData(2).data; -- cgit v1.2.3-1-g7c22 From 432c1ebb5d9165b9c63b10ef489c6f9e84470e72 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 10:10:07 +0200 Subject: Import single card: now with description and comments --- client/components/lists/listHeader.js | 17 ++++++++++++++++- i18n/en.i18n.json | 6 +++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 28e7ab89..745d1255 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -60,13 +60,28 @@ Template.listImportCardPopup.events({ sortIndex = Utils.calculateIndex(null, firstCardDom).base; const cardToCreate = { title: data.name, + description: data.desc, listId: this._id, boardId: this.boardId, userId: Meteor.userId(), sort: sortIndex, } - // 3. finally, insert new card into list + // 3. insert new card into list const _id = Cards.insert(cardToCreate); + // 4. parse actions and add comments/activities - if any + data.actions.forEach((current, i, actions)=>{ + if(current.type == 'commentCard') { + const commentToCreate = { + boardId: this.boardId, + cardId: _id, + userId: Meteor.userId(), + text: current.data.text + } + CardComments.insert(commentToCreate); + } + Popup.close(); + }); + // In case the filter is active we need to add the newly inserted card in // the list of exceptions -- cards that are not filtered. Otherwise the // card will disappear instantly. diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 436db890..331d8415 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -65,7 +65,7 @@ "card-edit-attachments": "Edit attachments", "card-edit-labels": "Edit labels", "card-edit-members": "Edit members", - "card-json": "Card data", + "card-json": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text", "card-json-placeholder": "Paste your valid JSON data here", "card-labels-title": "Change the labels for the card.", "card-members-title": "Add or remove members of the board from the card.", @@ -121,7 +121,7 @@ "header-logo-title": "Go back to your boards page.", "home": "Home", "import": "Import", - "import-card": "Import as new card", + "import-card": "Import a Trello card", "info": "Infos", "initials": "Initials", "joined": "joined", @@ -140,7 +140,7 @@ "list-select-cards": "Select all cards in this list", "listActionPopup-title": "List Actions", "listArchiveCardsPopup-title": "Archive All Cards in this List?", - "listImportCardPopup-title": "Import as new card", + "listImportCardPopup-title": "Import a Trello card", "listMoveCardsPopup-title": "Move All Cards in List", "lists": "Lists", "log-out": "Log Out", -- cgit v1.2.3-1-g7c22 From 68518f549719d4c150acc920197587425f2de341 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 10:54:42 +0200 Subject: Import single card: map labels --- client/components/lists/listHeader.js | 43 +++++++++++++++++++++++++++-------- models/boards.js | 10 ++++++-- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 745d1255..b716f70c 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -50,14 +50,14 @@ Template.listActionPopup.events({ }); Template.listImportCardPopup.events({ - submit(evt, template) { + submit(evt) { // 1. get the json data out of the form and parse it evt.preventDefault(); const jsonData = $(evt.currentTarget).find('textarea').val(); const data = JSON.parse(jsonData); // 2. map all fields for the card to create const firstCardDom = $(`#js-list-${this._id} .js-minicard:first`).get(0); - sortIndex = Utils.calculateIndex(null, firstCardDom).base; + const sortIndex = Utils.calculateIndex(null, firstCardDom).base; const cardToCreate = { title: data.name, description: data.desc, @@ -65,20 +65,43 @@ Template.listImportCardPopup.events({ boardId: this.boardId, userId: Meteor.userId(), sort: sortIndex, - } - // 3. insert new card into list + }; + // 3. map labels + data.labels.forEach((current) => { + const color = current.color; + const name = current.name; + const existingLabel = this.board().getLabel(name, color); + let labelId = undefined; + if (existingLabel) { + labelId = existingLabel._id; + } else { + let labelCreated = this.board().addLabel(name, color); + // XXX currently mutations return no value so we have to fetch the label we just created + // waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... + labelCreated = this.board().getLabel(name, color); + labelId = labelCreated._id; + } + if(labelId) { + if (!cardToCreate.labelIds) { + cardToCreate.labelIds = []; + } + cardToCreate.labelIds.push(labelId); + } + }); + // 4. insert new card into list const _id = Cards.insert(cardToCreate); - // 4. parse actions and add comments/activities - if any - data.actions.forEach((current, i, actions)=>{ - if(current.type == 'commentCard') { + // 5. parse actions and add comments + data.actions.forEach((current) => { + if(current.type === 'commentCard') { const commentToCreate = { boardId: this.boardId, cardId: _id, userId: Meteor.userId(), - text: current.data.text - } + text: current.data.text, + }; CardComments.insert(commentToCreate); } + // XXX add other type of activities? Popup.close(); }); @@ -87,7 +110,7 @@ Template.listImportCardPopup.events({ // card will disappear instantly. // See https://github.com/wekan/wekan/issues/80 Filter.addException(_id); - } + }, }); Template.listMoveCardsPopup.events({ diff --git a/models/boards.js b/models/boards.js index 4baec280..8d2b63e4 100644 --- a/models/boards.js +++ b/models/boards.js @@ -92,6 +92,12 @@ Boards.helpers({ return _.where(this.members, {isActive: true}); }, + getLabel(name, color) { + return this.labels.find((current) => { + return ((current.name === name) && (current.color === color)); + }); + }, + labelIndex(labelId) { return _.indexOf(_.pluck(this.labels, '_id'), labelId); }, @@ -293,8 +299,8 @@ if (Meteor.isServer) { }); }); - // If the user remove one label from a board, we cant to remove reference of - // this label in any card of this board. + // If the user removes a label from a board, we have to remove references to + // this label in all cards of the board. Boards.after.update((userId, doc, fieldNames, modifier) => { if (!_.contains(fieldNames, 'labels') || !modifier.$pull || -- cgit v1.2.3-1-g7c22 From 7e64c22c1a6352e805b0ba9402062823f9b02e11 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 12:15:52 +0200 Subject: Import single card: archived card --- client/components/lists/listHeader.js | 1 + models/cards.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index b716f70c..c4cace08 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -65,6 +65,7 @@ Template.listImportCardPopup.events({ boardId: this.boardId, userId: Meteor.userId(), sort: sortIndex, + archived: data.closed, }; // 3. map labels data.labels.forEach((current) => { diff --git a/models/cards.js b/models/cards.js index 95943ae2..2e16583d 100644 --- a/models/cards.js +++ b/models/cards.js @@ -194,8 +194,9 @@ Cards.mutations({ Cards.before.insert((userId, doc) => { doc.createdAt = new Date(); doc.dateLastActivity = new Date(); - doc.archived = false; - + if(!doc.hasOwnProperty('archived')){ + doc.archived = false; + } if (!doc.userId) { doc.userId = userId; } -- cgit v1.2.3-1-g7c22 From d8892d640860b77795b1e81d2ba121cf65d35373 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 17:57:58 +0200 Subject: Import single card: refactor to meteor method --- client/components/lists/listHeader.jade | 1 - client/components/lists/listHeader.js | 68 +++++------------------ models/import.js | 98 +++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 55 deletions(-) create mode 100644 models/import.js diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 597e2bbd..922ac27b 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -35,7 +35,6 @@ template(name="listImportCardPopup") form label | {{_ 'card-json'}} - //+editor(class="js-card-json" autofocus=true) textarea.js-card-json(placeholder="{{_ 'card-json-placeholder'}}" autofocus) input.primary.wide(type="submit" value="{{_ 'import'}}") diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index c4cace08..9aa26194 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -54,63 +54,23 @@ Template.listImportCardPopup.events({ // 1. get the json data out of the form and parse it evt.preventDefault(); const jsonData = $(evt.currentTarget).find('textarea').val(); - const data = JSON.parse(jsonData); - // 2. map all fields for the card to create const firstCardDom = $(`#js-list-${this._id} .js-minicard:first`).get(0); const sortIndex = Utils.calculateIndex(null, firstCardDom).base; - const cardToCreate = { - title: data.name, - description: data.desc, - listId: this._id, - boardId: this.boardId, - userId: Meteor.userId(), - sort: sortIndex, - archived: data.closed, - }; - // 3. map labels - data.labels.forEach((current) => { - const color = current.color; - const name = current.name; - const existingLabel = this.board().getLabel(name, color); - let labelId = undefined; - if (existingLabel) { - labelId = existingLabel._id; - } else { - let labelCreated = this.board().addLabel(name, color); - // XXX currently mutations return no value so we have to fetch the label we just created - // waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... - labelCreated = this.board().getLabel(name, color); - labelId = labelCreated._id; - } - if(labelId) { - if (!cardToCreate.labelIds) { - cardToCreate.labelIds = []; - } - cardToCreate.labelIds.push(labelId); - } - }); - // 4. insert new card into list - const _id = Cards.insert(cardToCreate); - // 5. parse actions and add comments - data.actions.forEach((current) => { - if(current.type === 'commentCard') { - const commentToCreate = { - boardId: this.boardId, - cardId: _id, - userId: Meteor.userId(), - text: current.data.text, - }; - CardComments.insert(commentToCreate); - } - // XXX add other type of activities? + try { + const trelloCard = JSON.parse(jsonData); + const cardId = Meteor.call('importTrelloCard', trelloCard, this._id, sortIndex); + // In case the filter is active we need to add the newly inserted card in + // the list of exceptions -- cards that are not filtered. Otherwise the + // card will disappear instantly. + // See https://github.com/wekan/wekan/issues/80 + Filter.addException(cardId); Popup.close(); - }); - - // In case the filter is active we need to add the newly inserted card in - // the list of exceptions -- cards that are not filtered. Otherwise the - // card will disappear instantly. - // See https://github.com/wekan/wekan/issues/80 - Filter.addException(_id); + } catch(e) { + // XXX handle error + // this.error.set('avatar-too-big'); + console.log('Invalid JSON'); + return; + } }, }); diff --git a/models/import.js b/models/import.js new file mode 100644 index 00000000..83559078 --- /dev/null +++ b/models/import.js @@ -0,0 +1,98 @@ +Meteor.methods({ + /** + * + */ + importTrelloCard(trelloCard, listId, sortIndex) { + DateString = Match.Where(function (dateAsString) { + check(dateAsString, String); + //const date = new Date(dateAsString); + //return (date.toString() !== 'Invalid Date') && !isNan(date); + return moment(dateAsString, moment.ISO_8601).isValid(); + }); + + check(trelloCard, Match.ObjectIncluding({ + name: String, + desc: String, + closed: Boolean, + dateLastActivity: DateString, + labels: [Match.ObjectIncluding({ + name: String, + color: String, + })], + actions: [Match.ObjectIncluding({ + type: String, + date: DateString, + data: Object, + })], + members: [Object], + })); + check(listId, String); + check(sortIndex, Number); + + const list = Lists.findOne(listId); + if(!list) { + throw 'exception-list-doesNotExist'; + } + + // XXX check we are allowed to run method + + // 1. map all fields for the card to create + const dateOfImport = new Date(); + // XXX parse trelloCard.actions to determine creation date + const cardToCreate = { + title: trelloCard.name, + description: trelloCard.desc, + listId: list._id, + boardId: list.boardId, + userId: Meteor.userId(), + sort: sortIndex, + archived: trelloCard.closed, + // XXX dateOfImport + createdAt: dateOfImport, + dateLastActivity: dateOfImport, + }; + // 2. map labels + trelloCard.labels.forEach((currentLabel) => { + const color = currentLabel.color; + const name = currentLabel.name; + const existingLabel = list.board().getLabel(name, color); + let labelId = undefined; + if (existingLabel) { + labelId = existingLabel._id; + } else { + let labelCreated = list.board().addLabel(name, color); + // XXX currently mutations return no value so we have to fetch the label we just created + // waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... + labelCreated = list.board().getLabel(name, color); + labelId = labelCreated._id; + } + if(labelId) { + if (!cardToCreate.labelIds) { + cardToCreate.labelIds = []; + } + cardToCreate.labelIds.push(labelId); + } + }); + // 3. insert new card into list + // XXX replace with direct MongoDB inserts + const _id = Cards.direct.insert(cardToCreate); + // XXX then add import activity + // 4. parse actions and add comments + trelloCard.actions.forEach((currentAction) => { + if(currentAction.type === 'commentCard') { + const commentToCreate = { + boardId: list.boardId, + cardId: _id, + userId: Meteor.userId(), + text: currentAction.data.text, + createdAt: currentAction.date, + }; + // console.log(commentToCreate); + CardComments.direct.insert(commentToCreate); + } + // XXX add other type of activities? + // XXX look for createCard to set create date > no do it BEFORE saving + }); + return _id; + }, +}); -- cgit v1.2.3-1-g7c22 From 81bd55113789e53e65080604bb29a3d61df83684 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 18:23:46 +0200 Subject: Import single card: now uses historical dates --- models/import.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/models/import.js b/models/import.js index 83559078..28b02805 100644 --- a/models/import.js +++ b/models/import.js @@ -38,7 +38,6 @@ Meteor.methods({ // 1. map all fields for the card to create const dateOfImport = new Date(); - // XXX parse trelloCard.actions to determine creation date const cardToCreate = { title: trelloCard.name, description: trelloCard.desc, @@ -47,10 +46,15 @@ Meteor.methods({ userId: Meteor.userId(), sort: sortIndex, archived: trelloCard.closed, - // XXX dateOfImport + // this is a default date, we'll fetch the actual one from the actions array createdAt: dateOfImport, dateLastActivity: dateOfImport, }; + // find actual creation date + const creationAction = trelloCard.actions.find((action) => {return action.type === 'createCard';}); + if(creationAction) { + cardToCreate.createdAt = creationAction.date; + } // 2. map labels trelloCard.labels.forEach((currentLabel) => { const color = currentLabel.color; @@ -74,25 +78,30 @@ Meteor.methods({ } }); // 3. insert new card into list - // XXX replace with direct MongoDB inserts - const _id = Cards.direct.insert(cardToCreate); + const cardId = Cards.direct.insert(cardToCreate); // XXX then add import activity // 4. parse actions and add comments trelloCard.actions.forEach((currentAction) => { if(currentAction.type === 'commentCard') { const commentToCreate = { boardId: list.boardId, - cardId: _id, - userId: Meteor.userId(), - text: currentAction.data.text, + cardId: cardId, createdAt: currentAction.date, + text: currentAction.data.text, + userId: Meteor.userId(), }; - // console.log(commentToCreate); - CardComments.direct.insert(commentToCreate); + const commentId = CardComments.direct.insert(commentToCreate); + Activities.direct.insert({ + activityType: 'addComment', + boardId: commentToCreate.boardId, + cardId: commentToCreate.cardId, + commentId: commentId, + createdAt: commentToCreate.createdAt, + userId: commentToCreate.userId, + }); } // XXX add other type of activities? - // XXX look for createCard to set create date > no do it BEFORE saving }); - return _id; + return cardId; }, }); -- cgit v1.2.3-1-g7c22 From 4b99ce2aa2ed94d54f677f94e410d2888aa3491f Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 19:28:30 +0200 Subject: Import single card: check user authorized --- models/import.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/models/import.js b/models/import.js index 28b02805..a4172c0d 100644 --- a/models/import.js +++ b/models/import.js @@ -3,13 +3,11 @@ Meteor.methods({ * */ importTrelloCard(trelloCard, listId, sortIndex) { + // 1. check parameters are ok from a syntax point of view DateString = Match.Where(function (dateAsString) { check(dateAsString, String); - //const date = new Date(dateAsString); - //return (date.toString() !== 'Invalid Date') && !isNan(date); return moment(dateAsString, moment.ISO_8601).isValid(); }); - check(trelloCard, Match.ObjectIncluding({ name: String, desc: String, @@ -29,14 +27,18 @@ Meteor.methods({ check(listId, String); check(sortIndex, Number); + // 2. check parameters are ok from a business point of view (exist & authorized) const list = Lists.findOne(listId); if(!list) { throw 'exception-list-doesNotExist'; } + if(Meteor.isServer) { + if (!allowIsBoardMember(Meteor.userId(), Boards.findOne(list.boardId))) { + throw 'exception-board-notAMember'; + } + } - // XXX check we are allowed to run method - - // 1. map all fields for the card to create + // 3. map all fields for the card to create const dateOfImport = new Date(); const cardToCreate = { title: trelloCard.name, @@ -50,12 +52,14 @@ Meteor.methods({ createdAt: dateOfImport, dateLastActivity: dateOfImport, }; - // find actual creation date + + // 4. find actual creation date const creationAction = trelloCard.actions.find((action) => {return action.type === 'createCard';}); if(creationAction) { cardToCreate.createdAt = creationAction.date; } - // 2. map labels + + // 5. map labels - create missing ones trelloCard.labels.forEach((currentLabel) => { const color = currentLabel.color; const name = currentLabel.name; @@ -77,10 +81,12 @@ Meteor.methods({ cardToCreate.labelIds.push(labelId); } }); - // 3. insert new card into list + + // 6. insert new card into list const cardId = Cards.direct.insert(cardToCreate); // XXX then add import activity - // 4. parse actions and add comments + + // 7. parse actions and add comments trelloCard.actions.forEach((currentAction) => { if(currentAction.type === 'commentCard') { const commentToCreate = { -- cgit v1.2.3-1-g7c22 From 7d57ce896baeac74498e8b76b5812ceb6df8b950 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 19:54:40 +0200 Subject: Import single card: create an 'importCard' activity entry --- client/components/activities/activities.jade | 5 +++++ client/components/activities/activities.js | 7 +++++++ i18n/en.i18n.json | 1 + models/import.js | 30 ++++++++++++++++++++-------- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index 85b1276e..c611ad75 100644 --- a/client/components/activities/activities.jade +++ b/client/components/activities/activities.jade @@ -26,6 +26,9 @@ template(name="boardActivities") if($eq activityType 'createCard') | {{{_ 'activity-added' cardLink boardLabel}}}. + if($eq activityType 'importCard') + | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}. + if($eq activityType 'archivedCard') | {{{_ 'activity-archived' cardLink}}}. @@ -72,6 +75,8 @@ template(name="cardActivities") +memberName(user=user) if($eq activityType 'createCard') | {{_ 'activity-added' cardLabel list.title}}. + if($eq activityType 'importCard') + | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}. if($eq activityType 'joinMember') if($eq currentUser._id member._id) | {{_ 'activity-joined' cardLabel}}. diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index 9a1435ef..b80493f7 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -60,6 +60,13 @@ BlazeComponent.extendComponent({ }, card.title)); }, + sourceLink() { + const source = this.currentData().source; + return source && Blaze.toHTML(HTML.A({ + href: source.url, + }, source.system)); + }, + memberLink() { return Blaze.toHTMLWithData(Template.memberName, { user: this.currentData().member(), diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 331d8415..40e21e53 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -7,6 +7,7 @@ "activity-attached": "attached %s to %s", "activity-created": "created %s", "activity-excluded": "excluded %s from %s", + "activity-imported": "imported %s into %s from %s", "activity-joined": "joined %s", "activity-moved": "moved %s from %s to %s", "activity-on": "on %s", diff --git a/models/import.js b/models/import.js index a4172c0d..a76bbc5f 100644 --- a/models/import.js +++ b/models/import.js @@ -41,16 +41,17 @@ Meteor.methods({ // 3. map all fields for the card to create const dateOfImport = new Date(); const cardToCreate = { - title: trelloCard.name, - description: trelloCard.desc, - listId: list._id, - boardId: list.boardId, - userId: Meteor.userId(), - sort: sortIndex, archived: trelloCard.closed, + boardId: list.boardId, // this is a default date, we'll fetch the actual one from the actions array createdAt: dateOfImport, dateLastActivity: dateOfImport, + description: trelloCard.desc, + listId: list._id, + sort: sortIndex, + title: trelloCard.name, + // XXX use the original user? + userId: Meteor.userId(), }; // 4. find actual creation date @@ -84,7 +85,20 @@ Meteor.methods({ // 6. insert new card into list const cardId = Cards.direct.insert(cardToCreate); - // XXX then add import activity + Activities.direct.insert({ + activityType: 'importCard', + boardId: cardToCreate.boardId, + cardId: cardId, + createdAt: dateOfImport, + listId: cardToCreate.listId, + source: { + id: trelloCard.id, + system: 'Trello', + url: trelloCard.url, + }, + // we attribute the import to current user, not the one from the original card + userId: Meteor.userId(), + }); // 7. parse actions and add comments trelloCard.actions.forEach((currentAction) => { @@ -94,6 +108,7 @@ Meteor.methods({ cardId: cardId, createdAt: currentAction.date, text: currentAction.data.text, + // XXX use the original comment user instead userId: Meteor.userId(), }; const commentId = CardComments.direct.insert(commentToCreate); @@ -106,7 +121,6 @@ Meteor.methods({ userId: commentToCreate.userId, }); } - // XXX add other type of activities? }); return cardId; }, -- cgit v1.2.3-1-g7c22 From b670a1ab36fcf514751e4b356487a96088dbbd24 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Wed, 14 Oct 2015 22:09:32 +0200 Subject: Import single card: proper error handling --- client/components/lists/listHeader.jade | 2 ++ client/components/lists/listHeader.js | 62 +++++++++++++++++++++------------ i18n/en.i18n.json | 4 +++ models/import.js | 48 +++++++++++++------------ 4 files changed, 71 insertions(+), 45 deletions(-) diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 922ac27b..e7b16912 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -32,6 +32,8 @@ template(name="listMoveCardsPopup") +boardLists template(name="listImportCardPopup") + if error.get + .warning {{_ error.get}} form label | {{_ 'card-json'}} diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 9aa26194..7db93618 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -49,30 +49,46 @@ Template.listActionPopup.events({ }, }); -Template.listImportCardPopup.events({ - submit(evt) { - // 1. get the json data out of the form and parse it - evt.preventDefault(); - const jsonData = $(evt.currentTarget).find('textarea').val(); - const firstCardDom = $(`#js-list-${this._id} .js-minicard:first`).get(0); - const sortIndex = Utils.calculateIndex(null, firstCardDom).base; - try { - const trelloCard = JSON.parse(jsonData); - const cardId = Meteor.call('importTrelloCard', trelloCard, this._id, sortIndex); - // In case the filter is active we need to add the newly inserted card in - // the list of exceptions -- cards that are not filtered. Otherwise the - // card will disappear instantly. - // See https://github.com/wekan/wekan/issues/80 - Filter.addException(cardId); - Popup.close(); - } catch(e) { - // XXX handle error - // this.error.set('avatar-too-big'); - console.log('Invalid JSON'); - return; - } + +BlazeComponent.extendComponent({ + events() { + return [{ + 'submit': (evt) => { + evt.preventDefault(); + const jsonData = $(evt.currentTarget).find('textarea').val(); + const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0); + const sortIndex = Utils.calculateIndex(null, firstCardDom).base; + let trelloCard; + try { + trelloCard = JSON.parse(jsonData); + } catch (e) { + console.log(e); + this.setError('error-json-malformed'); + return; + } + Meteor.call('importTrelloCard', trelloCard, this.currentData()._id, sortIndex, + (error, response) => { + if (error) { + console.log(error); + this.setError(error.error); + } else { + Filter.addException(response); + Popup.close(); + } + } + ); + } + },]; }, -}); + + onCreated() { + this.error = new ReactiveVar(''); + }, + + setError(error) { + this.error.set(error); + }, +}).register('listImportCardPopup'); Template.listMoveCardsPopup.events({ 'click .js-select-list'() { diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 40e21e53..9f383ae4 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -112,6 +112,10 @@ "editLabelPopup-title": "Change Label", "editProfilePopup-title": "Edit Profile", "email": "Email", + "error-board-notAMember": "You need to be a member of this board to do that", + "error-json-malformed": "Your text is not valid JSON", + "error-json-schema": "Your JSON data does not include the proper information in the correct format", + "error-list-doesNotExist": "This list does not exist", "filter": "Filter", "filter-cards": "Filter Cards", "filter-clear": "Clear filter", diff --git a/models/import.js b/models/import.js index a76bbc5f..51206ea3 100644 --- a/models/import.js +++ b/models/import.js @@ -1,40 +1,44 @@ Meteor.methods({ - /** - * - */ importTrelloCard(trelloCard, listId, sortIndex) { // 1. check parameters are ok from a syntax point of view DateString = Match.Where(function (dateAsString) { check(dateAsString, String); return moment(dateAsString, moment.ISO_8601).isValid(); }); - check(trelloCard, Match.ObjectIncluding({ - name: String, - desc: String, - closed: Boolean, - dateLastActivity: DateString, - labels: [Match.ObjectIncluding({ + try { + check(trelloCard, Match.ObjectIncluding({ name: String, - color: String, - })], - actions: [Match.ObjectIncluding({ - type: String, - date: DateString, - data: Object, - })], - members: [Object], - })); - check(listId, String); - check(sortIndex, Number); + desc: String, + closed: Boolean, + dateLastActivity: DateString, + labels: [Match.ObjectIncluding({ + name: String, + color: String, + })], + actions: [Match.ObjectIncluding({ + type: String, + date: DateString, + data: Object, + })], + members: [Object], + })); + check(listId, String); + check(sortIndex, Number); + } catch(e) { + if(Meteor.isServer) { + console.log(e); + } + throw new Meteor.Error('error-json-schema'); + } // 2. check parameters are ok from a business point of view (exist & authorized) const list = Lists.findOne(listId); if(!list) { - throw 'exception-list-doesNotExist'; + throw new Meteor.Error('error-list-doesNotExist'); } if(Meteor.isServer) { if (!allowIsBoardMember(Meteor.userId(), Boards.findOne(list.boardId))) { - throw 'exception-board-notAMember'; + throw new Meteor.Error('error-board-notAMember'); } } -- cgit v1.2.3-1-g7c22 From ab761f11868ce5af80d2d54e173827dc043fef02 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 14 Oct 2015 23:30:51 +0200 Subject: Draft v0.10 release notes --- History.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index ddf7032f..462e37a7 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,13 @@ -# NEXT — v0.9 +# v0.10 + +This release features: + +* Card import from Trello + +Thanks to GitHub users AlexanderS, fisle, ndarilek, and xavierpriour for their +contributions. + +# v0.9 This release is a large re-write of the previous code base. Despite being relatively similar to v0.8 feature-wise, this release marks the beginning of our -- cgit v1.2.3-1-g7c22 From 43de3b8a01b001ec665294fa8bd75f5d01df7df0 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 14 Oct 2015 23:40:00 +0200 Subject: Prevent dublicated empty labels of the same color --- client/components/cards/labels.js | 2 +- models/boards.js | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/components/cards/labels.js b/client/components/cards/labels.js index d2ee0140..6a411561 100644 --- a/client/components/cards/labels.js +++ b/client/components/cards/labels.js @@ -69,12 +69,12 @@ Template.formLabel.events({ Template.createLabelPopup.events({ // Create the new label 'submit .create-label'(evt, tpl) { + evt.preventDefault(); const board = Boards.findOne(Session.get('currentBoard')); const name = tpl.$('#labelName').val().trim(); const color = Blaze.getData(tpl.find('.fa-check')).color; board.addLabel(name, color); Popup.back(); - evt.preventDefault(); }, }); diff --git a/models/boards.js b/models/boards.js index 8d2b63e4..fd0212c5 100644 --- a/models/boards.js +++ b/models/boards.js @@ -93,9 +93,7 @@ Boards.helpers({ }, getLabel(name, color) { - return this.labels.find((current) => { - return ((current.name === name) && (current.color === color)); - }); + return _.findWhere(this.labels, { name, color }); }, labelIndex(labelId) { @@ -138,11 +136,22 @@ Boards.mutations({ addLabel(name, color) { const _id = Random.id(6); + + // If an empty label of a given color already exists we don't want to create + // an other one because they would be indistinguishable in the UI (they + // would still have different `_id` but that is not exposed to the user). + if (name === '' && this.getLabel(name, color)) { + return {}; + } return { $push: {labels: { _id, name, color }}}; }, editLabel(labelId, name, color) { const labelIndex = this.labelIndex(labelId); + + if (name === '' && this.getLabel(name, color)) { + return {}; + } return { $set: { [`labels.${labelIndex}.name`]: name, @@ -299,8 +308,8 @@ if (Meteor.isServer) { }); }); - // If the user removes a label from a board, we have to remove references to - // this label in all cards of the board. + // If the user remove one label from a board, we cant to remove reference of + // this label in any card of this board. Boards.after.update((userId, doc, fieldNames, modifier) => { if (!_.contains(fieldNames, 'labels') || !modifier.$pull || -- cgit v1.2.3-1-g7c22 From 944a1065d372095e8a0898a4162ca6bb562bab69 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 14 Oct 2015 23:50:12 +0200 Subject: Add some ESLint rules and fix some related issues --- .eslintrc | 58 +++++++++++++++++++---------------- .travis.yml | 1 + client/components/lists/listHeader.js | 6 ++-- client/lib/accessibility.js | 8 ++--- models/import.js | 15 +++++---- 5 files changed, 46 insertions(+), 42 deletions(-) diff --git a/.eslintrc b/.eslintrc index 2187cf75..081b2a56 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,41 +2,46 @@ ecmaFeatures: experimentalObjectRestSpread: true rules: - accessor-pairs: [2] - consistent-return: [2] + accessor-pairs: 2 + comma-dangle: [2, 'always-multiline'] + consistent-return: 2 + dot-notation: 2 + eqeqeq: 2 indent: [2, 2] - semi: [2, always] - comma-dangle: [2, always-multiline] + no-cond-assign: 2 + no-constant-condition: 2 + no-eval: 2 no-inner-declarations: [0] - dot-notation: [2] - eqeqeq: [2] - no-eval: [2] - radix: [2] + no-unneeded-ternary: 2 + radix: 2 + semi: [2, always] # Stylistic Issues - camelcase: [2] - comma-spacing: [2] - comma-style: [2] - new-parens: [2] - no-lonely-if: [2] - no-multiple-empty-lines: [2] - no-nested-ternary: [2] + camelcase: 2 + comma-spacing: 2 + comma-style: 2 linebreak-style: [2, unix] + new-parens: 2 + no-lonely-if: 2 + no-multiple-empty-lines: 2 + no-nested-ternary: 2 + no-spaced-func: 2 + operator-linebreak: 2 quotes: [2, single] - semi-spacing: [2] + semi-spacing: 2 + space-unary-ops: 2 spaced-comment: [2, always, markers: ['/']] - space-unary-ops: [2] # ECMAScript 6 - arrow-parens: [2] - arrow-spacing: [2] - no-class-assign: [2] - no-dupe-class-members: [2] - no-var: [2] - object-shorthand: [2] - prefer-const: [2] - prefer-template: [2] - prefer-spread: [2] + arrow-parens: 2 + arrow-spacing: 2 + no-class-assign: 2 + no-dupe-class-members: 2 + no-var: 2 + object-shorthand: 2 + prefer-const: 2 + prefer-spread: 2 + prefer-template: 2 globals: # Meteor globals @@ -78,6 +83,7 @@ globals: FS: false getSlug: false Migrations: false + moment: false Mousetrap: false Picker: false Presence: true diff --git a/.travis.yml b/.travis.yml index 2499948a..55f8ed48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,6 @@ node_js: - "0.10.40" install: - "npm install -g eslint" + - "npm install -g eslint-plugin-meteor" script: - "eslint ./" diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 7db93618..e34d23fd 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -62,14 +62,12 @@ BlazeComponent.extendComponent({ try { trelloCard = JSON.parse(jsonData); } catch (e) { - console.log(e); this.setError('error-json-malformed'); return; } Meteor.call('importTrelloCard', trelloCard, this.currentData()._id, sortIndex, (error, response) => { if (error) { - console.log(error); this.setError(error.error); } else { Filter.addException(response); @@ -77,8 +75,8 @@ BlazeComponent.extendComponent({ } } ); - } - },]; + }, + }]; }, onCreated() { diff --git a/client/lib/accessibility.js b/client/lib/accessibility.js index 64195b81..52b771d4 100644 --- a/client/lib/accessibility.js +++ b/client/lib/accessibility.js @@ -7,7 +7,7 @@ // Without an href, links are non-keyboard-focusable and are not presented on // blind screen readers. We default to the empty anchor `#` href. function enforceHref(attributes) { - if (! _.has(attributes, 'href')) { + if (!_.has(attributes, 'href')) { attributes.href = '#'; } return attributes; @@ -17,7 +17,7 @@ function enforceHref(attributes) { // presented by screen readers. `aria-label`, on the other hand, is specific to // accessibility and is presented in ways that title shouldn't be. function copyTitleInAriaLabel(attributes) { - if (! _.has(attributes, 'aria-label') && _.has(attributes, 'title')) { + if (!_.has(attributes, 'aria-label') && _.has(attributes, 'title')) { attributes['aria-label'] = attributes.title; } return attributes; @@ -34,8 +34,8 @@ const { HTML.A = (attributes, ...others) => { return superA(copyTitleInAriaLabel(enforceHref(attributes)), ...others); -} +}; HTML.I = (attributes, ...others) => { return superI(copyTitleInAriaLabel(attributes), ...others); -} +}; diff --git a/models/import.js b/models/import.js index 51206ea3..4fe4b478 100644 --- a/models/import.js +++ b/models/import.js @@ -1,7 +1,7 @@ Meteor.methods({ importTrelloCard(trelloCard, listId, sortIndex) { // 1. check parameters are ok from a syntax point of view - DateString = Match.Where(function (dateAsString) { + const DateString = Match.Where(function (dateAsString) { check(dateAsString, String); return moment(dateAsString, moment.ISO_8601).isValid(); }); @@ -25,9 +25,6 @@ Meteor.methods({ check(listId, String); check(sortIndex, Number); } catch(e) { - if(Meteor.isServer) { - console.log(e); - } throw new Meteor.Error('error-json-schema'); } @@ -59,7 +56,9 @@ Meteor.methods({ }; // 4. find actual creation date - const creationAction = trelloCard.actions.find((action) => {return action.type === 'createCard';}); + const creationAction = trelloCard.actions.find((action) => { + return action.type === 'createCard'; + }); if(creationAction) { cardToCreate.createdAt = creationAction.date; } @@ -92,7 +91,7 @@ Meteor.methods({ Activities.direct.insert({ activityType: 'importCard', boardId: cardToCreate.boardId, - cardId: cardId, + cardId, createdAt: dateOfImport, listId: cardToCreate.listId, source: { @@ -109,7 +108,7 @@ Meteor.methods({ if(currentAction.type === 'commentCard') { const commentToCreate = { boardId: list.boardId, - cardId: cardId, + cardId, createdAt: currentAction.date, text: currentAction.data.text, // XXX use the original comment user instead @@ -120,7 +119,7 @@ Meteor.methods({ activityType: 'addComment', boardId: commentToCreate.boardId, cardId: commentToCreate.cardId, - commentId: commentId, + commentId, createdAt: commentToCreate.createdAt, userId: commentToCreate.userId, }); -- cgit v1.2.3-1-g7c22 From 15ebfa63c61694e4aa60e0f9c5047f678d6cf0c4 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 15 Oct 2015 00:52:32 +0200 Subject: Translate the label colors --- .meteor/versions | 4 ++-- client/components/sidebar/sidebarFilters.jade | 2 +- client/config/blazeHelpers.js | 4 ++++ i18n/en.i18n.json | 10 ++++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.meteor/versions b/.meteor/versions index 5ced1e20..586c3297 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -87,8 +87,8 @@ mongo-livedata@1.0.9 mousetrap:mousetrap@1.4.6_1 mquandalle:autofocus@1.0.0 mquandalle:collection-mutations@0.1.0 -mquandalle:jade@0.4.3_1 -mquandalle:jade-compiler@0.4.3 +mquandalle:jade@0.4.4 +mquandalle:jade-compiler@0.4.4 mquandalle:jquery-textcomplete@0.8.0_1 mquandalle:jquery-ui-drag-drop-sort@0.1.0 mquandalle:moment@1.0.0 diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index d9275314..ef26ef76 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -13,7 +13,7 @@ template(name="filterSidebar") if name = name else - span.quiet {{_ "label-default" color}} + span.quiet {{_ "label-default" (_ (concat "color-" color))}} if Filter.labelIds.isSelected _id i.fa.fa-check hr diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js index 12990ed7..adf5ef6a 100644 --- a/client/config/blazeHelpers.js +++ b/client/config/blazeHelpers.js @@ -13,3 +13,7 @@ Blaze.registerHelper('currentCard', () => { }); Blaze.registerHelper('getUser', (userId) => Users.findOne(userId)); + +UI.registerHelper('concat', function (...args) { + return Array.prototype.slice.call(args, 0, -1).join(''); +}); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 9f383ae4..efc6128d 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -90,6 +90,16 @@ "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”.", + "color-green": "green", + "color-yellow": "yellow", + "color-orange": "orange", + "color-red": "red", + "color-purple": "purple", + "color-blue": "blue", + "color-sky": "sky", + "color-lime": "lime", + "color-pink": "pink", + "color-black": "black", "comment": "Comment", "comment-placeholder": "Write a comment", "computer": "Computer", -- cgit v1.2.3-1-g7c22 From 6dedf673d5bde58c8d6ca439825cbf84303a3b4e Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Fri, 16 Oct 2015 17:49:25 +0200 Subject: Prevent duplicate board labels 43de3b8 did prevent empty labels with the same color, but we also want to prevent label with the same non-empty name and same color because the rationale is identical. --- models/boards.js | 33 +++++++++++++++------------------ models/import.js | 40 ++++++++++++++++------------------------ 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/models/boards.js b/models/boards.js index fd0212c5..d7d40251 100644 --- a/models/boards.js +++ b/models/boards.js @@ -135,29 +135,26 @@ Boards.mutations({ }, addLabel(name, color) { - const _id = Random.id(6); - - // If an empty label of a given color already exists we don't want to create - // an other one because they would be indistinguishable in the UI (they - // would still have different `_id` but that is not exposed to the user). - if (name === '' && this.getLabel(name, color)) { - return {}; + // If label with the same name and color already exists we don't want to + // create another one because they would be indistinguishable in the UI + // (they would still have different `_id` but that is not exposed to the + // user). + if (!this.getLabel(name, color)) { + const _id = Random.id(6); + return { $push: {labels: { _id, name, color }}}; } - return { $push: {labels: { _id, name, color }}}; }, editLabel(labelId, name, color) { - const labelIndex = this.labelIndex(labelId); - - if (name === '' && this.getLabel(name, color)) { - return {}; + if (!this.getLabel(name, color)) { + const labelIndex = this.labelIndex(labelId); + return { + $set: { + [`labels.${labelIndex}.name`]: name, + [`labels.${labelIndex}.color`]: color, + }, + }; } - return { - $set: { - [`labels.${labelIndex}.name`]: name, - [`labels.${labelIndex}.color`]: color, - }, - }; }, removeLabel(labelId) { diff --git a/models/import.js b/models/import.js index 4fe4b478..e1f77efc 100644 --- a/models/import.js +++ b/models/import.js @@ -24,16 +24,17 @@ Meteor.methods({ })); check(listId, String); check(sortIndex, Number); - } catch(e) { + } catch (e) { throw new Meteor.Error('error-json-schema'); } - // 2. check parameters are ok from a business point of view (exist & authorized) + // 2. check parameters are ok from a business point of view (exist & + // authorized) const list = Lists.findOne(listId); - if(!list) { + if (!list) { throw new Meteor.Error('error-list-doesNotExist'); } - if(Meteor.isServer) { + if (Meteor.isServer) { if (!allowIsBoardMember(Meteor.userId(), Boards.findOne(list.boardId))) { throw new Meteor.Error('error-board-notAMember'); } @@ -48,6 +49,7 @@ Meteor.methods({ createdAt: dateOfImport, dateLastActivity: dateOfImport, description: trelloCard.desc, + labelIds: [], listId: list._id, sort: sortIndex, title: trelloCard.name, @@ -59,29 +61,18 @@ Meteor.methods({ const creationAction = trelloCard.actions.find((action) => { return action.type === 'createCard'; }); - if(creationAction) { + if (creationAction) { cardToCreate.createdAt = creationAction.date; } // 5. map labels - create missing ones trelloCard.labels.forEach((currentLabel) => { - const color = currentLabel.color; - const name = currentLabel.name; - const existingLabel = list.board().getLabel(name, color); - let labelId = undefined; - if (existingLabel) { - labelId = existingLabel._id; - } else { - let labelCreated = list.board().addLabel(name, color); - // XXX currently mutations return no value so we have to fetch the label we just created - // waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... - labelCreated = list.board().getLabel(name, color); - labelId = labelCreated._id; - } - if(labelId) { - if (!cardToCreate.labelIds) { - cardToCreate.labelIds = []; - } + const { name, color } = currentLabel; + // `addLabel` won't create dublicate labels (ie labels with the same name + // and color) so here it is used more in a "enforceLabelExistence" way. + list.board().addLabel(name, color); + const { _id: labelId } = list.board().getLabel(name, color); + if (labelId) { cardToCreate.labelIds.push(labelId); } }); @@ -99,13 +90,14 @@ Meteor.methods({ system: 'Trello', url: trelloCard.url, }, - // we attribute the import to current user, not the one from the original card + // we attribute the import to current user, not the one from the original + // card userId: Meteor.userId(), }); // 7. parse actions and add comments trelloCard.actions.forEach((currentAction) => { - if(currentAction.type === 'commentCard') { + if (currentAction.type === 'commentCard') { const commentToCreate = { boardId: list.boardId, cardId, -- cgit v1.2.3-1-g7c22 From 468694a84cc164e4923f2d2e4631c37ceb1c4b55 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Thu, 15 Oct 2015 14:01:13 +0200 Subject: Import board: added UI --- client/components/boards/boardHeader.jade | 2 + client/components/boards/boardHeader.js | 1 + client/components/boards/boardHeader.styl | 2 + client/components/import/import.jade | 8 ++++ client/components/import/import.js | 80 +++++++++++++++++++++++++++++++ client/components/lists/listHeader.jade | 9 ---- client/components/lists/listHeader.js | 39 --------------- i18n/en.i18n.json | 8 +++- models/import.js | 25 ++++++++-- 9 files changed, 119 insertions(+), 55 deletions(-) create mode 100644 client/components/boards/boardHeader.styl create mode 100644 client/components/import/import.jade create mode 100644 client/components/import/import.js diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index ffc79143..e460170b 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -107,6 +107,8 @@ template(name="createBoardPopup") | {{{_ 'board-private-info'}}} a.js-change-visibility {{_ 'change'}}. input.primary.wide(type="submit" value="{{_ 'create'}}") + | {{_ 'or'}} + a.js-import {{_ 'import-board'}} template(name="boardChangeTitlePopup") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index dbd76895..92d5f6d4 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -145,6 +145,7 @@ BlazeComponent.extendComponent({ this.setVisibility(this.currentData()); }, 'click .js-change-visibility': this.toggleVisibilityMenu, + 'click .js-import': Popup.open('boardImportBoard'), submit: this.onSubmit, }]; }, diff --git a/client/components/boards/boardHeader.styl b/client/components/boards/boardHeader.styl new file mode 100644 index 00000000..adfe4b19 --- /dev/null +++ b/client/components/boards/boardHeader.styl @@ -0,0 +1,2 @@ +a.js-import + text-decoration underline diff --git a/client/components/import/import.jade b/client/components/import/import.jade new file mode 100644 index 00000000..8059b65b --- /dev/null +++ b/client/components/import/import.jade @@ -0,0 +1,8 @@ +template(name="importPopup") + if error.get + .warning {{_ error.get}} + form + label + | {{_ getLabel}} + textarea.js-card-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) + input.primary.wide(type="submit" value="{{_ 'import'}}") diff --git a/client/components/import/import.js b/client/components/import/import.js new file mode 100644 index 00000000..f15185ed --- /dev/null +++ b/client/components/import/import.js @@ -0,0 +1,80 @@ +/** + * Abstract root for all import popup screens. + * Descendants must define: + * - getMethodName(): return the Meteor method to call for import, passing json data decoded as object + * and additional data (see below) + * - getAdditionalData(): return object containing additional data passed to Meteor method + * (like list ID and position for a card import) + * - getLabel(): i18n key for the text displayed in the popup, usually to explain how to get the data out of the + * source system. + */ +const ImportPopup = BlazeComponent.extendComponent({ + template() {return 'importPopup';}, + events() { + return [{ + 'submit': (evt) => { + evt.preventDefault(); + const dataJson = $(evt.currentTarget).find('textarea').val(); + let dataObject; + try { + dataObject = JSON.parse(dataJson); + } catch (e) { + this.setError('error-json-malformed'); + return; + } + Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(), + (error, response) => { + if (error) { + this.setError(error.error); + } else { + Filter.addException(response); + Popup.close(); + } + } + ); + }, + }]; + }, + + onCreated() { + this.error = new ReactiveVar(''); + }, + + setError(error) { + this.error.set(error); + }, +}); + +ImportPopup.extendComponent({ + getAdditionalData() { + const listId = this.data()._id; + const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0); + const sortIndex = Utils.calculateIndex(null, firstCardDom).base; + const result = {listId, sortIndex}; + return result; + }, + + getMethodName() { + return 'importTrelloCard'; + }, + + getLabel() { + return 'import-card-trello-instruction'; + }, +}).register('listImportCardPopup'); + +ImportPopup.extendComponent({ + getAdditionalData() { + const result = {}; + return result; + }, + + getMethodName() { + return 'importTrelloBoard'; + }, + + getLabel() { + return 'import-board-trello-instruction'; + }, +}).register('boardImportBoardPopup'); + diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index e7b16912..72cd0fe9 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -31,15 +31,6 @@ template(name="listActionPopup") template(name="listMoveCardsPopup") +boardLists -template(name="listImportCardPopup") - if error.get - .warning {{_ error.get}} - form - label - | {{_ 'card-json'}} - textarea.js-card-json(placeholder="{{_ 'card-json-placeholder'}}" autofocus) - input.primary.wide(type="submit" value="{{_ 'import'}}") - template(name="boardLists") ul.pop-over-list each currentBoard.lists diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index e34d23fd..4f5fc3a0 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -49,45 +49,6 @@ Template.listActionPopup.events({ }, }); - -BlazeComponent.extendComponent({ - events() { - return [{ - 'submit': (evt) => { - evt.preventDefault(); - const jsonData = $(evt.currentTarget).find('textarea').val(); - const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0); - const sortIndex = Utils.calculateIndex(null, firstCardDom).base; - let trelloCard; - try { - trelloCard = JSON.parse(jsonData); - } catch (e) { - this.setError('error-json-malformed'); - return; - } - Meteor.call('importTrelloCard', trelloCard, this.currentData()._id, sortIndex, - (error, response) => { - if (error) { - this.setError(error.error); - } else { - Filter.addException(response); - Popup.close(); - } - } - ); - }, - }]; - }, - - onCreated() { - this.error = new ReactiveVar(''); - }, - - setError(error) { - this.error.set(error); - }, -}).register('listImportCardPopup'); - Template.listMoveCardsPopup.events({ 'click .js-select-list'() { const fromList = Template.parentData(2).data; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index efc6128d..82ea14e5 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -54,6 +54,7 @@ "boardChangeColorPopup-title": "Change Board Background", "boardChangeTitlePopup-title": "Rename Board", "boardChangeVisibilityPopup-title": "Change Visibility", + "boardImportBoardPopup-title": "Import board from Trello", "boardMenuPopup-title": "Board Menu", "boards": "Boards", "bucket-example": "Like “Bucket List” for example", @@ -66,8 +67,6 @@ "card-edit-attachments": "Edit attachments", "card-edit-labels": "Edit labels", "card-edit-members": "Edit members", - "card-json": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text", - "card-json-placeholder": "Paste your valid JSON data here", "card-labels-title": "Change the labels for the card.", "card-members-title": "Add or remove members of the board from the card.", "cardAttachmentsPopup-title": "Attach From", @@ -136,7 +135,11 @@ "header-logo-title": "Go back to your boards page.", "home": "Home", "import": "Import", + "import-board": "import from Trello", + "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text", "import-card": "Import a Trello card", + "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text", + "import-json-placeholder": "Paste your valid JSON data here", "info": "Infos", "initials": "Initials", "joined": "joined", @@ -175,6 +178,7 @@ "normal": "Normal", "normal-desc": "Can view and edit cards. Can't change settings.", "optional": "optional", + "or": "or", "page-maybe-private": "This page may be private. You may be able to view it by logging in.", "page-not-found": "Page not found.", "password": "Password", diff --git a/models/import.js b/models/import.js index 4fe4b478..e81dd42a 100644 --- a/models/import.js +++ b/models/import.js @@ -1,5 +1,5 @@ Meteor.methods({ - importTrelloCard(trelloCard, listId, sortIndex) { + importTrelloCard(trelloCard, data) { // 1. check parameters are ok from a syntax point of view const DateString = Match.Where(function (dateAsString) { check(dateAsString, String); @@ -22,14 +22,16 @@ Meteor.methods({ })], members: [Object], })); - check(listId, String); - check(sortIndex, Number); + check(data, { + listId: String, + sortIndex: Number, + }); } catch(e) { throw new Meteor.Error('error-json-schema'); } // 2. check parameters are ok from a business point of view (exist & authorized) - const list = Lists.findOne(listId); + const list = Lists.findOne(data.listId); if(!list) { throw new Meteor.Error('error-list-doesNotExist'); } @@ -49,7 +51,7 @@ Meteor.methods({ dateLastActivity: dateOfImport, description: trelloCard.desc, listId: list._id, - sort: sortIndex, + sort: data.sortIndex, title: trelloCard.name, // XXX use the original user? userId: Meteor.userId(), @@ -127,4 +129,17 @@ Meteor.methods({ }); return cardId; }, + importTrelloBoard(trelloBoard, data) { + // 1. check parameters are ok from a syntax point of view + try { + // XXX do proper checking + check(trelloBoard, Object); + check(data, Object); + } catch(e) { + throw new Meteor.Error('error-json-schema'); + } + // 2. check parameters are ok from a business point of view (exist & authorized) + // XXX check we are allowed + // 3. create all elements + }, }); -- cgit v1.2.3-1-g7c22 From 595d5f97ac7b95ff71b391071d7d339e4ccbd4f6 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Sat, 17 Oct 2015 19:29:25 +0200 Subject: Import board: now proper createdAt dates --- client/components/import/import.js | 10 +++++- models/import.js | 73 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/client/components/import/import.js b/client/components/import/import.js index f15185ed..a2972562 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -28,7 +28,7 @@ const ImportPopup = BlazeComponent.extendComponent({ this.setError(error.error); } else { Filter.addException(response); - Popup.close(); + this.onFinish(response); } } ); @@ -43,6 +43,10 @@ const ImportPopup = BlazeComponent.extendComponent({ setError(error) { this.error.set(error); }, + + onFinish() { + Popup.close(); + } }); ImportPopup.extendComponent({ @@ -76,5 +80,9 @@ ImportPopup.extendComponent({ getLabel() { return 'import-board-trello-instruction'; }, + + onFinish(response) { + Utils.goBoardId(response); + }, }).register('boardImportBoardPopup'); diff --git a/models/import.js b/models/import.js index e81dd42a..e0badc66 100644 --- a/models/import.js +++ b/models/import.js @@ -1,3 +1,72 @@ +const trelloCreator = { + // the object creation dates, indexed by Trello id (so we only parse actions once!) + createdAt: { + board: null, + cards: {}, + lists: {}, + }, + + // the labels we created, indexed by Trello id (to map when importing cards) + labels: {}, + + /** + * must call parseActions before calling this one + */ + createBoard(trelloBoard, dateOfImport) { + const createdAt = this.createdAt.board; + const boardToCreate = { + archived: trelloBoard.closed, + // XXX map from Trello colors + color: Boards.simpleSchema()._schema.color.allowedValues[0], + createdAt, + labels: [], + members: [{ + userId: Meteor.userId(), + isAdmin: true, + isActive: true, + }], + // XXX make a more robust mapping algorithm? + permission: trelloBoard.prefs.permissionLevel, + slug: getSlug(trelloBoard.name) || 'board', + stars: 0, + title: trelloBoard.name, + }; + trelloBoard.labels.forEach((label) => { + labelToCreate = { + _id: Random.id(6), + color: label.color, + name: label.name, + }; + // we need to remember them by Trello ID, as this is the only ref we have when importing cards + this.labels[label.id] = labelToCreate; + boardToCreate.labels.push(labelToCreate); + }); + const boardId = Boards.direct.insert(boardToCreate); + return boardId; + }, + + parseActions(trelloActions) { + trelloActions.forEach((action) =>{ + switch (action.type) { + case 'createBoard': + this.createdAt.board = action.date; + break; + case 'createCard': + const cardId = action.data.card.id; + this.createdAt.cards[cardId] = action.date; + break; + case 'createList': + const listId = action.data.list.id; + this.createdAt.lists[listId] = action.date; + break; + default: + // do nothing + break; + } + }); + } +} + Meteor.methods({ importTrelloCard(trelloCard, data) { // 1. check parameters are ok from a syntax point of view @@ -141,5 +210,9 @@ Meteor.methods({ // 2. check parameters are ok from a business point of view (exist & authorized) // XXX check we are allowed // 3. create all elements + const dateOfImport = new Date(); + trelloCreator.parseActions(trelloBoard.actions); + const boardId = trelloCreator.createBoard(trelloBoard, dateOfImport); + return boardId; }, }); -- cgit v1.2.3-1-g7c22 From 469d47cd9f6e92518beb5f28cc7e755bf2ae1578 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Sun, 18 Oct 2015 01:02:44 +0200 Subject: Import board: create board, lists, and cards --- models/import.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/models/import.js b/models/import.js index e0badc66..d0b25173 100644 --- a/models/import.js +++ b/models/import.js @@ -1,18 +1,21 @@ -const trelloCreator = { - // the object creation dates, indexed by Trello id (so we only parse actions once!) - createdAt: { - board: null, - cards: {}, - lists: {}, - }, - - // the labels we created, indexed by Trello id (to map when importing cards) - labels: {}, +class TrelloCreator { + constructor() { + // the object creation dates, indexed by Trello id (so we only parse actions once!) + this.createdAt = { + board: null, + cards: {}, + lists: {}, + }; + // the labels we created, indexed by Trello id (to map when importing cards) + this.labels = {}; + // the lists we created, indexed by Trello id (to map when importing cards) + this.lists = {}; + } /** * must call parseActions before calling this one */ - createBoard(trelloBoard, dateOfImport) { + createBoardAndLabels(trelloBoard, dateOfImport) { const createdAt = this.createdAt.board; const boardToCreate = { archived: trelloBoard.closed, @@ -42,8 +45,51 @@ const trelloCreator = { boardToCreate.labels.push(labelToCreate); }); const boardId = Boards.direct.insert(boardToCreate); + // XXX add activities return boardId; - }, + } + + createLists(trelloLists, boardId, dateOfImport) { + trelloLists.forEach((list) => { + const listToCreate = { + archived: list.closed, + boardId, + createdAt: this.createdAt.lists[list.id], + title: list.name, + userId: Meteor.userId(), + }; + listToCreate._id = Lists.direct.insert(listToCreate); + this.lists[list.id] = listToCreate; + // XXX add activities + }); + } + + createCards(trelloCards, boardId, dateOfImport) { + trelloCards.forEach((card) => { + const cardToCreate = { + archived: card.closed, + boardId, + createdAt: this.createdAt.cards[card.id], + dateLastActivity: dateOfImport, + description: card.desc, + listId: this.lists[card.idList]._id, + sort: card.pos, + title: card.name, + // XXX use the original user? + userId: Meteor.userId(), + }; + // add labels + if(card.idLabels) { + cardToCreate.labelIds = card.idLabels.map((trelloId) => { + return this.labels[trelloId]._id; + }); + } + Cards.direct.insert(cardToCreate); + // XXX add comments + // XXX add attachments + // XXX add activities + }); + } parseActions(trelloActions) { trelloActions.forEach((action) =>{ @@ -59,6 +105,7 @@ const trelloCreator = { const listId = action.data.list.id; this.createdAt.lists[listId] = action.date; break; + // XXX extract comments as well default: // do nothing break; @@ -199,6 +246,7 @@ Meteor.methods({ return cardId; }, importTrelloBoard(trelloBoard, data) { + const trelloCreator = new TrelloCreator(); // 1. check parameters are ok from a syntax point of view try { // XXX do proper checking @@ -212,7 +260,12 @@ Meteor.methods({ // 3. create all elements const dateOfImport = new Date(); trelloCreator.parseActions(trelloBoard.actions); - const boardId = trelloCreator.createBoard(trelloBoard, dateOfImport); + const boardId = trelloCreator.createBoardAndLabels(trelloBoard, dateOfImport); + trelloCreator.createLists(trelloBoard.lists, boardId, dateOfImport); + trelloCreator.createCards(trelloBoard.cards, boardId, dateOfImport); + // XXX add activities + // XXX set modifiedAt or lastActivity + // XXX add members return boardId; }, }); -- cgit v1.2.3-1-g7c22 From 4540bd36c4b07080ea5d29f0fb31bb20e637c2d5 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 00:59:50 +0200 Subject: Import board: import comments and log activities --- client/components/activities/activities.jade | 56 +++++---- client/components/activities/activities.js | 17 ++- client/components/import/import.js | 2 +- i18n/en.i18n.json | 1 + models/import.js | 166 +++++++++++++++++++-------- 5 files changed, 164 insertions(+), 78 deletions(-) diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index c611ad75..28a9f9c9 100644 --- a/client/components/activities/activities.jade +++ b/client/components/activities/activities.jade @@ -14,41 +14,56 @@ template(name="boardActivities") p.activity-desc +memberName(user=user) - if($eq activityType 'createBoard') - | {{_ 'activity-created' boardLabel}}. + if($eq activityType 'addAttachment') + | {{{_ 'activity-attached' attachmentLink cardLink}}}. - if($eq activityType 'createList') - | {{_ 'activity-added' list.title boardLabel}}. + if($eq activityType 'addBoardMember') + | {{{_ 'activity-added' memberLink boardLabel}}}. + + if($eq activityType 'addComment') + | {{{_ 'activity-on' cardLink}}} + a.activity-comment(href="{{ card.absoluteUrl }}") + +viewer + = comment.text + + if($eq activityType 'archivedCard') + | {{{_ 'activity-archived' cardLink}}}. if($eq activityType 'archivedList') | {{_ 'activity-archived' list.title}}. + if($eq activityType 'createBoard') + | {{_ 'activity-created' boardLabel}}. + if($eq activityType 'createCard') | {{{_ 'activity-added' cardLink boardLabel}}}. + if($eq activityType 'createList') + | {{_ 'activity-added' list.title boardLabel}}. + + if($eq activityType 'importBoard') + | {{{_ 'activity-imported-board' boardLabel sourceLink}}}. + if($eq activityType 'importCard') | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}. - if($eq activityType 'archivedCard') - | {{{_ 'activity-archived' cardLink}}}. + if($eq activityType 'importList') + | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}. - if($eq activityType 'restoredCard') - | {{{_ 'activity-sent' cardLink boardLabel}}}. + if($eq activityType 'joinMember') + if($eq currentUser._id member._id) + | {{{_ 'activity-joined' cardLink}}}. + else + | {{{_ 'activity-added' memberLink cardLink}}}. if($eq activityType 'moveCard') | {{{_ 'activity-moved' cardLink oldList.title list.title}}}. - if($eq activityType 'addBoardMember') - | {{{_ 'activity-added' memberLink boardLabel}}}. - if($eq activityType 'removeBoardMember') | {{{_ 'activity-excluded' memberLink boardLabel}}}. - if($eq activityType 'joinMember') - if($eq currentUser._id member._id) - | {{{_ 'activity-joined' cardLink}}}. - else - | {{{_ 'activity-added' memberLink cardLink}}}. + if($eq activityType 'restoredCard') + | {{{_ 'activity-sent' cardLink boardLabel}}}. if($eq activityType 'unjoinMember') if($eq currentUser._id member._id) @@ -56,15 +71,6 @@ template(name="boardActivities") else | {{{_ 'activity-removed' memberLink cardLink}}}. - if($eq activityType 'addComment') - | {{{_ 'activity-on' cardLink}}} - a.activity-comment(href="{{ card.absoluteUrl }}") - +viewer - = comment.text - - if($eq activityType 'addAttachment') - | {{{_ 'activity-attached' attachmentLink cardLink}}}. - span.activity-meta {{ moment createdAt }} template(name="cardActivities") diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index b80493f7..b25c0ca8 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -60,11 +60,22 @@ BlazeComponent.extendComponent({ }, card.title)); }, + listLabel() { + return this.currentData().list().title; + }, + sourceLink() { const source = this.currentData().source; - return source && Blaze.toHTML(HTML.A({ - href: source.url, - }, source.system)); + if(source) { + if(source.url) { + return Blaze.toHTML(HTML.A({ + href: source.url, + }, source.system)); + } else { + return source.system; + } + } + return null; }, memberLink() { diff --git a/client/components/import/import.js b/client/components/import/import.js index a2972562..00918aac 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -46,7 +46,7 @@ const ImportPopup = BlazeComponent.extendComponent({ onFinish() { Popup.close(); - } + }, }); ImportPopup.extendComponent({ diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 82ea14e5..0823ba08 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -8,6 +8,7 @@ "activity-created": "created %s", "activity-excluded": "excluded %s from %s", "activity-imported": "imported %s into %s from %s", + "activity-imported-board": "imported %s from %s", "activity-joined": "joined %s", "activity-moved": "moved %s from %s to %s", "activity-on": "on %s", diff --git a/models/import.js b/models/import.js index d0b25173..5095ee2e 100644 --- a/models/import.js +++ b/models/import.js @@ -10,12 +10,14 @@ class TrelloCreator { this.labels = {}; // the lists we created, indexed by Trello id (to map when importing cards) this.lists = {}; + // the comments, indexed by Trello card id (to map when importing cards) + this.comments = {}; } /** * must call parseActions before calling this one */ - createBoardAndLabels(trelloBoard, dateOfImport) { + createBoardAndLabels(trelloBoard) { const createdAt = this.createdAt.board; const boardToCreate = { archived: trelloBoard.closed, @@ -35,7 +37,7 @@ class TrelloCreator { title: trelloBoard.name, }; trelloBoard.labels.forEach((label) => { - labelToCreate = { + const labelToCreate = { _id: Random.id(6), color: label.color, name: label.name, @@ -45,11 +47,23 @@ class TrelloCreator { boardToCreate.labels.push(labelToCreate); }); const boardId = Boards.direct.insert(boardToCreate); - // XXX add activities + // log activity + Activities.direct.insert({ + activityType: 'importBoard', + boardId, + createdAt: new Date(), + source: { + id: trelloBoard.id, + system: 'Trello', + url: trelloBoard.url, + }, + // we attribute the import to current user, not the one from the original object + userId: Meteor.userId(), + }); return boardId; } - createLists(trelloLists, boardId, dateOfImport) { + createLists(trelloLists, boardId) { trelloLists.forEach((list) => { const listToCreate = { archived: list.closed, @@ -60,17 +74,29 @@ class TrelloCreator { }; listToCreate._id = Lists.direct.insert(listToCreate); this.lists[list.id] = listToCreate; - // XXX add activities + // log activity + Activities.direct.insert({ + activityType: 'importList', + boardId, + createdAt: new Date(), + listId: listToCreate._id, + source: { + id: list.id, + system: 'Trello', + }, + // we attribute the import to current user, not the one from the original object + userId: Meteor.userId(), + }); }); } - createCards(trelloCards, boardId, dateOfImport) { + createCardsAndComments(trelloCards, boardId) { trelloCards.forEach((card) => { const cardToCreate = { archived: card.closed, boardId, createdAt: this.createdAt.cards[card.id], - dateLastActivity: dateOfImport, + dateLastActivity: new Date(), description: card.desc, listId: this.lists[card.idList]._id, sort: card.pos, @@ -84,37 +110,102 @@ class TrelloCreator { return this.labels[trelloId]._id; }); } - Cards.direct.insert(cardToCreate); - // XXX add comments + // insert card + const cardId = Cards.direct.insert(cardToCreate); + // log activity + Activities.direct.insert({ + activityType: 'importCard', + boardId, + cardId, + createdAt: new Date(), + listId: cardToCreate.listId, + source: { + id: card.id, + system: 'Trello', + url: card.url, + }, + // we attribute the import to current user, not the one from the original card + userId: Meteor.userId(), + }); + // add comments + const comments = this.comments[card.id]; + if(comments) { + comments.forEach((comment) => { + const commentToCreate = { + boardId, + cardId, + createdAt: comment.date, + text: comment.data.text, + // XXX use the original comment user instead + userId: Meteor.userId(), + }; + const commentId = CardComments.direct.insert(commentToCreate); + Activities.direct.insert({ + activityType: 'addComment', + boardId: commentToCreate.boardId, + cardId: commentToCreate.cardId, + commentId, + createdAt: commentToCreate.createdAt, + userId: commentToCreate.userId, + }); + }); + } // XXX add attachments - // XXX add activities }); } parseActions(trelloActions) { - trelloActions.forEach((action) =>{ + trelloActions.forEach((action) => { switch (action.type) { - case 'createBoard': - this.createdAt.board = action.date; - break; - case 'createCard': - const cardId = action.data.card.id; - this.createdAt.cards[cardId] = action.date; - break; - case 'createList': - const listId = action.data.list.id; - this.createdAt.lists[listId] = action.date; - break; - // XXX extract comments as well - default: - // do nothing - break; + case 'createBoard': + this.createdAt.board = action.date; + break; + case 'createCard': + const cardId = action.data.card.id; + this.createdAt.cards[cardId] = action.date; + break; + case 'createList': + const listId = action.data.list.id; + this.createdAt.lists[listId] = action.date; + break; + case 'commentCard': + const id = action.data.card.id; + if(this.comments[id]) { + this.comments[id].push(action); + } else { + this.comments[id] = [action]; + } + break; + default: + // do nothing + break; } }); } } Meteor.methods({ + importTrelloBoard(trelloBoard, data) { + const trelloCreator = new TrelloCreator(); + // 1. check parameters are ok from a syntax point of view + try { + // XXX do proper checking + check(trelloBoard, Object); + check(data, Object); + } catch(e) { + throw new Meteor.Error('error-json-schema'); + } + // 2. check parameters are ok from a business point of view (exist & authorized) + // XXX check we are allowed + // 3. create all elements + trelloCreator.parseActions(trelloBoard.actions); + const boardId = trelloCreator.createBoardAndLabels(trelloBoard); + trelloCreator.createLists(trelloBoard.lists, boardId); + trelloCreator.createCardsAndComments(trelloBoard.cards, boardId); + // XXX set modifiedAt or lastActivity + // XXX add members + return boardId; + }, importTrelloCard(trelloCard, data) { // 1. check parameters are ok from a syntax point of view const DateString = Match.Where(function (dateAsString) { @@ -245,27 +336,4 @@ Meteor.methods({ }); return cardId; }, - importTrelloBoard(trelloBoard, data) { - const trelloCreator = new TrelloCreator(); - // 1. check parameters are ok from a syntax point of view - try { - // XXX do proper checking - check(trelloBoard, Object); - check(data, Object); - } catch(e) { - throw new Meteor.Error('error-json-schema'); - } - // 2. check parameters are ok from a business point of view (exist & authorized) - // XXX check we are allowed - // 3. create all elements - const dateOfImport = new Date(); - trelloCreator.parseActions(trelloBoard.actions); - const boardId = trelloCreator.createBoardAndLabels(trelloBoard, dateOfImport); - trelloCreator.createLists(trelloBoard.lists, boardId, dateOfImport); - trelloCreator.createCards(trelloBoard.cards, boardId, dateOfImport); - // XXX add activities - // XXX set modifiedAt or lastActivity - // XXX add members - return boardId; - }, }); -- cgit v1.2.3-1-g7c22 From 456674f1114f6e698891ad3b5b8b4bd505a550c5 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 11:46:04 +0200 Subject: Import board: set proper color and modifiedAt dates --- models/import.js | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/models/import.js b/models/import.js index 5095ee2e..e925d028 100644 --- a/models/import.js +++ b/models/import.js @@ -21,8 +21,7 @@ class TrelloCreator { const createdAt = this.createdAt.board; const boardToCreate = { archived: trelloBoard.closed, - // XXX map from Trello colors - color: Boards.simpleSchema()._schema.color.allowedValues[0], + color: this.getColor(trelloBoard.prefs.background), createdAt, labels: [], members: [{ @@ -46,12 +45,14 @@ class TrelloCreator { this.labels[label.id] = labelToCreate; boardToCreate.labels.push(labelToCreate); }); + const now = new Date(); const boardId = Boards.direct.insert(boardToCreate); + Boards.direct.update(boardId, {$set: {modifiedAt: now}}); // log activity Activities.direct.insert({ activityType: 'importBoard', boardId, - createdAt: new Date(), + createdAt: now, source: { id: trelloBoard.id, system: 'Trello', @@ -72,14 +73,17 @@ class TrelloCreator { title: list.name, userId: Meteor.userId(), }; - listToCreate._id = Lists.direct.insert(listToCreate); + const listId = Lists.direct.insert(listToCreate); + const now = new Date(); + Lists.direct.update(listId, {$set: {'updatedAt': now}}); + listToCreate._id = listId; this.lists[list.id] = listToCreate; // log activity Activities.direct.insert({ activityType: 'importList', boardId, - createdAt: new Date(), - listId: listToCreate._id, + createdAt: now, + listId, source: { id: list.id, system: 'Trello', @@ -139,6 +143,7 @@ class TrelloCreator { // XXX use the original comment user instead userId: Meteor.userId(), }; + // dateLastActivity will be set from activity insert, no need to update it ourselves const commentId = CardComments.direct.insert(commentToCreate); Activities.direct.insert({ activityType: 'addComment', @@ -154,6 +159,23 @@ class TrelloCreator { }); } + getColor(trelloColorCode) { + // trello color name => wekan color + const mapColors = { + 'blue': 'belize', + 'orange': 'pumpkin', + 'green': 'nephritis', + 'red': 'pomegranate', + 'purple': 'wisteria', + 'pink': 'pomegranate', + 'lime': 'nephritis', + 'sky': 'belize', + 'grey': 'midnight', + }; + const wekanColor = mapColors[trelloColorCode]; + return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0]; + } + parseActions(trelloActions) { trelloActions.forEach((action) => { switch (action.type) { @@ -202,7 +224,6 @@ Meteor.methods({ const boardId = trelloCreator.createBoardAndLabels(trelloBoard); trelloCreator.createLists(trelloBoard.lists, boardId); trelloCreator.createCardsAndComments(trelloBoard.cards, boardId); - // XXX set modifiedAt or lastActivity // XXX add members return boardId; }, -- cgit v1.2.3-1-g7c22 From ec304de811d41f2679fc8ef171c0884db8bc9014 Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 12:41:56 +0200 Subject: Import board: check json structure before importing --- models/import.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/models/import.js b/models/import.js index e925d028..742fcf2f 100644 --- a/models/import.js +++ b/models/import.js @@ -1,3 +1,8 @@ +const DateString = Match.Where(function (dateAsString) { + check(dateAsString, String); + return moment(dateAsString, moment.ISO_8601).isValid(); +}); + class TrelloCreator { constructor() { // the object creation dates, indexed by Trello id (so we only parse actions once!) @@ -14,6 +19,50 @@ class TrelloCreator { this.comments = {}; } + checkActions(trelloActions) { + check(trelloActions, [Match.ObjectIncluding({ + data: Object, + date: DateString, + type: String, + })]); + // XXX perform deeper checks based on type + } + + checkBoard(trelloBoard) { + check(trelloBoard, Match.ObjectIncluding({ + closed: Boolean, + labels: [Match.ObjectIncluding({ + // XXX check versus list + color: String, + name: String, + })], + name: String, + prefs: Match.ObjectIncluding({ + // XXX check versus list + background: String, + // XXX check versus list + permissionLevel: String, + }), + })); + } + + checkLists(trelloLists) { + check(trelloLists, [Match.ObjectIncluding({ + closed: Boolean, + name: String, + })]); + } + + checkCards(trelloCards) { + check(trelloCards, [Match.ObjectIncluding({ + closed: Boolean, + desc: String, + // XXX check idLabels + name: String, + pos: Number, + })]); + } + /** * must call parseActions before calling this one */ @@ -29,7 +78,7 @@ class TrelloCreator { isAdmin: true, isActive: true, }], - // XXX make a more robust mapping algorithm? + // current mapping is easy as trello and wekan use same keys: 'private' and 'public' permission: trelloBoard.prefs.permissionLevel, slug: getSlug(trelloBoard.name) || 'board', stars: 0, @@ -209,16 +258,19 @@ class TrelloCreator { Meteor.methods({ importTrelloBoard(trelloBoard, data) { const trelloCreator = new TrelloCreator(); - // 1. check parameters are ok from a syntax point of view + // 1. check all parameters are ok from a syntax point of view try { - // XXX do proper checking - check(trelloBoard, Object); - check(data, Object); + // we don't use additional data - this should be an empty object + check(data, {}); + trelloCreator.checkActions(trelloBoard.actions); + trelloCreator.checkBoard(trelloBoard); + trelloCreator.checkLists(trelloBoard.lists); + trelloCreator.checkCards(trelloBoard.cards); } catch(e) { throw new Meteor.Error('error-json-schema'); } // 2. check parameters are ok from a business point of view (exist & authorized) - // XXX check we are allowed + // nothing to check, everyone can import boards in their account // 3. create all elements trelloCreator.parseActions(trelloBoard.actions); const boardId = trelloCreator.createBoardAndLabels(trelloBoard); -- cgit v1.2.3-1-g7c22 From 8e0ad9119190ac0cfa22827fa278b498eba02d6c Mon Sep 17 00:00:00 2001 From: Xavier Priour Date: Mon, 19 Oct 2015 20:14:29 +0200 Subject: Import board: map team permission, and refactor code to share with card import --- models/boards.js | 8 +++ models/import.js | 204 ++++++++++++++++++++----------------------------------- 2 files changed, 80 insertions(+), 132 deletions(-) diff --git a/models/boards.js b/models/boards.js index fd0212c5..e42e06c6 100644 --- a/models/boards.js +++ b/models/boards.js @@ -111,6 +111,14 @@ Boards.helpers({ colorClass() { return `board-color-${this.color}`; }, + + // XXX currently mutations return no value so we have an issue when using addLabel in import + // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... + pushLabel(name, color) { + const _id = Random.id(6); + Boards.direct.update(this._id, { $push: {labels: { _id, name, color }}}); + return _id; + }, }); Boards.mutations({ diff --git a/models/import.js b/models/import.js index 742fcf2f..be699746 100644 --- a/models/import.js +++ b/models/import.js @@ -11,9 +11,9 @@ class TrelloCreator { cards: {}, lists: {}, }; - // the labels we created, indexed by Trello id (to map when importing cards) + // map of labels Trello ID => Wekan ID this.labels = {}; - // the lists we created, indexed by Trello id (to map when importing cards) + // map of lists Trello ID => Wekan ID this.lists = {}; // the comments, indexed by Trello card id (to map when importing cards) this.comments = {}; @@ -25,41 +25,45 @@ class TrelloCreator { date: DateString, type: String, })]); - // XXX perform deeper checks based on type + // XXX we could perform more thorough checks based on action type } checkBoard(trelloBoard) { check(trelloBoard, Match.ObjectIncluding({ closed: Boolean, - labels: [Match.ObjectIncluding({ - // XXX check versus list - color: String, - name: String, - })], name: String, prefs: Match.ObjectIncluding({ - // XXX check versus list + // XXX refine control by validating 'background' against a list of allowed values (is it worth the maintenance?) background: String, - // XXX check versus list - permissionLevel: String, + permissionLevel: Match.Where((value) => {return ['org', 'private', 'public'].indexOf(value)>= 0;}), }), })); } - checkLists(trelloLists) { - check(trelloLists, [Match.ObjectIncluding({ + checkCards(trelloCards) { + check(trelloCards, [Match.ObjectIncluding({ closed: Boolean, + dateLastActivity: DateString, + desc: String, + idLabels: [String], + idMembers: [String], name: String, + pos: Number, })]); } - checkCards(trelloCards) { - check(trelloCards, [Match.ObjectIncluding({ + checkLabels(trelloLabels) { + check(trelloLabels, [Match.ObjectIncluding({ + // XXX refine control by validating 'color' against a list of allowed values (is it worth the maintenance?) + color: String, + name: String, + })]); + } + + checkLists(trelloLists) { + check(trelloLists, [Match.ObjectIncluding({ closed: Boolean, - desc: String, - // XXX check idLabels name: String, - pos: Number, })]); } @@ -78,8 +82,7 @@ class TrelloCreator { isAdmin: true, isActive: true, }], - // current mapping is easy as trello and wekan use same keys: 'private' and 'public' - permission: trelloBoard.prefs.permissionLevel, + permission: this.getPermission(trelloBoard.prefs.permissionLevel), slug: getSlug(trelloBoard.name) || 'board', stars: 0, title: trelloBoard.name, @@ -91,7 +94,7 @@ class TrelloCreator { name: label.name, }; // we need to remember them by Trello ID, as this is the only ref we have when importing cards - this.labels[label.id] = labelToCreate; + this.labels[label.id] = labelToCreate._id; boardToCreate.labels.push(labelToCreate); }); const now = new Date(); @@ -113,6 +116,23 @@ class TrelloCreator { return boardId; } + /** + * Create labels if they do not exist and load this.labels. + */ + createLabels(trelloLabels, board) { + trelloLabels.forEach((label) => { + const color = label.color; + const name = label.name; + const existingLabel = board.getLabel(name, color); + if (existingLabel) { + this.labels[label.id] = existingLabel._id; + } else { + const idLabelCreated = board.pushLabel(name, color); + this.labels[label.id] = idLabelCreated; + } + }); + } + createLists(trelloLists, boardId) { trelloLists.forEach((list) => { const listToCreate = { @@ -125,8 +145,7 @@ class TrelloCreator { const listId = Lists.direct.insert(listToCreate); const now = new Date(); Lists.direct.update(listId, {$set: {'updatedAt': now}}); - listToCreate._id = listId; - this.lists[list.id] = listToCreate; + this.lists[list.id] = listId; // log activity Activities.direct.insert({ activityType: 'importList', @@ -144,6 +163,7 @@ class TrelloCreator { } createCardsAndComments(trelloCards, boardId) { + const result = []; trelloCards.forEach((card) => { const cardToCreate = { archived: card.closed, @@ -151,7 +171,7 @@ class TrelloCreator { createdAt: this.createdAt.cards[card.id], dateLastActivity: new Date(), description: card.desc, - listId: this.lists[card.idList]._id, + listId: this.lists[card.idList], sort: card.pos, title: card.name, // XXX use the original user? @@ -160,7 +180,7 @@ class TrelloCreator { // add labels if(card.idLabels) { cardToCreate.labelIds = card.idLabels.map((trelloId) => { - return this.labels[trelloId]._id; + return this.labels[trelloId]; }); } // insert card @@ -205,7 +225,9 @@ class TrelloCreator { }); } // XXX add attachments + result.push(cardId); }); + return result; } getColor(trelloColorCode) { @@ -225,6 +247,14 @@ class TrelloCreator { return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0]; } + getPermission(trelloPermissionCode) { + if(trelloPermissionCode === 'public') { + return 'public'; + } + // Wekan does NOT have organization level, so we default both 'private' and 'org' to private. + return 'private'; + } + parseActions(trelloActions) { trelloActions.forEach((action) => { switch (action.type) { @@ -258,19 +288,23 @@ class TrelloCreator { Meteor.methods({ importTrelloBoard(trelloBoard, data) { const trelloCreator = new TrelloCreator(); + // 1. check all parameters are ok from a syntax point of view try { // we don't use additional data - this should be an empty object check(data, {}); trelloCreator.checkActions(trelloBoard.actions); trelloCreator.checkBoard(trelloBoard); + trelloCreator.checkLabels(trelloBoard.labels); trelloCreator.checkLists(trelloBoard.lists); trelloCreator.checkCards(trelloBoard.cards); } catch(e) { throw new Meteor.Error('error-json-schema'); } + // 2. check parameters are ok from a business point of view (exist & authorized) // nothing to check, everyone can import boards in their account + // 3. create all elements trelloCreator.parseActions(trelloBoard.actions); const boardId = trelloCreator.createBoardAndLabels(trelloBoard); @@ -279,33 +313,19 @@ Meteor.methods({ // XXX add members return boardId; }, + importTrelloCard(trelloCard, data) { + const trelloCreator = new TrelloCreator(); + // 1. check parameters are ok from a syntax point of view - const DateString = Match.Where(function (dateAsString) { - check(dateAsString, String); - return moment(dateAsString, moment.ISO_8601).isValid(); - }); try { - check(trelloCard, Match.ObjectIncluding({ - name: String, - desc: String, - closed: Boolean, - dateLastActivity: DateString, - labels: [Match.ObjectIncluding({ - name: String, - color: String, - })], - actions: [Match.ObjectIncluding({ - type: String, - date: DateString, - data: Object, - })], - members: [Object], - })); check(data, { listId: String, sortIndex: Number, }); + trelloCreator.checkCards([trelloCard]); + trelloCreator.checkLabels(trelloCard.labels); + trelloCreator.checkActions(trelloCard.actions); } catch(e) { throw new Meteor.Error('error-json-schema'); } @@ -321,92 +341,12 @@ Meteor.methods({ } } - // 3. map all fields for the card to create - const dateOfImport = new Date(); - const cardToCreate = { - archived: trelloCard.closed, - boardId: list.boardId, - // this is a default date, we'll fetch the actual one from the actions array - createdAt: dateOfImport, - dateLastActivity: dateOfImport, - description: trelloCard.desc, - listId: list._id, - sort: data.sortIndex, - title: trelloCard.name, - // XXX use the original user? - userId: Meteor.userId(), - }; - - // 4. find actual creation date - const creationAction = trelloCard.actions.find((action) => { - return action.type === 'createCard'; - }); - if(creationAction) { - cardToCreate.createdAt = creationAction.date; - } - - // 5. map labels - create missing ones - trelloCard.labels.forEach((currentLabel) => { - const color = currentLabel.color; - const name = currentLabel.name; - const existingLabel = list.board().getLabel(name, color); - let labelId = undefined; - if (existingLabel) { - labelId = existingLabel._id; - } else { - let labelCreated = list.board().addLabel(name, color); - // XXX currently mutations return no value so we have to fetch the label we just created - // waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... - labelCreated = list.board().getLabel(name, color); - labelId = labelCreated._id; - } - if(labelId) { - if (!cardToCreate.labelIds) { - cardToCreate.labelIds = []; - } - cardToCreate.labelIds.push(labelId); - } - }); - - // 6. insert new card into list - const cardId = Cards.direct.insert(cardToCreate); - Activities.direct.insert({ - activityType: 'importCard', - boardId: cardToCreate.boardId, - cardId, - createdAt: dateOfImport, - listId: cardToCreate.listId, - source: { - id: trelloCard.id, - system: 'Trello', - url: trelloCard.url, - }, - // we attribute the import to current user, not the one from the original card - userId: Meteor.userId(), - }); - - // 7. parse actions and add comments - trelloCard.actions.forEach((currentAction) => { - if(currentAction.type === 'commentCard') { - const commentToCreate = { - boardId: list.boardId, - cardId, - createdAt: currentAction.date, - text: currentAction.data.text, - // XXX use the original comment user instead - userId: Meteor.userId(), - }; - const commentId = CardComments.direct.insert(commentToCreate); - Activities.direct.insert({ - activityType: 'addComment', - boardId: commentToCreate.boardId, - cardId: commentToCreate.cardId, - commentId, - createdAt: commentToCreate.createdAt, - userId: commentToCreate.userId, - }); - } - }); - return cardId; + // 3. create all elements + trelloCreator.lists[trelloCard.idList] = data.listId; + trelloCreator.parseActions(trelloCard.actions); + const board = list.board(); + trelloCreator.createLabels(trelloCard.labels, board); + const cardIds = trelloCreator.createCardsAndComments([trelloCard], board._id); + return cardIds[0]; }, }); -- cgit v1.2.3-1-g7c22 From f3fc154eb0cf25c46871510a0d4004037188ec15 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 20 Oct 2015 19:06:20 +0200 Subject: Re-implement label deletion This was not ported during v0.9 re-factor. Fixes #322 --- client/components/cards/labels.jade | 2 +- models/boards.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/cards/labels.jade b/client/components/cards/labels.jade index a868627c..31bd4d06 100644 --- a/client/components/cards/labels.jade +++ b/client/components/cards/labels.jade @@ -18,7 +18,7 @@ template(name="editLabelPopup") form.edit-label +formLabel button.primary.wide.left(type="submit") {{_ 'save'}} - span.right + button.js-delete-label.negate.wide.right {{_ 'delete'}} template(name="deleteLabelPopup") p {{_ "label-delete-pop"}} diff --git a/models/boards.js b/models/boards.js index d7d40251..8a9d21e8 100644 --- a/models/boards.js +++ b/models/boards.js @@ -319,7 +319,7 @@ if (Meteor.isServer) { { boardId: doc._id }, { $pull: { - labels: removedLabelId, + labelIds: removedLabelId, }, }, { multi: true } -- cgit v1.2.3-1-g7c22 From 118b434a5aad35df8eefea85624ab9abafab56f0 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 20 Oct 2015 20:02:12 +0200 Subject: Provide a default date for lists and cards creation date See https://github.com/wekan/wekan/pull/362#issuecomment-149645497 for motivation. This commit also contains cosmetic changes to the import Popup and on the code style to be more consistent with the code base. --- client/components/boards/boardHeader.jade | 5 +-- client/components/import/import.jade | 5 ++- client/components/import/import.js | 28 ++++++++------- client/components/main/popup.styl | 6 ++-- models/import.js | 60 ++++++++++++++++++------------- 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index e460170b..cb86e9bb 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -107,8 +107,9 @@ template(name="createBoardPopup") | {{{_ 'board-private-info'}}} a.js-change-visibility {{_ 'change'}}. input.primary.wide(type="submit" value="{{_ 'create'}}") - | {{_ 'or'}} - a.js-import {{_ 'import-board'}} + span.quiet + | {{_ 'or'}} + a.js-import {{_ 'import-board'}} template(name="boardChangeTitlePopup") diff --git a/client/components/import/import.jade b/client/components/import/import.jade index 8059b65b..f63661af 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -2,7 +2,6 @@ template(name="importPopup") if error.get .warning {{_ error.get}} form - label - | {{_ getLabel}} - textarea.js-card-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) + p: label(for='import-textarea') {{_ getLabel}} + textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus) input.primary.wide(type="submit" value="{{_ 'import'}}") diff --git a/client/components/import/import.js b/client/components/import/import.js index 00918aac..c6957fa9 100644 --- a/client/components/import/import.js +++ b/client/components/import/import.js @@ -1,20 +1,21 @@ -/** - * Abstract root for all import popup screens. - * Descendants must define: - * - getMethodName(): return the Meteor method to call for import, passing json data decoded as object - * and additional data (see below) - * - getAdditionalData(): return object containing additional data passed to Meteor method - * (like list ID and position for a card import) - * - getLabel(): i18n key for the text displayed in the popup, usually to explain how to get the data out of the - * source system. - */ +/// Abstract root for all import popup screens. +/// Descendants must define: +/// - getMethodName(): return the Meteor method to call for import, passing json +/// data decoded as object and additional data (see below); +/// - getAdditionalData(): return object containing additional data passed to +/// Meteor method (like list ID and position for a card import); +/// - getLabel(): i18n key for the text displayed in the popup, usually to +/// explain how to get the data out of the source system. const ImportPopup = BlazeComponent.extendComponent({ - template() {return 'importPopup';}, + template() { + return 'importPopup'; + }, + events() { return [{ 'submit': (evt) => { evt.preventDefault(); - const dataJson = $(evt.currentTarget).find('textarea').val(); + const dataJson = $(evt.currentTarget).find('.js-import-json').val(); let dataObject; try { dataObject = JSON.parse(dataJson); @@ -52,7 +53,8 @@ const ImportPopup = BlazeComponent.extendComponent({ ImportPopup.extendComponent({ getAdditionalData() { const listId = this.data()._id; - const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0); + const selector = `#js-list-${this.currentData()._id} .js-minicard:first`; + const firstCardDom = $(selector).get(0); const sortIndex = Utils.calculateIndex(null, firstCardDom).base; const result = {listId, sortIndex}; return result; diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl index 3bef4f7d..8a685069 100644 --- a/client/components/main/popup.styl +++ b/client/components/main/popup.styl @@ -17,9 +17,11 @@ $popupWidth = 300px margin: 4px -10px width: $popupWidth + p, + textarea, input[type="text"], input[type="email"], - input[type="password"] + input[type="password"], input[type="file"] margin: 4px 0 12px width: 100% @@ -30,8 +32,6 @@ $popupWidth = 300px textarea height: 72px - margin: 4px 0 12px - width: 100% .header height: 36px diff --git a/models/import.js b/models/import.js index be699746..7b441df6 100644 --- a/models/import.js +++ b/models/import.js @@ -5,17 +5,18 @@ const DateString = Match.Where(function (dateAsString) { class TrelloCreator { constructor() { - // the object creation dates, indexed by Trello id (so we only parse actions once!) + // The object creation dates, indexed by Trello id (so we only parse actions + // once!) this.createdAt = { board: null, cards: {}, lists: {}, }; - // map of labels Trello ID => Wekan ID + // Map of labels Trello ID => Wekan ID this.labels = {}; - // map of lists Trello ID => Wekan ID + // Map of lists Trello ID => Wekan ID this.lists = {}; - // the comments, indexed by Trello card id (to map when importing cards) + // The comments, indexed by Trello card id (to map when importing cards) this.comments = {}; } @@ -33,9 +34,12 @@ class TrelloCreator { closed: Boolean, name: String, prefs: Match.ObjectIncluding({ - // XXX refine control by validating 'background' against a list of allowed values (is it worth the maintenance?) + // XXX refine control by validating 'background' against a list of + // allowed values (is it worth the maintenance?) background: String, - permissionLevel: Match.Where((value) => {return ['org', 'private', 'public'].indexOf(value)>= 0;}), + permissionLevel: Match.Where((value) => { + return ['org', 'private', 'public'].indexOf(value)>= 0; + }), }), })); } @@ -54,7 +58,8 @@ class TrelloCreator { checkLabels(trelloLabels) { check(trelloLabels, [Match.ObjectIncluding({ - // XXX refine control by validating 'color' against a list of allowed values (is it worth the maintenance?) + // XXX refine control by validating 'color' against a list of allowed + // values (is it worth the maintenance?) color: String, name: String, })]); @@ -67,9 +72,7 @@ class TrelloCreator { })]); } - /** - * must call parseActions before calling this one - */ + // You must call parseActions before calling this one. createBoardAndLabels(trelloBoard) { const createdAt = this.createdAt.board; const boardToCreate = { @@ -93,7 +96,8 @@ class TrelloCreator { color: label.color, name: label.name, }; - // we need to remember them by Trello ID, as this is the only ref we have when importing cards + // We need to remember them by Trello ID, as this is the only ref we have + // when importing cards. this.labels[label.id] = labelToCreate._id; boardToCreate.labels.push(labelToCreate); }); @@ -110,15 +114,14 @@ class TrelloCreator { system: 'Trello', url: trelloBoard.url, }, - // we attribute the import to current user, not the one from the original object + // We attribute the import to current user, not the one from the original + // object. userId: Meteor.userId(), }); return boardId; } - /** - * Create labels if they do not exist and load this.labels. - */ + // Create labels if they do not exist and load this.labels. createLabels(trelloLabels, board) { trelloLabels.forEach((label) => { const color = label.color; @@ -138,7 +141,11 @@ class TrelloCreator { const listToCreate = { archived: list.closed, boardId, - createdAt: this.createdAt.lists[list.id], + // We are being defensing here by providing a default date (now) if the + // creation date wasn't found on the action log. This happen on old + // Trello boards (eg from 2013) that didn't log the 'createList' action + // we require. + createdAt: new Date(this.createdAt.lists[list.id] || Date.now()), title: list.name, userId: Meteor.userId(), }; @@ -156,7 +163,8 @@ class TrelloCreator { id: list.id, system: 'Trello', }, - // we attribute the import to current user, not the one from the original object + // We attribute the import to current user, not the one from the + // original object userId: Meteor.userId(), }); }); @@ -168,7 +176,7 @@ class TrelloCreator { const cardToCreate = { archived: card.closed, boardId, - createdAt: this.createdAt.cards[card.id], + createdAt: new Date(this.createdAt.cards[card.id] || Date.now()), dateLastActivity: new Date(), description: card.desc, listId: this.lists[card.idList], @@ -197,7 +205,8 @@ class TrelloCreator { system: 'Trello', url: card.url, }, - // we attribute the import to current user, not the one from the original card + // we attribute the import to current user, not the one from the + // original card userId: Meteor.userId(), }); // add comments @@ -212,7 +221,8 @@ class TrelloCreator { // XXX use the original comment user instead userId: Meteor.userId(), }; - // dateLastActivity will be set from activity insert, no need to update it ourselves + // dateLastActivity will be set from activity insert, no need to + // update it ourselves const commentId = CardComments.direct.insert(commentToCreate); Activities.direct.insert({ activityType: 'addComment', @@ -251,7 +261,8 @@ class TrelloCreator { if(trelloPermissionCode === 'public') { return 'public'; } - // Wekan does NOT have organization level, so we default both 'private' and 'org' to private. + // Wekan does NOT have organization level, so we default both 'private' and + // 'org' to private. return 'private'; } @@ -302,8 +313,8 @@ Meteor.methods({ throw new Meteor.Error('error-json-schema'); } - // 2. check parameters are ok from a business point of view (exist & authorized) - // nothing to check, everyone can import boards in their account + // 2. check parameters are ok from a business point of view (exist & + // authorized) nothing to check, everyone can import boards in their account // 3. create all elements trelloCreator.parseActions(trelloBoard.actions); @@ -330,7 +341,8 @@ Meteor.methods({ throw new Meteor.Error('error-json-schema'); } - // 2. check parameters are ok from a business point of view (exist & authorized) + // 2. check parameters are ok from a business point of view (exist & + // authorized) const list = Lists.findOne(data.listId); if(!list) { throw new Meteor.Error('error-list-doesNotExist'); -- cgit v1.2.3-1-g7c22 From 9154b06fc39f5cd046e1ed17a822e67a593b46c7 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 21 Oct 2015 04:26:16 +0200 Subject: Upgrade kenton:accounts-sandstorm package to 0.1.6 We now delegates more user attributes sync (avatar and permissions) to this package instead of doing it ourselves. --- .meteor/versions | 2 +- client/components/users/userAvatar.jade | 4 ++-- models/users.js | 13 +++++++++++++ sandstorm.js | 28 +++++++++------------------- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/.meteor/versions b/.meteor/versions index 586c3297..023e10aa 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -62,7 +62,7 @@ idmontie:migrations@1.0.0 jquery@1.11.4 kadira:blaze-layout@2.1.0 kadira:flow-router@2.7.0 -kenton:accounts-sandstorm@0.1.4 +kenton:accounts-sandstorm@0.1.6 launch-screen@1.0.4 less@2.5.0_3 livedata@1.0.15 diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade index e08666e5..44e899a7 100644 --- a/client/components/users/userAvatar.jade +++ b/client/components/users/userAvatar.jade @@ -1,7 +1,7 @@ template(name="userAvatar") a.member.js-member(title="{{userData.profile.fullname}} ({{userData.username}})") - if userData.profile.avatarUrl - img.avatar.avatar-image(src=userData.profile.avatarUrl) + if userData.getAvatarUrl + img.avatar.avatar-image(src=userData.getAvatarUrl) else +userAvatarInitials(userId=userData._id) diff --git a/models/users.js b/models/users.js index 4260dc56..1ea09a03 100644 --- a/models/users.js +++ b/models/users.js @@ -35,6 +35,19 @@ Users.helpers({ _.where(board.members, {userId: this._id})[0].isAdmin; }, + getAvatarUrl() { + // Although we put the avatar picture URL in the `profile` object, we need + // to support Sandstorm which put in the `picture` attribute by default. + // XXX Should we move both cases to `picture`? + if (this.picture) { + return this.picture; + } else if (this.profile && this.profile.avatarUrl) { + return this.profile.avatarUrl; + } else { + return null; + } + }, + getInitials() { const profile = this.profile || {}; if (profile.initials) diff --git a/sandstorm.js b/sandstorm.js index b0039d63..b3006454 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -21,16 +21,9 @@ if (isSandstorm && Meteor.isServer) { permission: 'public', }; - // This function should probably be handled by `accounts-sandstorm` but - // apparently meteor-core misses an API to handle that cleanly, cf. - // https://github.com/meteor/meteor/blob/ff783e9a12ffa04af6fd163843a563c9f4bbe8c1/packages/accounts-base/accounts_server.js#L1143 - function updateUserAvatar(userId, avatarUrl) { - Users.findOne(userId).setAvatarUrl(avatarUrl); - } - function updateUserPermissions(userId, permissions) { - const isActive = permissions.indexOf('participate') > -1; - const isAdmin = permissions.indexOf('configure') > -1; + const isActive = permissions.includes('participate'); + const isAdmin = permissions.includes('configure'); const permissionDoc = { userId, isActive, isAdmin }; const boardMembers = Boards.findOne(sandstormBoard._id).members; @@ -55,7 +48,8 @@ if (isSandstorm && Meteor.isServer) { // and the home page was accessible by pressing the back button of the // browser, a server-side redirection solves both of these issues. // - // XXX Maybe sandstorm manifest could provide some kind of "home URL"? + // XXX Maybe the sandstorm http-bridge could provide some kind of "home URL" + // in the manifest? const base = req.headers['x-sandstorm-base-path']; // 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 @@ -68,20 +62,14 @@ if (isSandstorm && Meteor.isServer) { res.end(); // `accounts-sandstorm` populate the Users collection when new users - // accesses the document, but in case a already known user come back, we + // accesses the document, but in case a already known user comes back, we // need to update his associated document to match the request HTTP headers // informations. const user = Users.findOne({ 'services.sandstorm.id': req.headers['x-sandstorm-user-id'], }); if (user) { - const userId = user._id; - const avatarUrl = req.headers['x-sandstorm-user-picture']; - const permissions = req.headers['x-sandstorm-permissions'].split(',') || []; - - // XXX The user may also change his name, we should handle it. - updateUserAvatar(userId, avatarUrl); - updateUserPermissions(userId, permissions); + updateUserPermissions(user._id, user.permissions); } }); @@ -90,6 +78,8 @@ if (isSandstorm && Meteor.isServer) { // unique board document. Note that when the `Users.after.insert` hook is // called, the user is inserted into the database but not connected. So // despite the appearances `userId` is null in this block. + // + // XXX We should support the `preferredHandle` exposed by Sandstorm Users.after.insert((userId, doc) => { if (!Boards.findOne(sandstormBoard._id)) { Boards.insert(sandstormBoard, {validate: false}); @@ -104,7 +94,7 @@ if (isSandstorm && Meteor.isServer) { // LibreBoard v0.8 didn’t implement the Sandstorm sharing model and instead // kept the visibility setting (“public” or “private”) in the UI as does the - // main Meteor application. We need to enforce “public” visibility has the + // main Meteor application. We need to enforce “public” visibility as the // sharing is now handled by Sandstorm. // See https://github.com/wekan/wekan/issues/346 Migrations.add('enforce-public-visibility-for-sandstorm', () => { -- cgit v1.2.3-1-g7c22 From c6b12dc5ada1b37d759796fefe0dbc5b327f130c Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Wed, 21 Oct 2015 04:34:44 +0200 Subject: Upgrade peerlibrary:blaze-components to v0.14 This change includes method renames and others UI related packages updates. --- .meteor/versions | 23 ++++++++++++----------- client/components/activities/activities.js | 2 +- client/components/boards/boardBody.js | 4 ++-- client/components/cards/cardDetails.js | 14 +++++++------- client/components/lists/list.js | 4 ++-- client/components/lists/listBody.js | 2 +- client/components/lists/listHeader.js | 2 +- client/components/sidebar/sidebar.js | 2 +- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/.meteor/versions b/.meteor/versions index 023e10aa..9a2087f1 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -60,16 +60,15 @@ http@1.1.1 id-map@1.0.4 idmontie:migrations@1.0.0 jquery@1.11.4 -kadira:blaze-layout@2.1.0 +kadira:blaze-layout@2.2.0 kadira:flow-router@2.7.0 kenton:accounts-sandstorm@0.1.6 launch-screen@1.0.4 -less@2.5.0_3 livedata@1.0.15 localstorage@1.0.5 logging@1.0.8 -matb33:collection-hooks@0.8.0 -matteodem:easy-search@1.6.3 +matb33:collection-hooks@0.8.1 +matteodem:easy-search@1.6.4 meteor@1.1.9 meteor-base@1.0.1 meteor-platform@1.2.3 @@ -101,12 +100,14 @@ observe-sequence@1.0.7 ongoworks:speakingurl@1.1.0 ordered-dict@1.0.4 peerlibrary:assert@0.2.5 -peerlibrary:base-component@0.10.0 -peerlibrary:blaze-components@0.13.0 +peerlibrary:base-component@0.13.0 +peerlibrary:blaze-components@0.14.0 +peerlibrary:computed-field@0.3.0 +peerlibrary:reactive-field@0.1.0 perak:markdown@1.0.5 promise@0.5.0 raix:eventemitter@0.1.3 -raix:handlebar-helpers@0.2.4 +raix:handlebar-helpers@0.2.5 random@1.0.4 rate-limit@1.0.0 reactive-dict@1.1.2 @@ -124,7 +125,7 @@ spacebars@1.0.7 spacebars-compiler@1.0.7 srp@1.0.4 standard-minifiers@1.0.1 -tap:i18n@1.6.1 +tap:i18n@1.7.0 templates:tabs@2.2.0 templating@1.1.4 templating-tools@1.0.0 @@ -132,9 +133,9 @@ tracker@1.0.9 ui@1.0.8 underscore@1.0.4 url@1.0.5 -useraccounts:core@1.12.3 -useraccounts:flow-routing@1.12.3 -useraccounts:unstyled@1.12.3 +useraccounts:core@1.12.4 +useraccounts:flow-routing@1.12.4 +useraccounts:unstyled@1.12.4 verron:autosize@3.0.8 webapp@1.2.2 webapp-hashing@1.0.5 diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index b25c0ca8..a491c2e8 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -9,7 +9,7 @@ BlazeComponent.extendComponent({ // XXX Should we use ReactiveNumber? this.page = new ReactiveVar(1); this.loadNextPageLocked = false; - const sidebar = this.componentParent(); // XXX for some reason not working + const sidebar = this.parentComponent(); // XXX for some reason not working sidebar.callFirstWith(null, 'resetNextPeak'); this.autorun(() => { const mode = this.data().mode; diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 95590beb..517e53ba 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -34,7 +34,7 @@ BlazeComponent.extendComponent({ }, openNewListForm() { - this.componentChildren('addListForm')[0].open(); + this.childrenComponents('addListForm')[0].open(); }, // XXX Flow components allow us to avoid creating these two setter methods by @@ -179,7 +179,7 @@ BlazeComponent.extendComponent({ // Proxy open() { - this.componentChildren('inlinedForm')[0].open(); + this.childrenComponents('inlinedForm')[0].open(); }, events() { diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 58ce28e4..5e07abab 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -13,19 +13,19 @@ BlazeComponent.extendComponent({ }, reachNextPeak() { - const activitiesComponent = this.componentChildren('activities')[0]; + const activitiesComponent = this.childrenComponents('activities')[0]; activitiesComponent.loadNextPage(); }, onCreated() { this.isLoaded = new ReactiveVar(false); - this.componentParent().showOverlay.set(true); - this.componentParent().mouseHasEnterCardDetails = false; + this.parentComponent().showOverlay.set(true); + this.parentComponent().mouseHasEnterCardDetails = false; }, scrollParentContainer() { const cardPanelWidth = 510; - const bodyBoardComponent = this.componentParent(); + const bodyBoardComponent = this.parentComponent(); const $cardContainer = bodyBoardComponent.$('.js-lists'); const $cardView = this.$(this.firstNode()); @@ -52,7 +52,7 @@ BlazeComponent.extendComponent({ }, onDestroyed() { - this.componentParent().showOverlay.set(false); + this.parentComponent().showOverlay.set(false); }, events() { @@ -83,8 +83,8 @@ BlazeComponent.extendComponent({ 'click .js-add-members': Popup.open('cardMembers'), 'click .js-add-labels': Popup.open('cardLabels'), 'mouseenter .js-card-details'() { - this.componentParent().showOverlay.set(true); - this.componentParent().mouseHasEnterCardDetails = true; + this.parentComponent().showOverlay.set(true); + this.parentComponent().mouseHasEnterCardDetails = true; }, })]; }, diff --git a/client/components/lists/list.js b/client/components/lists/list.js index af9bef98..75e816b5 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -7,7 +7,7 @@ BlazeComponent.extendComponent({ // Proxy openForm(options) { - this.componentChildren('listBody')[0].openForm(options); + this.childrenComponents('listBody')[0].openForm(options); }, onCreated() { @@ -25,7 +25,7 @@ BlazeComponent.extendComponent({ if (!Meteor.user() || !Meteor.user().isBoardMember()) return; - const boardComponent = this.componentParent(); + const boardComponent = this.parentComponent(); const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)'; const $cards = this.$('.js-minicards'); $cards.sortable({ diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 0b69b50a..88b31788 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -11,7 +11,7 @@ BlazeComponent.extendComponent({ options = options || {}; options.position = options.position || 'top'; - const forms = this.componentChildren('inlinedForm'); + const forms = this.childrenComponents('inlinedForm'); let form = _.find(forms, (component) => { return component.data().position === options.position; }); diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 4f5fc3a0..b5df2c81 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -5,7 +5,7 @@ BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); - const newTitle = this.componentChildren('inlinedForm')[0].getValue(); + const newTitle = this.childrenComponents('inlinedForm')[0].getValue(); const list = this.currentData(); if ($.trim(newTitle)) { list.rename(newTitle); diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index ff65ad9d..72866055 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -54,7 +54,7 @@ BlazeComponent.extendComponent({ }, reachNextPeak() { - const activitiesComponent = this.componentChildren('activities')[0]; + const activitiesComponent = this.childrenComponents('activities')[0]; activitiesComponent.loadNextPage(); }, -- cgit v1.2.3-1-g7c22 From aa974aa54ab6e5b7db7450206d12b44ffb3a0306 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 22 Oct 2015 04:02:12 +0200 Subject: Prefer ES5 methods over underscore utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since 07cc454 (ie the switch to Meteor 1.2) we includes the `es5-shim` polyfill to support methods like `Array.prototype.forEach` in a consistent way across all supported browsers (IE8+). MDG recently released a blog post recommending the use of these native methods instead of underscore [0]. We know follow this recommendation. This commit also favor some ES6 features (argument defaults, destructing assignment) in places where we didn’t use them. [0]: http://info.meteor.com/blog/es2015-get-started --- client/components/cards/cardDetails.js | 5 +++-- client/components/cards/labels.js | 2 +- client/components/main/editor.js | 2 +- client/components/sidebar/sidebar.js | 4 ++-- client/components/users/userHeader.js | 2 +- client/config/accounts.js | 2 +- client/lib/filter.js | 4 ++-- client/lib/modal.js | 4 ++-- client/lib/multiSelection.js | 9 +++++---- client/lib/popup.js | 4 ++-- models/boards.js | 2 +- models/users.js | 16 ++++++++-------- server/migrations.js | 6 +++--- server/publications/boards.js | 2 +- 14 files changed, 33 insertions(+), 31 deletions(-) diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 5e07abab..2d2679ec 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -62,7 +62,8 @@ BlazeComponent.extendComponent({ }, }; - return [_.extend(events, { + return [{ + ...events, 'click .js-close-card-details'() { Utils.goBoardId(this.data().boardId); }, @@ -86,7 +87,7 @@ BlazeComponent.extendComponent({ this.parentComponent().showOverlay.set(true); this.parentComponent().mouseHasEnterCardDetails = true; }, - })]; + }]; }, }).register('cardDetails'); diff --git a/client/components/cards/labels.js b/client/components/cards/labels.js index 6a411561..4e61a0c6 100644 --- a/client/components/cards/labels.js +++ b/client/components/cards/labels.js @@ -13,7 +13,7 @@ BlazeComponent.extendComponent({ }, labels() { - return _.map(labelColors, (color) => { + return labelColors.map((color) => { return { color, name: '' }; }); }, diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 67b65bec..168c85d0 100644 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -54,7 +54,7 @@ const at = HTML.CharRef({html: '@', str: '@'}); Blaze.Template.registerHelper('mentions', new Template('mentions', function() { const view = this; const currentBoard = Boards.findOne(Session.get('currentBoard')); - const knowedUsers = _.map(currentBoard.members, (member) => { + const knowedUsers = currentBoard.members.map((member) => { member.username = Users.findOne(member.userId).username; return member; }); diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 72866055..ccb9f2f5 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -95,10 +95,10 @@ BlazeComponent.extendComponent({ events() { // XXX Hacky, we need some kind of `super` const mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events(); - return mixinEvents.concat([{ + return [...mixinEvents, { 'click .js-toggle-sidebar': this.toggle, 'click .js-back-home': this.setView, - }]); + }]; }, }).register('sidebar'); diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index 0f91fd15..a7382769 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -41,7 +41,7 @@ Template.changePasswordPopup.onRendered(function() { Template.changeLanguagePopup.helpers({ languages() { - return _.map(TAPi18n.getLanguages(), (lang, tag) => { + return TAPi18n.getLanguages().map((lang, tag) => { const name = lang.name; return { tag, name }; }); diff --git a/client/config/accounts.js b/client/config/accounts.js index df0935f7..d475e6b2 100644 --- a/client/config/accounts.js +++ b/client/config/accounts.js @@ -25,7 +25,7 @@ AccountsTemplates.configure({ }, }); -_.each(['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'], +['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'].forEach( (routeName) => AccountsTemplates.configureRoute(routeName)); // We display the form to change the password in a popup window that already diff --git a/client/lib/filter.js b/client/lib/filter.js index f7baf480..74305284 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -95,7 +95,7 @@ Filter = { return {}; const filterSelector = {}; - _.forEach(this._fields, (fieldName) => { + this._fields.forEach((fieldName) => { const filter = this[fieldName]; if (filter._isActive()) filterSelector[fieldName] = filter._getMongoSelector(); @@ -116,7 +116,7 @@ Filter = { }, reset() { - _.forEach(this._fields, (fieldName) => { + this._fields.forEach((fieldName) => { const filter = this[fieldName]; filter.reset(); }); diff --git a/client/lib/modal.js b/client/lib/modal.js index 5b3392b2..7b7516e0 100644 --- a/client/lib/modal.js +++ b/client/lib/modal.js @@ -21,9 +21,9 @@ window.Modal = new class { } } - open(modalName, options) { + open(modalName, { onCloseGoTo = ''}) { this._currentModal.set(modalName); - this._onCloseGoTo = options && options.onCloseGoTo || ''; + this._onCloseGoTo = onCloseGoTo; } }; diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js index c2bb2bbc..eeb2015d 100644 --- a/client/lib/multiSelection.js +++ b/client/lib/multiSelection.js @@ -119,12 +119,13 @@ MultiSelection = { } }, - toggle(cardIds, options) { + toggle(cardIds, options = {}) { cardIds = _.isString(cardIds) ? [cardIds] : cardIds; - options = _.extend({ + options = { add: true, remove: true, - }, options || {}); + ...options, + }; if (!this.isActive()) { this.reset(); @@ -133,7 +134,7 @@ MultiSelection = { const selectedCards = this._selectedCards.get(); - _.each(cardIds, (cardId) => { + cardIds.forEach((cardId) => { const indexOfCard = selectedCards.indexOf(cardId); if (options.remove && indexOfCard > -1) diff --git a/client/lib/popup.js b/client/lib/popup.js index 3c39af29..7418d938 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -91,7 +91,7 @@ window.Popup = new class { if (!self.isOpen()) { self.current = Blaze.renderWithData(self.template, () => { self._dep.depend(); - return _.extend(self._getTopStack(), { stack: self._stack }); + return { ...self._getTopStack(), stack: self._stack }; }, document.body); } else { @@ -191,7 +191,7 @@ window.Popup = new class { // We close a potential opened popup on any left click on the document, or go // one step back by pressing escape. const escapeActions = ['back', 'close']; -_.each(escapeActions, (actionName) => { +escapeActions.forEach((actionName) => { EscapeActions.register(`popup-${actionName}`, () => Popup[actionName](), () => Popup.isOpen(), diff --git a/models/boards.js b/models/boards.js index 4d9fd7c0..1d365a95 100644 --- a/models/boards.js +++ b/models/boards.js @@ -279,7 +279,7 @@ Boards.before.insert((userId, doc) => { // Handle labels const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; const defaultLabelsColors = _.clone(colors).splice(0, 6); - doc.labels = _.map(defaultLabelsColors, (color) => { + doc.labels = defaultLabelsColors.map((color) => { return { color, _id: Random.id(6), diff --git a/models/users.js b/models/users.js index 1ea09a03..b35104ec 100644 --- a/models/users.js +++ b/models/users.js @@ -14,13 +14,13 @@ Users.helpers({ }, starredBoards() { - const starredBoardIds = this.profile.starredBoards || []; - return Boards.find({archived: false, _id: {$in: starredBoardIds}}); + const {starredBoards = []} = this.profile; + return Boards.find({archived: false, _id: {$in: starredBoards}}); }, hasStarred(boardId) { - const starredBoardIds = this.profile.starredBoards || []; - return _.contains(starredBoardIds, boardId); + const {starredBoards = []} = this.profile; + return _.contains(starredBoards, boardId); }, isBoardMember() { @@ -54,9 +54,9 @@ Users.helpers({ return profile.initials; else if (profile.fullname) { - return _.reduce(profile.fullname.split(/\s+/), (memo, word) => { + return profile.fullname.split(/\s+/).reduce((memo = '', word) => { return memo + word[0]; - }, '').toUpperCase(); + }).toUpperCase(); } else { return this.username[0].toUpperCase(); @@ -130,7 +130,7 @@ if (Meteor.isServer) { // b. We use it to find deleted and newly inserted ids by using it in one // direction and then in the other. function incrementBoards(boardsIds, inc) { - _.forEach(boardsIds, (boardId) => { + boardsIds.forEach((boardId) => { Boards.update(boardId, {$inc: {stars: inc}}); }); } @@ -149,7 +149,7 @@ if (Meteor.isServer) { // Insert the Welcome Board Boards.insert(ExampleBoard, (err, boardId) => { - _.forEach(['Basics', 'Advanced'], (title) => { + ['Basics', 'Advanced'].forEach((title) => { const list = { title, boardId, diff --git a/server/migrations.js b/server/migrations.js index 509d8510..99125976 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -43,7 +43,7 @@ Migrations.add('board-background-color', () => { }); Migrations.add('lowercase-board-permission', () => { - _.forEach(['Public', 'Private'], (permission) => { + ['Public', 'Private'].forEach((permission) => { Boards.update( { permission }, { $set: { permission: permission.toLowerCase() } }, @@ -116,11 +116,11 @@ Migrations.add('add-member-isactive-field', () => { const formerUsers = _.difference(allUsersWithSomeActivity, currentUsers); const newMemberSet = []; - _.forEach(board.members, (member) => { + board.members.forEach((member) => { member.isActive = true; newMemberSet.push(member); }); - _.forEach(formerUsers, (userId) => { + formerUsers.forEach((userId) => { newMemberSet.push({ userId, isAdmin: false, diff --git a/server/publications/boards.js b/server/publications/boards.js index 403d0084..4ab7a12e 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -10,7 +10,7 @@ Meteor.publish('boards', function() { // Defensive programming to verify that starredBoards has the expected // format -- since the field is in the `profile` a user can modify it. - const starredBoards = Users.findOne(this.userId).profile.starredBoards || []; + const {starredBoards = []} = Users.findOne(this.userId).profile; check(starredBoards, [String]); return Boards.find({ -- cgit v1.2.3-1-g7c22 From b9d20e04f2f6307c0e708e56d05e25ebc942d2df Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 22 Oct 2015 18:08:39 +0200 Subject: Display the board name in the page title Fixes #364 --- .eslintrc | 1 + .meteor/packages | 1 + .meteor/versions | 2 ++ client/config/router.js | 23 +++++++++++++++++++++++ 4 files changed, 27 insertions(+) diff --git a/.eslintrc b/.eslintrc index 081b2a56..c952e5e0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -78,6 +78,7 @@ globals: Avatars: true BlazeComponent: false BlazeLayout: false + DocHead: false ESSearchResults: false FlowRouter: false FS: false diff --git a/.meteor/packages b/.meteor/packages index 765932d4..39242f48 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -50,6 +50,7 @@ alethes:pages arillo:flow-router-helpers audit-argument-checks kadira:blaze-layout +kadira:dochead kadira:flow-router meteorhacks:picker meteorhacks:subs-manager diff --git a/.meteor/versions b/.meteor/versions index 9a2087f1..840f09f9 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -60,7 +60,9 @@ http@1.1.1 id-map@1.0.4 idmontie:migrations@1.0.0 jquery@1.11.4 +jsx@0.1.6 kadira:blaze-layout@2.2.0 +kadira:dochead@1.1.0 kadira:flow-router@2.7.0 kenton:accounts-sandstorm@0.1.6 launch-screen@1.0.4 diff --git a/client/config/router.js b/client/config/router.js index 1cac43a0..0a6958d0 100644 --- a/client/config/router.js +++ b/client/config/router.js @@ -88,3 +88,26 @@ _.each(redirections, (newPath, oldPath) => { }], }); }); + +// As it is not possible to use template helpers in the page we create a +// reactive function whose role is to set any page-specific tag in the +// using the `kadira:dochead` package. Currently we only use it to display the +// board title if we are in a board page (see #364) but we may want to support +// some tags in the future. +const appTitle = 'Wekan'; + +// XXX The `Meteor.startup` should not be necessary -- we don't need to wait for +// the complete DOM to be ready to call `DocHead.setTitle`. But the problem is +// that the global variable `Boards` is undefined when this file loads so we +// wait a bit until hopefully all files are loaded. This will be fixed in a +// clean way once Meteor will support ES6 modules -- hopefully in Meteor 1.3. +Meteor.startup(() => { + Tracker.autorun(() => { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + const titleStack = [appTitle]; + if (currentBoard) { + titleStack.push(currentBoard.title); + } + DocHead.setTitle(titleStack.reverse().join(' - ')); + }); +}); -- cgit v1.2.3-1-g7c22 From b3696e1e3b366770af8c41861b5cf996cdd2378a Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 22 Oct 2015 18:35:17 +0200 Subject: Fix a typo in the card activity Fixes #366 --- i18n/en.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 0823ba08..82fb4ccd 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -14,7 +14,7 @@ "activity-on": "on %s", "activity-removed": "removed %s from %s", "activity-sent": "sent %s to %s", - "activity-unjoined": "unjoinded %s", + "activity-unjoined": "unjoined %s", "add": "Add", "add-attachment": "Add an attachment", "add-board": "Add a new board", -- cgit v1.2.3-1-g7c22 From 31b60d82fcae64a844805a2a76a0af25fb9c16c2 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Fri, 23 Oct 2015 16:56:55 +0200 Subject: Upgrade Meteor to 1.2.1-rc4 This version includes a more complete selection of ES2015 polyfills that I started used across the code base, for instance by replacing `$.trim(str)` by `str.trim()`. --- .meteor/release | 2 +- .meteor/versions | 36 +++++++++++++++--------------- client/components/activities/activities.js | 4 ++-- client/components/activities/comments.js | 12 +++++----- client/components/boards/boardBody.js | 6 ++--- client/components/cards/cardDetails.js | 6 ++--- client/components/lists/listBody.js | 6 ++--- client/components/lists/listHeader.js | 6 ++--- client/components/main/editor.js | 8 +++---- client/components/users/userHeader.js | 6 ++--- models/boards.js | 4 ++-- sandstorm.js | 2 +- 12 files changed, 50 insertions(+), 48 deletions(-) diff --git a/.meteor/release b/.meteor/release index 5684262a..71252f55 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.2.0.2 +METEOR@1.2.1-rc.4 diff --git a/.meteor/versions b/.meteor/versions index 840f09f9..b661658f 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,12 +1,12 @@ 3stack:presence@1.0.3 -accounts-base@1.2.1 -accounts-password@1.1.3 +accounts-base@1.2.2-rc.0 +accounts-password@1.1.4-rc.0 aldeed:collection2@2.5.0 aldeed:simple-schema@1.3.3 alethes:pages@1.8.4 arillo:flow-router-helpers@0.4.5 audit-argument-checks@1.0.4 -autoupdate@1.2.3 +autoupdate@1.2.4-rc.0 babel-compiler@5.8.24_1 babel-runtime@0.1.4 base64@1.0.4 @@ -34,22 +34,22 @@ cfs:storage-adapter@0.2.3 cfs:tempstore@0.1.5 cfs:upload-http@0.0.20 cfs:worker@0.1.4 -check@1.0.6 -coffeescript@1.0.10 +check@1.1.0-rc.0 +coffeescript@1.0.11-rc.0 cosmos:browserify@0.5.1 dburles:collection-helpers@1.0.3 ddp@1.2.2 ddp-client@1.2.1 -ddp-common@1.2.1 +ddp-common@1.2.2-rc.0 ddp-rate-limiter@1.0.0 -ddp-server@1.2.1 +ddp-server@1.2.2-rc.0 deps@1.0.9 diff-sequence@1.0.1 -ecmascript@0.1.5 -ecmascript-collections@0.1.6 +ecmascript@0.1.6-rc.0 +ecmascript-runtime@0.2.6-rc.0 ejson@1.0.7 -email@1.0.7 -es5-shim@4.1.13 +email@1.0.8-rc.0 +es5-shim@4.1.14-rc.0 fastclick@1.0.7 fortawesome:fontawesome@4.4.0 geojson-utils@1.0.4 @@ -71,7 +71,7 @@ localstorage@1.0.5 logging@1.0.8 matb33:collection-hooks@0.8.1 matteodem:easy-search@1.6.4 -meteor@1.1.9 +meteor@1.1.10-rc.0 meteor-base@1.0.1 meteor-platform@1.2.3 meteorhacks:aggregate@1.3.0 @@ -82,7 +82,7 @@ meteorspark:util@0.2.0 minifiers@1.1.7 minimongo@1.0.10 mobile-status-bar@1.0.6 -mongo@1.1.2 +mongo@1.1.3-rc.0 mongo-id@1.0.1 mongo-livedata@1.0.9 mousetrap:mousetrap@1.4.6_1 @@ -107,10 +107,10 @@ peerlibrary:blaze-components@0.14.0 peerlibrary:computed-field@0.3.0 peerlibrary:reactive-field@0.1.0 perak:markdown@1.0.5 -promise@0.5.0 +promise@0.5.1-rc.0 raix:eventemitter@0.1.3 raix:handlebar-helpers@0.2.5 -random@1.0.4 +random@1.0.5-rc.0 rate-limit@1.0.0 reactive-dict@1.1.2 reactive-var@1.0.6 @@ -126,10 +126,10 @@ softwarerero:accounts-t9n@1.1.4 spacebars@1.0.7 spacebars-compiler@1.0.7 srp@1.0.4 -standard-minifiers@1.0.1 +standard-minifiers@1.0.2-rc.0 tap:i18n@1.7.0 templates:tabs@2.2.0 -templating@1.1.4 +templating@1.1.5-rc.0 templating-tools@1.0.0 tracker@1.0.9 ui@1.0.8 @@ -139,6 +139,6 @@ useraccounts:core@1.12.4 useraccounts:flow-routing@1.12.4 useraccounts:unstyled@1.12.4 verron:autosize@3.0.8 -webapp@1.2.2 +webapp@1.2.3-rc.0 webapp-hashing@1.0.5 zimme:active-route@2.3.2 diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js index a491c2e8..64e9865d 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -101,9 +101,9 @@ BlazeComponent.extendComponent({ }, 'submit .js-edit-comment'(evt) { evt.preventDefault(); - const commentText = this.currentComponent().getValue(); + const commentText = this.currentComponent().getValue().trim(); const commentId = Template.parentData().commentId; - if ($.trim(commentText)) { + if (commentText) { CardComments.update(commentId, { $set: { text: commentText, diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js index 08401caa..f9bbcff8 100644 --- a/client/components/activities/comments.js +++ b/client/components/activities/comments.js @@ -23,12 +23,13 @@ BlazeComponent.extendComponent({ commentFormIsOpen.set(true); }, 'submit .js-new-comment-form'(evt) { - const input = this.getInput(); - if ($.trim(input.val())) { + const input = this.getInput() + const text = input.val().trim(); + if (text) { CardComments.insert({ + text, boardId: this.currentData().boardId, cardId: this.currentData()._id, - text: input.val(), }); resetCommentInput(input); Tracker.flush(); @@ -72,8 +73,9 @@ EscapeActions.register('inlinedForm', docId: Session.get('currentCard'), }; const commentInput = $('.js-new-comment-input'); - if ($.trim(commentInput.val())) { - UnsavedEdits.set(draftKey, commentInput.val()); + const draft = commentInput.val().trim(); + if (draft) { + UnsavedEdits.set(draftKey, draft); } else { UnsavedEdits.reset(draftKey); } diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 517e53ba..e7cfc791 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -186,10 +186,10 @@ BlazeComponent.extendComponent({ return [{ submit(evt) { evt.preventDefault(); - const title = this.find('.list-name-input'); - if ($.trim(title.value)) { + const title = this.find('.list-name-input').value.trim(); + if (title) { Lists.insert({ - title: title.value, + title, boardId: Session.get('currentBoard'), sort: $('.list').length, }); diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 2d2679ec..fa818c5a 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -75,8 +75,8 @@ BlazeComponent.extendComponent({ }, 'submit .js-card-details-title'(evt) { evt.preventDefault(); - const title = this.currentComponent().getValue(); - if ($.trim(title)) { + const title = this.currentComponent().getValue().trim(); + if (title) { this.data().setTitle(title); } }, @@ -106,7 +106,7 @@ BlazeComponent.extendComponent({ close(isReset = false) { if (this.isOpen.get() && !isReset) { - const draft = $.trim(this.getValue()); + const draft = this.getValue().trim(); if (draft !== Cards.findOne(Session.get('currentCard')).description) { UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue()); } diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 88b31788..25aeffcc 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -12,7 +12,7 @@ BlazeComponent.extendComponent({ options.position = options.position || 'top'; const forms = this.childrenComponents('inlinedForm'); - let form = _.find(forms, (component) => { + let form = forms.find((component) => { return component.data().position === options.position; }); if (!form && forms.length > 0) { @@ -26,7 +26,7 @@ BlazeComponent.extendComponent({ const firstCardDom = this.find('.js-minicard:first'); const lastCardDom = this.find('.js-minicard:last'); const textarea = $(evt.currentTarget).find('textarea'); - const title = textarea.val(); + const title = textarea.val().trim(); const position = this.currentData().position; let sortIndex; if (position === 'top') { @@ -35,7 +35,7 @@ BlazeComponent.extendComponent({ sortIndex = Utils.calculateIndex(lastCardDom, null).base; } - if ($.trim(title)) { + if (title) { const _id = Cards.insert({ title, listId: this.data()._id, diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index b5df2c81..ab547f02 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -6,9 +6,9 @@ BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); const newTitle = this.childrenComponents('inlinedForm')[0].getValue(); - const list = this.currentData(); - if ($.trim(newTitle)) { - list.rename(newTitle); + const list = this.currentData().trim(); + if (newTitle) { + list.rename(newTitle.trim()); } }, diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 168c85d0..82fce641 100644 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -8,8 +8,8 @@ Template.editor.onRendered(() => { { match: /\B:([\-+\w]*)$/, search(term, callback) { - callback($.map(Emoji.values, (emoji) => { - return emoji.indexOf(term) === 0 ? emoji : null; + callback(Emoji.values.map((emoji) => { + return emoji.includes(term) ? emoji : null; })); }, template(value) { @@ -28,9 +28,9 @@ Template.editor.onRendered(() => { match: /\B@(\w*)$/, search(term, callback) { const currentBoard = Boards.findOne(Session.get('currentBoard')); - callback($.map(currentBoard.members, (member) => { + callback(currentBoard.members.map((member) => { const username = Users.findOne(member.userId).username; - return username.indexOf(term) === 0 ? username : null; + return username.includes(term) ? username : null; })); }, template(value) { diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index a7382769..7d35c1b9 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -18,9 +18,9 @@ Template.memberMenuPopup.events({ Template.editProfilePopup.events({ submit(evt, tpl) { evt.preventDefault(); - const fullname = $.trim(tpl.find('.js-profile-fullname').value); - const username = $.trim(tpl.find('.js-profile-username').value); - const initials = $.trim(tpl.find('.js-profile-initials').value); + const fullname = tpl.find('.js-profile-fullname').value.trim(); + const username = tpl.find('.js-profile-username').value.trim(); + const initials = tpl.find('.js-profile-initials').value.trim(); Users.update(Meteor.userId(), {$set: { 'profile.fullname': fullname, 'profile.initials': initials, diff --git a/models/boards.js b/models/boards.js index 1d365a95..98d6ec77 100644 --- a/models/boards.js +++ b/models/boards.js @@ -97,11 +97,11 @@ Boards.helpers({ }, labelIndex(labelId) { - return _.indexOf(_.pluck(this.labels, '_id'), labelId); + return _.pluck(this.labels, '_id').indexOf(labelId); }, memberIndex(memberId) { - return _.indexOf(_.pluck(this.members, 'userId'), memberId); + return _.pluck(this.members, 'userId').indexOf(memberId); }, absoluteUrl() { diff --git a/sandstorm.js b/sandstorm.js index b3006454..51f8ba34 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -27,7 +27,7 @@ if (isSandstorm && Meteor.isServer) { const permissionDoc = { userId, isActive, isAdmin }; const boardMembers = Boards.findOne(sandstormBoard._id).members; - const memberIndex = _.indexOf(_.pluck(boardMembers, 'userId'), userId); + const memberIndex = _.pluck(boardMembers, 'userId').indexOf(userId); let modifier; if (memberIndex > -1) -- cgit v1.2.3-1-g7c22 From 3ad672a20b8e216684bbcb932b792d67548c2484 Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Tue, 27 Oct 2015 17:38:04 +0100 Subject: Add missing semicolon --- client/components/activities/comments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js index f9bbcff8..18bf9ef0 100644 --- a/client/components/activities/comments.js +++ b/client/components/activities/comments.js @@ -23,7 +23,7 @@ BlazeComponent.extendComponent({ commentFormIsOpen.set(true); }, 'submit .js-new-comment-form'(evt) { - const input = this.getInput() + const input = this.getInput(); const text = input.val().trim(); if (text) { CardComments.insert({ -- cgit v1.2.3-1-g7c22 From 3956a6ec205b753c55a2d11f8f780acb996bb461 Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Tue, 27 Oct 2015 17:40:38 +0100 Subject: Add eslint-plugin-meteor Add rules for eslint-plugin-meteor. Use local version of eslint and eslint-plugin-meteor, instead of relying on global versions. Ensures consistent versions of eslint and eslint-plugin-meteor for all developers. --- .eslintrc | 73 ++++++++++++++++++++++++--------------------------- .gitignore | 1 + models/attachments.js | 2 +- models/users.js | 32 +++++++++++++--------- package.json | 24 +++++++++++++++++ sandstorm.js | 2 +- 6 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 package.json diff --git a/.eslintrc b/.eslintrc index c952e5e0..14a9e667 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,14 @@ ecmaFeatures: experimentalObjectRestSpread: true +plugins: + - meteor + +parser: babel-eslint + rules: + strict: 0 + no-undef: 2 accessor-pairs: 2 comma-dangle: [2, 'always-multiline'] consistent-return: 2 @@ -43,36 +50,35 @@ rules: prefer-spread: 2 prefer-template: 2 -globals: - # Meteor globals - Meteor: false - DDP: false - Mongo: false - Session: false - Accounts: false - Template: false - Blaze: false - UI: false - Match: false - check: false - Tracker: false - Deps: false - ReactiveVar: false - EJSON: false - HTTP: false - Email: false - Assets: false - Handlebars: false - Package: false - App: false - Npm: false - Tinytest: false - Random: false - HTML: false + # eslint-plugin-meteor + meteor/globals: 2 + meteor/no-zero-timeout: 2 + meteor/no-session: 0 + meteor/pubsub: 2 + meteor/core: 2 + meteor/methods: 2 + meteor/check: 2 + meteor/connections: 2 + meteor/collections: 2 + meteor/session: [2, 'no-equal'] + +settings: + meteor: + # Our collections + collections: + - AccountsTemplates + - Activities + - Attachments + - Boards + - CardComments + - Cards + - Lists + - UnsavedEditCollection + - Users + +globals: # Exported by packages we use - '$': false - _: false autosize: false Avatar: true Avatars: true @@ -97,17 +103,6 @@ globals: T9n: false TAPi18n: false - # Our collections - AccountsTemplates: true - Activities: true - Attachments: true - Boards: true - CardComments: true - Cards: true - Lists: true - UnsavedEditCollection: true - Users: true - # Our objects CSSEvents: true EscapeActions: true diff --git a/.gitignore b/.gitignore index 986a8b46..7c40fcd4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .tx/ *.sublime-workspace tmp/ +node_modules/ diff --git a/models/attachments.js b/models/attachments.js index 8ef0fef0..01e467ff 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,4 +1,4 @@ -Attachments = new FS.Collection('attachments', { +Attachments = new FS.Collection('attachments', { // eslint-disable-line meteor/collections stores: [ // XXX Add a new store for cover thumbnails so we don't load big images in diff --git a/models/users.js b/models/users.js index b35104ec..1e69564d 100644 --- a/models/users.js +++ b/models/users.js @@ -1,4 +1,4 @@ -Users = Meteor.users; +Users = Meteor.users; // eslint-disable-line meteor/collections // Search a user in the complete server database by its name or username. This // is used for instance to add a new user to a board. @@ -8,6 +8,24 @@ Users.initEasySearch(searchInFields, { returnFields: [...searchInFields, 'profile.avatarUrl'], }); +if (Meteor.isClient) { + Users.helpers({ + isBoardMember() { + const board = Boards.findOne(Session.get('currentBoard')); + return board && + _.contains(_.pluck(board.members, 'userId'), this._id) && + _.where(board.members, {userId: this._id})[0].isActive; + }, + + isBoardAdmin() { + const board = Boards.findOne(Session.get('currentBoard')); + return board && + this.isBoardMember(board) && + _.where(board.members, {userId: this._id})[0].isAdmin; + }, + }); +} + Users.helpers({ boards() { return Boards.find({ userId: this._id }); @@ -23,18 +41,6 @@ Users.helpers({ return _.contains(starredBoards, boardId); }, - isBoardMember() { - const board = Boards.findOne(Session.get('currentBoard')); - return board && _.contains(_.pluck(board.members, 'userId'), this._id) && - _.where(board.members, {userId: this._id})[0].isActive; - }, - - isBoardAdmin() { - const board = Boards.findOne(Session.get('currentBoard')); - return board && this.isBoardMember(board) && - _.where(board.members, {userId: this._id})[0].isAdmin; - }, - getAvatarUrl() { // Although we put the avatar picture URL in the `profile` object, we need // to support Sandstorm which put in the `picture` attribute by default. diff --git a/package.json b/package.json new file mode 100644 index 00000000..821f4619 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "wekan", + "version": "1.0.0", + "description": "The open-source Trello-like kanban", + "private": true, + "scripts": { + "lint": "eslint .", + "test": "npm run lint" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wekan/wekan.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/wekan/wekan/issues" + }, + "homepage": "https://github.com/wekan/wekan#readme", + "devDependencies": { + "babel-eslint": "4.1.3", + "eslint": "1.7.3", + "eslint-plugin-meteor": "1.5.0" + } +} diff --git a/sandstorm.js b/sandstorm.js index 51f8ba34..65f24866 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -109,7 +109,7 @@ if (isSandstorm && Meteor.isClient) { // sandstorm client to return relative paths instead of absolutes. const _absoluteUrl = Meteor.absoluteUrl; const _defaultOptions = Meteor.absoluteUrl.defaultOptions; - Meteor.absoluteUrl = (path, options) => { + Meteor.absoluteUrl = (path, options) => { // eslint-disable-line meteor/core const url = _absoluteUrl(path, options); return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, ''); }; -- cgit v1.2.3-1-g7c22 From 6e87d50941821dec7c8be5d77211fa5c5836733d Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Tue, 27 Oct 2015 18:01:44 +0100 Subject: Adapt CI --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 55f8ed48..a8724631 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: node_js node_js: - "0.10.40" install: - - "npm install -g eslint" - - "npm install -g eslint-plugin-meteor" + - "npm install" script: - - "eslint ./" + - "npm test" -- cgit v1.2.3-1-g7c22 From c9130324f6c337d0393bf37fe72b67b47ece8322 Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Tue, 27 Oct 2015 18:34:09 +0100 Subject: Enable more ESLint-plugin-Meteor rules --- .eslintrc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index 14a9e667..a8017e66 100644 --- a/.eslintrc +++ b/.eslintrc @@ -51,17 +51,21 @@ rules: prefer-template: 2 # eslint-plugin-meteor + ## Meteor API meteor/globals: 2 - meteor/no-zero-timeout: 2 - meteor/no-session: 0 - meteor/pubsub: 2 meteor/core: 2 + meteor/pubsub: 2 meteor/methods: 2 meteor/check: 2 meteor/connections: 2 meteor/collections: 2 meteor/session: [2, 'no-equal'] + ## Best practices + meteor/no-session: 0 + meteor/no-zero-timeout: 2 + meteor/no-blaze-lifecycle-assignment: 2 + settings: meteor: -- cgit v1.2.3-1-g7c22 From dd101543920148a48b74541ebbbaca97f6bc6d11 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 29 Oct 2015 01:17:23 +0100 Subject: Clean the linting script This also has the side effect of correcting the `npm test` Unix exit status number -- which was previously always 0. We also edit the website URL. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 821f4619..cf998e9c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "lint": "eslint .", - "test": "npm run lint" + "test": "npm run --silent lint" }, "repository": { "type": "git", @@ -15,7 +15,7 @@ "bugs": { "url": "https://github.com/wekan/wekan/issues" }, - "homepage": "https://github.com/wekan/wekan#readme", + "homepage": "http://wekan.io", "devDependencies": { "babel-eslint": "4.1.3", "eslint": "1.7.3", -- cgit v1.2.3-1-g7c22 From 68521fc1c3d52b6b7b6d39931618c90338b5b3aa Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 29 Oct 2015 15:31:33 +0100 Subject: Update packages and update Meteor to 1.2.1 Blaze-components had yet another methods rename. --- .meteor/packages | 3 -- .meteor/release | 2 +- .meteor/versions | 57 +++++++++++++++++------------------ client/components/boards/boardBody.js | 4 +-- 4 files changed, 31 insertions(+), 35 deletions(-) diff --git a/.meteor/packages b/.meteor/packages index 39242f48..5b666ed1 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -2,9 +2,6 @@ # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -# -# XXX Should we replace tmeasday:presence by 3stack:presence? Or maybe the -# packages will merge in the future? meteor-base diff --git a/.meteor/release b/.meteor/release index 71252f55..3a05e0a2 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.2.1-rc.4 +METEOR@1.2.1 diff --git a/.meteor/versions b/.meteor/versions index b661658f..bd2390c4 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,12 +1,12 @@ 3stack:presence@1.0.3 -accounts-base@1.2.2-rc.0 -accounts-password@1.1.4-rc.0 +accounts-base@1.2.2 +accounts-password@1.1.4 aldeed:collection2@2.5.0 aldeed:simple-schema@1.3.3 alethes:pages@1.8.4 arillo:flow-router-helpers@0.4.5 audit-argument-checks@1.0.4 -autoupdate@1.2.4-rc.0 +autoupdate@1.2.4 babel-compiler@5.8.24_1 babel-runtime@0.1.4 base64@1.0.4 @@ -34,22 +34,22 @@ cfs:storage-adapter@0.2.3 cfs:tempstore@0.1.5 cfs:upload-http@0.0.20 cfs:worker@0.1.4 -check@1.1.0-rc.0 -coffeescript@1.0.11-rc.0 -cosmos:browserify@0.5.1 -dburles:collection-helpers@1.0.3 +check@1.1.0 +coffeescript@1.0.11 +cosmos:browserify@0.8.1 +dburles:collection-helpers@1.0.4 ddp@1.2.2 ddp-client@1.2.1 -ddp-common@1.2.2-rc.0 +ddp-common@1.2.2 ddp-rate-limiter@1.0.0 -ddp-server@1.2.2-rc.0 +ddp-server@1.2.2 deps@1.0.9 diff-sequence@1.0.1 -ecmascript@0.1.6-rc.0 -ecmascript-runtime@0.2.6-rc.0 +ecmascript@0.1.6 +ecmascript-runtime@0.2.6 ejson@1.0.7 -email@1.0.8-rc.0 -es5-shim@4.1.14-rc.0 +email@1.0.8 +es5-shim@4.1.14 fastclick@1.0.7 fortawesome:fontawesome@4.4.0 geojson-utils@1.0.4 @@ -58,20 +58,19 @@ html-tools@1.0.5 htmljs@1.0.5 http@1.1.1 id-map@1.0.4 -idmontie:migrations@1.0.0 +idmontie:migrations@1.0.1 jquery@1.11.4 -jsx@0.1.6 kadira:blaze-layout@2.2.0 -kadira:dochead@1.1.0 +kadira:dochead@1.3.2 kadira:flow-router@2.7.0 -kenton:accounts-sandstorm@0.1.6 +kenton:accounts-sandstorm@0.1.7 launch-screen@1.0.4 livedata@1.0.15 localstorage@1.0.5 logging@1.0.8 matb33:collection-hooks@0.8.1 matteodem:easy-search@1.6.4 -meteor@1.1.10-rc.0 +meteor@1.1.10 meteor-base@1.0.1 meteor-platform@1.2.3 meteorhacks:aggregate@1.3.0 @@ -82,14 +81,14 @@ meteorspark:util@0.2.0 minifiers@1.1.7 minimongo@1.0.10 mobile-status-bar@1.0.6 -mongo@1.1.3-rc.0 +mongo@1.1.3 mongo-id@1.0.1 mongo-livedata@1.0.9 mousetrap:mousetrap@1.4.6_1 mquandalle:autofocus@1.0.0 mquandalle:collection-mutations@0.1.0 -mquandalle:jade@0.4.4 -mquandalle:jade-compiler@0.4.4 +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 @@ -102,17 +101,17 @@ observe-sequence@1.0.7 ongoworks:speakingurl@1.1.0 ordered-dict@1.0.4 peerlibrary:assert@0.2.5 -peerlibrary:base-component@0.13.0 -peerlibrary:blaze-components@0.14.0 +peerlibrary:base-component@0.14.0 +peerlibrary:blaze-components@0.15.1 peerlibrary:computed-field@0.3.0 peerlibrary:reactive-field@0.1.0 perak:markdown@1.0.5 -promise@0.5.1-rc.0 +promise@0.5.1 raix:eventemitter@0.1.3 raix:handlebar-helpers@0.2.5 -random@1.0.5-rc.0 +random@1.0.5 rate-limit@1.0.0 -reactive-dict@1.1.2 +reactive-dict@1.1.3 reactive-var@1.0.6 reload@1.1.4 retry@1.0.4 @@ -126,10 +125,10 @@ softwarerero:accounts-t9n@1.1.4 spacebars@1.0.7 spacebars-compiler@1.0.7 srp@1.0.4 -standard-minifiers@1.0.2-rc.0 +standard-minifiers@1.0.2 tap:i18n@1.7.0 templates:tabs@2.2.0 -templating@1.1.5-rc.0 +templating@1.1.5 templating-tools@1.0.0 tracker@1.0.9 ui@1.0.8 @@ -139,6 +138,6 @@ useraccounts:core@1.12.4 useraccounts:flow-routing@1.12.4 useraccounts:unstyled@1.12.4 verron:autosize@3.0.8 -webapp@1.2.3-rc.0 +webapp@1.2.3 webapp-hashing@1.0.5 zimme:active-route@2.3.2 diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index e7cfc791..28257d00 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -34,7 +34,7 @@ BlazeComponent.extendComponent({ }, openNewListForm() { - this.childrenComponents('addListForm')[0].open(); + this.childComponents('addListForm')[0].open(); }, // XXX Flow components allow us to avoid creating these two setter methods by @@ -179,7 +179,7 @@ BlazeComponent.extendComponent({ // Proxy open() { - this.childrenComponents('inlinedForm')[0].open(); + this.childComponents('inlinedForm')[0].open(); }, events() { -- cgit v1.2.3-1-g7c22 From e92f67f1910a0a3bc40b85436a89e7d55a04a615 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 29 Oct 2015 18:47:39 +0100 Subject: Update eslint-plugin-meteor to 1.7.0 This fixes https://github.com/dferber90/eslint-plugin-meteor/issues/49 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf998e9c..77a9fb74 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,6 @@ "devDependencies": { "babel-eslint": "4.1.3", "eslint": "1.7.3", - "eslint-plugin-meteor": "1.5.0" + "eslint-plugin-meteor": "1.7.0" } } -- cgit v1.2.3-1-g7c22 From dd3cdf3945b26c70b00b5c9c1dd13c74eaed2f8b Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 29 Oct 2015 23:08:27 +0100 Subject: Fix some bugs introduced in aa974aa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yes Wekan need some tests. Yes I need to stop refactoring my code when I’m halp-sleeping in my bed at 4am. --- client/components/boards/boardBody.js | 6 ++++-- client/components/lists/listHeader.js | 4 ++-- client/components/users/userHeader.js | 2 +- client/lib/modal.js | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 28257d00..5c1c974f 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -186,7 +186,8 @@ BlazeComponent.extendComponent({ return [{ submit(evt) { evt.preventDefault(); - const title = this.find('.list-name-input').value.trim(); + const titleInput = this.find('.list-name-input'); + const title = titleInput.value.trim(); if (title) { Lists.insert({ title, @@ -194,7 +195,8 @@ BlazeComponent.extendComponent({ sort: $('.list').length, }); - title.value = ''; + titleInput.value = ''; + titleInput.focus(); } }, }]; diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index ab547f02..dbf9fced 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -5,8 +5,8 @@ BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); - const newTitle = this.childrenComponents('inlinedForm')[0].getValue(); - const list = this.currentData().trim(); + const newTitle = this.childrenComponents('inlinedForm')[0].getValue().trim(); + const list = this.currentData(); if (newTitle) { list.rename(newTitle.trim()); } diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js index 7d35c1b9..a478da0c 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -41,7 +41,7 @@ Template.changePasswordPopup.onRendered(function() { Template.changeLanguagePopup.helpers({ languages() { - return TAPi18n.getLanguages().map((lang, tag) => { + return _.map(TAPi18n.getLanguages(), (lang, tag) => { const name = lang.name; return { tag, name }; }); diff --git a/client/lib/modal.js b/client/lib/modal.js index 7b7516e0..e6301cb5 100644 --- a/client/lib/modal.js +++ b/client/lib/modal.js @@ -21,7 +21,7 @@ window.Modal = new class { } } - open(modalName, { onCloseGoTo = ''}) { + open(modalName, { onCloseGoTo = ''} = {}) { this._currentModal.set(modalName); this._onCloseGoTo = onCloseGoTo; } -- cgit v1.2.3-1-g7c22 From 41b23f88aea0f421226f92b081cdb1b61c64bde4 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Fri, 30 Oct 2015 03:05:43 +0100 Subject: Implement fast-render This required updating 3stack:presence because of the following bug: https://github.com/3stack-software/meteor-presence/pull/3 --- .eslintrc | 1 + .meteor/packages | 1 + .meteor/versions | 5 ++++- History.md | 2 ++ server/publications/fast-render.js | 7 +++++++ 5 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 server/publications/fast-render.js diff --git a/.eslintrc b/.eslintrc index a8017e66..f9321bfb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -90,6 +90,7 @@ globals: BlazeLayout: false DocHead: false ESSearchResults: false + FastRender: false FlowRouter: false FS: false getSlug: false diff --git a/.meteor/packages b/.meteor/packages index 5b666ed1..bdb0b60e 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -49,6 +49,7 @@ audit-argument-checks kadira:blaze-layout kadira:dochead kadira:flow-router +meteorhacks:fast-render meteorhacks:picker meteorhacks:subs-manager mquandalle:autofocus diff --git a/.meteor/versions b/.meteor/versions index bd2390c4..db5d11e7 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,4 +1,4 @@ -3stack:presence@1.0.3 +3stack:presence@1.0.4 accounts-base@1.2.2 accounts-password@1.1.4 aldeed:collection2@2.5.0 @@ -35,6 +35,7 @@ cfs:tempstore@0.1.5 cfs:upload-http@0.0.20 cfs:worker@0.1.4 check@1.1.0 +chuangbo:cookie@1.1.0 coffeescript@1.0.11 cosmos:browserify@0.8.1 dburles:collection-helpers@1.0.4 @@ -75,6 +76,8 @@ meteor-base@1.0.1 meteor-platform@1.2.3 meteorhacks:aggregate@1.3.0 meteorhacks:collection-utils@1.2.0 +meteorhacks:fast-render@2.10.0 +meteorhacks:inject-data@1.4.1 meteorhacks:picker@1.0.3 meteorhacks:subs-manager@1.6.2 meteorspark:util@0.2.0 diff --git a/History.md b/History.md index 462e37a7..bc8ff5ca 100644 --- a/History.md +++ b/History.md @@ -3,6 +3,8 @@ This release features: * Card import from Trello +* Accelerate the initial page rendering by sending the data on the intial HTTP + response instead of waiting for the DDP connection to open. Thanks to GitHub users AlexanderS, fisle, ndarilek, and xavierpriour for their contributions. diff --git a/server/publications/fast-render.js b/server/publications/fast-render.js new file mode 100644 index 00000000..e28b6f2e --- /dev/null +++ b/server/publications/fast-render.js @@ -0,0 +1,7 @@ +FastRender.onAllRoutes(function() { + this.subscribe('boards'); +}); + +FastRender.route('/b/:id/:slug', function({ id }) { + this.subscribe('board', id); +}); -- cgit v1.2.3-1-g7c22