mirror of
https://github.com/wekan/wekan.git
synced 2025-04-24 14:08:31 -04:00
Merge branch 'devel' into minicard-editor
Conflicts: client/components/lists/listBody.js
This commit is contained in:
commit
2b134ff7a9
54 changed files with 1027 additions and 310 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
@ -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
|
137
.eslintrc
137
.eslintrc
|
@ -1,83 +1,101 @@
|
|||
ecmaFeatures:
|
||||
experimentalObjectRestSpread: true
|
||||
|
||||
plugins:
|
||||
- meteor
|
||||
|
||||
parser: babel-eslint
|
||||
|
||||
rules:
|
||||
accessor-pairs: [2]
|
||||
consistent-return: [2]
|
||||
strict: 0
|
||||
no-undef: 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
|
||||
|
||||
# eslint-plugin-meteor
|
||||
## Meteor API
|
||||
meteor/globals: 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:
|
||||
|
||||
# Our collections
|
||||
collections:
|
||||
- AccountsTemplates
|
||||
- Activities
|
||||
- Attachments
|
||||
- Boards
|
||||
- CardComments
|
||||
- Cards
|
||||
- Lists
|
||||
- UnsavedEditCollection
|
||||
- Users
|
||||
|
||||
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
|
||||
|
||||
# Exported by packages we use
|
||||
'$': false
|
||||
_: false
|
||||
autosize: false
|
||||
Avatar: true
|
||||
Avatars: true
|
||||
BlazeComponent: false
|
||||
BlazeLayout: false
|
||||
DocHead: false
|
||||
ESSearchResults: false
|
||||
FastRender: false
|
||||
FlowRouter: false
|
||||
FS: false
|
||||
getSlug: false
|
||||
Migrations: false
|
||||
moment: false
|
||||
Mousetrap: false
|
||||
Picker: false
|
||||
Presence: true
|
||||
|
@ -90,17 +108,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
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@
|
|||
.tx/
|
||||
*.sublime-workspace
|
||||
tmp/
|
||||
node_modules/
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -50,7 +47,9 @@ alethes:pages
|
|||
arillo:flow-router-helpers
|
||||
audit-argument-checks
|
||||
kadira:blaze-layout
|
||||
kadira:dochead
|
||||
kadira:flow-router
|
||||
meteorhacks:fast-render
|
||||
meteorhacks:picker
|
||||
meteorhacks:subs-manager
|
||||
mquandalle:autofocus
|
||||
|
|
|
@ -1 +1 @@
|
|||
METEOR@1.2.0.1
|
||||
METEOR@1.2.1
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
3stack:presence@1.0.3
|
||||
accounts-base@1.2.1
|
||||
accounts-password@1.1.3
|
||||
3stack:presence@1.0.4
|
||||
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.3
|
||||
autoupdate@1.2.4
|
||||
babel-compiler@5.8.24_1
|
||||
babel-runtime@0.1.4
|
||||
base64@1.0.4
|
||||
|
@ -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
|
||||
|
@ -30,26 +30,27 @@ 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
|
||||
check@1.0.6
|
||||
coffeescript@1.0.9
|
||||
cosmos:browserify@0.5.1
|
||||
dburles:collection-helpers@1.0.3
|
||||
check@1.1.0
|
||||
chuangbo:cookie@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.1
|
||||
ddp-common@1.2.2
|
||||
ddp-rate-limiter@1.0.0
|
||||
ddp-server@1.2.1
|
||||
ddp-server@1.2.2
|
||||
deps@1.0.9
|
||||
diff-sequence@1.0.1
|
||||
ecmascript@0.1.4
|
||||
ecmascript-collections@0.1.6
|
||||
ecmascript@0.1.6
|
||||
ecmascript-runtime@0.2.6
|
||||
ejson@1.0.7
|
||||
email@1.0.7
|
||||
es5-shim@4.1.13
|
||||
email@1.0.8
|
||||
es5-shim@4.1.14
|
||||
fastclick@1.0.7
|
||||
fortawesome:fontawesome@4.4.0
|
||||
geojson-utils@1.0.4
|
||||
|
@ -58,38 +59,40 @@ 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
|
||||
kadira:blaze-layout@2.1.0
|
||||
kadira:flow-router@2.6.1
|
||||
kenton:accounts-sandstorm@0.1.4
|
||||
kadira:blaze-layout@2.2.0
|
||||
kadira:dochead@1.3.2
|
||||
kadira:flow-router@2.7.0
|
||||
kenton:accounts-sandstorm@0.1.7
|
||||
launch-screen@1.0.4
|
||||
less@2.5.0_2
|
||||
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
|
||||
matb33:collection-hooks@0.8.1
|
||||
matteodem:easy-search@1.6.4
|
||||
meteor@1.1.10
|
||||
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
|
||||
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.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.3_1
|
||||
mquandalle:jade-compiler@0.4.3
|
||||
mquandalle:jquery-textcomplete@0.3.9_1
|
||||
mquandalle:jade@0.4.5
|
||||
mquandalle:jade-compiler@0.4.5
|
||||
mquandalle:jquery-textcomplete@0.8.0_1
|
||||
mquandalle:jquery-ui-drag-drop-sort@0.1.0
|
||||
mquandalle:moment@1.0.0
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
|
@ -101,15 +104,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.10.0
|
||||
peerlibrary:blaze-components@0.13.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.4.8
|
||||
promise@0.5.1
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.4
|
||||
random@1.0.4
|
||||
raix:handlebar-helpers@0.2.5
|
||||
random@1.0.5
|
||||
rate-limit@1.0.0
|
||||
reactive-dict@1.1.1
|
||||
reactive-dict@1.1.3
|
||||
reactive-var@1.0.6
|
||||
reload@1.1.4
|
||||
retry@1.0.4
|
||||
|
@ -123,19 +128,19 @@ softwarerero:accounts-t9n@1.1.4
|
|||
spacebars@1.0.7
|
||||
spacebars-compiler@1.0.7
|
||||
srp@1.0.4
|
||||
standard-minifiers@1.0.0
|
||||
tap:i18n@1.6.1
|
||||
standard-minifiers@1.0.2
|
||||
tap:i18n@1.7.0
|
||||
templates:tabs@2.2.0
|
||||
templating@1.1.3
|
||||
templating@1.1.5
|
||||
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
|
||||
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@1.2.3
|
||||
webapp-hashing@1.0.5
|
||||
zimme:active-route@2.3.2
|
||||
|
|
|
@ -3,6 +3,6 @@ language: node_js
|
|||
node_js:
|
||||
- "0.10.40"
|
||||
install:
|
||||
- "npm install -g eslint"
|
||||
- "npm install"
|
||||
script:
|
||||
- "eslint ./"
|
||||
- "npm test"
|
||||
|
|
13
History.md
13
History.md
|
@ -1,4 +1,15 @@
|
|||
# NEXT — v0.9
|
||||
# v0.10
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
|
|
@ -14,53 +14,62 @@ template(name="boardActivities")
|
|||
p.activity-desc
|
||||
+memberName(user=user)
|
||||
|
||||
if($eq activityType 'createBoard')
|
||||
| {{_ 'activity-created' boardLabel}}.
|
||||
|
||||
if($eq activityType 'createList')
|
||||
| {{_ 'activity-added' list.title boardLabel}}.
|
||||
|
||||
if($eq activityType 'archivedList')
|
||||
| {{_ 'activity-archived' list.title}}.
|
||||
|
||||
if($eq activityType 'createCard')
|
||||
| {{{_ 'activity-added' cardLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'archivedCard')
|
||||
| {{{_ 'activity-archived' cardLink}}}.
|
||||
|
||||
if($eq activityType 'restoredCard')
|
||||
| {{{_ 'activity-sent' cardLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'moveCard')
|
||||
| {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
|
||||
if($eq activityType 'addAttachment')
|
||||
| {{{_ 'activity-attached' attachmentLink cardLink}}}.
|
||||
|
||||
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 'unjoinMember')
|
||||
if($eq currentUser._id member._id)
|
||||
| {{{_ 'activity-unjoined' cardLink}}}.
|
||||
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}}}.
|
||||
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 'importList')
|
||||
| {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
|
||||
|
||||
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 'removeBoardMember')
|
||||
| {{{_ 'activity-excluded' memberLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'restoredCard')
|
||||
| {{{_ 'activity-sent' cardLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'unjoinMember')
|
||||
if($eq currentUser._id member._id)
|
||||
| {{{_ 'activity-unjoined' cardLink}}}.
|
||||
else
|
||||
| {{{_ 'activity-removed' memberLink cardLink}}}.
|
||||
|
||||
span.activity-meta {{ moment createdAt }}
|
||||
|
||||
|
@ -72,6 +81,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}}.
|
||||
|
|
|
@ -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;
|
||||
|
@ -55,11 +55,29 @@ 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));
|
||||
},
|
||||
|
||||
listLabel() {
|
||||
return this.currentData().list().title;
|
||||
},
|
||||
|
||||
sourceLink() {
|
||||
const source = this.currentData().source;
|
||||
if(source) {
|
||||
if(source.url) {
|
||||
return Blaze.toHTML(HTML.A({
|
||||
href: source.url,
|
||||
}, source.system));
|
||||
} else {
|
||||
return source.system;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
memberLink() {
|
||||
return Blaze.toHTMLWithData(Template.memberName, {
|
||||
user: this.currentData().member(),
|
||||
|
@ -69,7 +87,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()));
|
||||
},
|
||||
|
@ -83,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,
|
||||
|
|
|
@ -24,11 +24,12 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
'submit .js-new-comment-form'(evt) {
|
||||
const input = this.getInput();
|
||||
if ($.trim(input.val())) {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
openNewListForm() {
|
||||
this.componentChildren('addListForm')[0].open();
|
||||
this.childComponents('addListForm')[0].open();
|
||||
},
|
||||
|
||||
// XXX Flow components allow us to avoid creating these two setter methods by
|
||||
|
@ -179,22 +179,24 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// Proxy
|
||||
open() {
|
||||
this.componentChildren('inlinedForm')[0].open();
|
||||
this.childComponents('inlinedForm')[0].open();
|
||||
},
|
||||
|
||||
events() {
|
||||
return [{
|
||||
submit(evt) {
|
||||
evt.preventDefault();
|
||||
const title = this.find('.list-name-input');
|
||||
if ($.trim(title.value)) {
|
||||
const titleInput = this.find('.list-name-input');
|
||||
const title = titleInput.value.trim();
|
||||
if (title) {
|
||||
Lists.insert({
|
||||
title: title.value,
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: $('.list').length,
|
||||
});
|
||||
|
||||
title.value = '';
|
||||
titleInput.value = '';
|
||||
titleInput.focus();
|
||||
}
|
||||
},
|
||||
}];
|
||||
|
|
|
@ -107,6 +107,9 @@ template(name="createBoardPopup")
|
|||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import {{_ 'import-board'}}
|
||||
|
||||
|
||||
template(name="boardChangeTitlePopup")
|
||||
|
|
|
@ -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,
|
||||
}];
|
||||
},
|
||||
|
|
2
client/components/boards/boardHeader.styl
Normal file
2
client/components/boards/boardHeader.styl
Normal file
|
@ -0,0 +1,2 @@
|
|||
a.js-import
|
||||
text-decoration underline
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
@ -62,7 +62,8 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
};
|
||||
|
||||
return [_.extend(events, {
|
||||
return [{
|
||||
...events,
|
||||
'click .js-close-card-details'() {
|
||||
Utils.goBoardId(this.data().boardId);
|
||||
},
|
||||
|
@ -74,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);
|
||||
}
|
||||
},
|
||||
|
@ -83,10 +84,10 @@ 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;
|
||||
},
|
||||
})];
|
||||
}];
|
||||
},
|
||||
}).register('cardDetails');
|
||||
|
||||
|
@ -105,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());
|
||||
}
|
||||
|
|
|
@ -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"}}
|
||||
|
|
|
@ -13,7 +13,7 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
labels() {
|
||||
return _.map(labelColors, (color) => {
|
||||
return labelColors.map((color) => {
|
||||
return { color, name: '' };
|
||||
});
|
||||
},
|
||||
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
7
client/components/import/import.jade
Normal file
7
client/components/import/import.jade
Normal file
|
@ -0,0 +1,7 @@
|
|||
template(name="importPopup")
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
form
|
||||
p: label(for='import-textarea') {{_ getLabel}}
|
||||
textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
|
||||
input.primary.wide(type="submit" value="{{_ 'import'}}")
|
90
client/components/import/import.js
Normal file
90
client/components/import/import.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
/// 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('.js-import-json').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);
|
||||
this.onFinish(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
}];
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
this.error = new ReactiveVar('');
|
||||
},
|
||||
|
||||
setError(error) {
|
||||
this.error.set(error);
|
||||
},
|
||||
|
||||
onFinish() {
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
ImportPopup.extendComponent({
|
||||
getAdditionalData() {
|
||||
const listId = this.data()._id;
|
||||
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;
|
||||
},
|
||||
|
||||
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';
|
||||
},
|
||||
|
||||
onFinish(response) {
|
||||
Utils.goBoardId(response);
|
||||
},
|
||||
}).register('boardImportBoardPopup');
|
||||
|
|
@ -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({
|
||||
|
|
|
@ -11,8 +11,8 @@ BlazeComponent.extendComponent({
|
|||
options = options || {};
|
||||
options.position = options.position || 'top';
|
||||
|
||||
const forms = this.componentChildren('inlinedForm');
|
||||
let form = _.find(forms, (component) => {
|
||||
const forms = this.childrenComponents('inlinedForm');
|
||||
let form = forms.find((component) => {
|
||||
return component.data().position === options.position;
|
||||
});
|
||||
if (!form && forms.length > 0) {
|
||||
|
@ -26,8 +26,8 @@ BlazeComponent.extendComponent({
|
|||
const firstCardDom = this.find('.js-minicard:first');
|
||||
const lastCardDom = this.find('.js-minicard:last');
|
||||
const textarea = $(evt.currentTarget).find('textarea');
|
||||
let title = textarea.val();
|
||||
const position = Blaze.getData(evt.currentTarget).position;
|
||||
const position = this.currentData().position;
|
||||
let title = textarea.val().trim();
|
||||
let sortIndex;
|
||||
if (position === 'top') {
|
||||
sortIndex = Utils.calculateIndex(null, firstCardDom).base;
|
||||
|
@ -62,7 +62,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
});
|
||||
|
||||
if ($.trim(title)) {
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
title,
|
||||
listId: this.data()._id,
|
||||
|
|
|
@ -25,6 +25,7 @@ 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")
|
||||
|
|
|
@ -5,10 +5,10 @@ BlazeComponent.extendComponent({
|
|||
|
||||
editTitle(evt) {
|
||||
evt.preventDefault();
|
||||
const newTitle = this.componentChildren('inlinedForm')[0].getValue();
|
||||
const newTitle = this.childrenComponents('inlinedForm')[0].getValue().trim();
|
||||
const list = this.currentData();
|
||||
if ($.trim(newTitle)) {
|
||||
list.rename(newTitle);
|
||||
if (newTitle) {
|
||||
list.rename(newTitle.trim());
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
let dropdownMenuIsOpened = false;
|
||||
|
||||
Template.editor.onRendered(() => {
|
||||
const $textarea = this.$('textarea');
|
||||
|
||||
autosize($textarea);
|
||||
|
||||
$textarea.textcomplete([
|
||||
$textarea.escapeableTextComplete([
|
||||
// Emojies
|
||||
{
|
||||
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) {
|
||||
|
@ -30,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) {
|
||||
|
@ -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
|
||||
|
@ -78,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;
|
||||
});
|
||||
|
|
|
@ -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'}}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -54,7 +54,7 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
reachNextPeak() {
|
||||
const activitiesComponent = this.componentChildren('activities')[0];
|
||||
const activitiesComponent = this.childrenComponents('activities')[0];
|
||||
activitiesComponent.loadNextPage();
|
||||
},
|
||||
|
||||
|
@ -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');
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('');
|
||||
});
|
||||
|
|
|
@ -88,3 +88,26 @@ _.each(redirections, (newPath, oldPath) => {
|
|||
}],
|
||||
});
|
||||
});
|
||||
|
||||
// As it is not possible to use template helpers in the page <head> we create a
|
||||
// reactive function whose role is to set any page-specific tag in the <head>
|
||||
// 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 <meta> 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(' - '));
|
||||
});
|
||||
});
|
||||
|
|
41
client/lib/accessibility.js
Normal file
41
client/lib/accessibility.js
Normal file
|
@ -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);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
30
client/lib/textComplete.js
Normal file
30
client/lib/textComplete.js
Normal file
|
@ -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
|
||||
);
|
|
@ -7,12 +7,14 @@
|
|||
"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-imported-board": "imported %s from %s",
|
||||
"activity-joined": "joined %s",
|
||||
"activity-moved": "moved %s from %s to %s",
|
||||
"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",
|
||||
|
@ -53,6 +55,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",
|
||||
|
@ -87,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",
|
||||
|
@ -109,6 +122,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",
|
||||
|
@ -118,6 +135,12 @@
|
|||
"fullname": "Full Name",
|
||||
"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",
|
||||
|
@ -136,6 +159,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 a Trello card",
|
||||
"listMoveCardsPopup-title": "Move All Cards in List",
|
||||
"lists": "Lists",
|
||||
"log-out": "Log Out",
|
||||
|
@ -155,6 +179,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 <a href='%s'>logging in</a>.",
|
||||
"page-not-found": "Page not found.",
|
||||
"password": "Password",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -92,12 +92,16 @@ Boards.helpers({
|
|||
return _.where(this.members, {isActive: true});
|
||||
},
|
||||
|
||||
getLabel(name, color) {
|
||||
return _.findWhere(this.labels, { name, color });
|
||||
},
|
||||
|
||||
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() {
|
||||
|
@ -107,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({
|
||||
|
@ -131,18 +143,26 @@ Boards.mutations({
|
|||
},
|
||||
|
||||
addLabel(name, color) {
|
||||
const _id = Random.id(6);
|
||||
return { $push: {labels: { _id, name, color }}};
|
||||
// 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 }}};
|
||||
}
|
||||
},
|
||||
|
||||
editLabel(labelId, name, color) {
|
||||
const labelIndex = this.labelIndex(labelId);
|
||||
return {
|
||||
$set: {
|
||||
[`labels.${labelIndex}.name`]: name,
|
||||
[`labels.${labelIndex}.color`]: color,
|
||||
},
|
||||
};
|
||||
if (!this.getLabel(name, color)) {
|
||||
const labelIndex = this.labelIndex(labelId);
|
||||
return {
|
||||
$set: {
|
||||
[`labels.${labelIndex}.name`]: name,
|
||||
[`labels.${labelIndex}.color`]: color,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
removeLabel(labelId) {
|
||||
|
@ -259,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),
|
||||
|
@ -307,7 +327,7 @@ if (Meteor.isServer) {
|
|||
{ boardId: doc._id },
|
||||
{
|
||||
$pull: {
|
||||
labels: removedLabelId,
|
||||
labelIds: removedLabelId,
|
||||
},
|
||||
},
|
||||
{ multi: true }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
364
models/import.js
Normal file
364
models/import.js
Normal file
|
@ -0,0 +1,364 @@
|
|||
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!)
|
||||
this.createdAt = {
|
||||
board: null,
|
||||
cards: {},
|
||||
lists: {},
|
||||
};
|
||||
// Map of labels Trello ID => Wekan ID
|
||||
this.labels = {};
|
||||
// Map of lists Trello ID => Wekan ID
|
||||
this.lists = {};
|
||||
// The comments, indexed by Trello card id (to map when importing cards)
|
||||
this.comments = {};
|
||||
}
|
||||
|
||||
checkActions(trelloActions) {
|
||||
check(trelloActions, [Match.ObjectIncluding({
|
||||
data: Object,
|
||||
date: DateString,
|
||||
type: String,
|
||||
})]);
|
||||
// XXX we could perform more thorough checks based on action type
|
||||
}
|
||||
|
||||
checkBoard(trelloBoard) {
|
||||
check(trelloBoard, Match.ObjectIncluding({
|
||||
closed: Boolean,
|
||||
name: String,
|
||||
prefs: Match.ObjectIncluding({
|
||||
// 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;
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
checkCards(trelloCards) {
|
||||
check(trelloCards, [Match.ObjectIncluding({
|
||||
closed: Boolean,
|
||||
dateLastActivity: DateString,
|
||||
desc: String,
|
||||
idLabels: [String],
|
||||
idMembers: [String],
|
||||
name: String,
|
||||
pos: Number,
|
||||
})]);
|
||||
}
|
||||
|
||||
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,
|
||||
name: String,
|
||||
})]);
|
||||
}
|
||||
|
||||
// You must call parseActions before calling this one.
|
||||
createBoardAndLabels(trelloBoard) {
|
||||
const createdAt = this.createdAt.board;
|
||||
const boardToCreate = {
|
||||
archived: trelloBoard.closed,
|
||||
color: this.getColor(trelloBoard.prefs.background),
|
||||
createdAt,
|
||||
labels: [],
|
||||
members: [{
|
||||
userId: Meteor.userId(),
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
}],
|
||||
permission: this.getPermission(trelloBoard.prefs.permissionLevel),
|
||||
slug: getSlug(trelloBoard.name) || 'board',
|
||||
stars: 0,
|
||||
title: trelloBoard.name,
|
||||
};
|
||||
trelloBoard.labels.forEach((label) => {
|
||||
const 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._id;
|
||||
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: now,
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
archived: list.closed,
|
||||
boardId,
|
||||
// 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(),
|
||||
};
|
||||
const listId = Lists.direct.insert(listToCreate);
|
||||
const now = new Date();
|
||||
Lists.direct.update(listId, {$set: {'updatedAt': now}});
|
||||
this.lists[list.id] = listId;
|
||||
// log activity
|
||||
Activities.direct.insert({
|
||||
activityType: 'importList',
|
||||
boardId,
|
||||
createdAt: now,
|
||||
listId,
|
||||
source: {
|
||||
id: list.id,
|
||||
system: 'Trello',
|
||||
},
|
||||
// We attribute the import to current user, not the one from the
|
||||
// original object
|
||||
userId: Meteor.userId(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createCardsAndComments(trelloCards, boardId) {
|
||||
const result = [];
|
||||
trelloCards.forEach((card) => {
|
||||
const cardToCreate = {
|
||||
archived: card.closed,
|
||||
boardId,
|
||||
createdAt: new Date(this.createdAt.cards[card.id] || Date.now()),
|
||||
dateLastActivity: new Date(),
|
||||
description: card.desc,
|
||||
listId: this.lists[card.idList],
|
||||
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];
|
||||
});
|
||||
}
|
||||
// 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(),
|
||||
};
|
||||
// dateLastActivity will be set from activity insert, no need to
|
||||
// update it ourselves
|
||||
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
|
||||
result.push(cardId);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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) {
|
||||
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 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);
|
||||
trelloCreator.createLists(trelloBoard.lists, boardId);
|
||||
trelloCreator.createCardsAndComments(trelloBoard.cards, boardId);
|
||||
// XXX add members
|
||||
return boardId;
|
||||
},
|
||||
|
||||
importTrelloCard(trelloCard, data) {
|
||||
const trelloCreator = new TrelloCreator();
|
||||
|
||||
// 1. check parameters are ok from a syntax point of view
|
||||
try {
|
||||
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');
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
if (Meteor.isServer) {
|
||||
if (!allowIsBoardMember(Meteor.userId(), Boards.findOne(list.boardId))) {
|
||||
throw new Meteor.Error('error-board-notAMember');
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
},
|
||||
});
|
|
@ -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,31 +8,50 @@ 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 });
|
||||
},
|
||||
|
||||
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() {
|
||||
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.
|
||||
// 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() {
|
||||
|
@ -41,9 +60,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();
|
||||
|
@ -117,7 +136,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}});
|
||||
});
|
||||
}
|
||||
|
@ -136,7 +155,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,
|
||||
|
|
24
package.json
Normal file
24
package.json
Normal file
|
@ -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 --silent lint"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wekan/wekan.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wekan/wekan/issues"
|
||||
},
|
||||
"homepage": "http://wekan.io",
|
||||
"devDependencies": {
|
||||
"babel-eslint": "4.1.3",
|
||||
"eslint": "1.7.3",
|
||||
"eslint-plugin-meteor": "1.7.0"
|
||||
}
|
||||
}
|
39
sandstorm.js
39
sandstorm.js
|
@ -21,20 +21,13 @@ 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;
|
||||
const memberIndex = _.indexOf(_.pluck(boardMembers, 'userId'), userId);
|
||||
const memberIndex = _.pluck(boardMembers, 'userId').indexOf(userId);
|
||||
|
||||
let modifier;
|
||||
if (memberIndex > -1)
|
||||
|
@ -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});
|
||||
|
@ -101,6 +91,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 as 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) {
|
||||
|
@ -110,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}/, '');
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
@ -37,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() } },
|
||||
|
@ -110,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,
|
||||
|
|
|
@ -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({
|
||||
|
|
7
server/publications/fast-render.js
Normal file
7
server/publications/fast-render.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
FastRender.onAllRoutes(function() {
|
||||
this.subscribe('boards');
|
||||
});
|
||||
|
||||
FastRender.route('/b/:id/:slug', function({ id }) {
|
||||
this.subscribe('board', id);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue