Browse Source

Merge pull request #1 from wekan/devel

ldap changes
Thiago Fernando 6 years ago
parent
commit
ce0473480b
100 changed files with 7950 additions and 723 deletions
  1. 7 3
      .eslintrc.json
  2. 17 6
      .github/ISSUE_TEMPLATE.md
  3. 16 2
      .gitignore
  4. 15 7
      .meteor/packages
  5. 16 5
      .meteor/versions
  6. 1 1
      .tx/config
  7. 2145 1
      CHANGELOG.md
  8. 4 0
      CONTRIBUTING.md
  9. 0 5
      Contributing.md
  10. 153 38
      Dockerfile
  11. 1 1
      LICENSE
  12. 79 43
      README.md
  13. 129 0
      SECURITY.md
  14. 9 0
      Stackerfile.yml
  15. 1 1
      app.json
  16. 68 1
      client/components/activities/activities.jade
  17. 59 4
      client/components/activities/activities.js
  18. 9 2
      client/components/activities/comments.js
  19. 12 4
      client/components/boards/boardArchive.jade
  20. 11 6
      client/components/boards/boardArchive.js
  21. 17 3
      client/components/boards/boardBody.jade
  22. 208 8
      client/components/boards/boardBody.js
  23. 4 3
      client/components/boards/boardBody.styl
  24. 75 128
      client/components/boards/boardHeader.jade
  25. 24 77
      client/components/boards/boardHeader.js
  26. 19 0
      client/components/boards/boardHeader.styl
  27. 9 4
      client/components/boards/boardsList.jade
  28. 32 4
      client/components/boards/boardsList.js
  29. 14 0
      client/components/boards/boardsList.styl
  30. 8 0
      client/components/boards/miniboard.jade
  31. 7 2
      client/components/cards/attachments.js
  32. 76 0
      client/components/cards/cardCustomFields.jade
  33. 179 0
      client/components/cards/cardCustomFields.js
  34. 0 16
      client/components/cards/cardDate.jade
  35. 62 37
      client/components/cards/cardDate.js
  36. 2 21
      client/components/cards/cardDate.styl
  37. 163 46
      client/components/cards/cardDetails.jade
  38. 319 90
      client/components/cards/cardDetails.js
  39. 107 7
      client/components/cards/cardDetails.styl
  40. 4 4
      client/components/cards/cardTime.jade
  41. 8 11
      client/components/cards/cardTime.js
  42. 3 4
      client/components/cards/checklists.jade
  43. 9 4
      client/components/cards/checklists.js
  44. 53 1
      client/components/cards/labels.styl
  45. 58 13
      client/components/cards/minicard.jade
  46. 11 0
      client/components/cards/minicard.js
  47. 123 1
      client/components/cards/minicard.styl
  48. 97 0
      client/components/cards/subtasks.jade
  49. 146 0
      client/components/cards/subtasks.js
  50. 142 0
      client/components/cards/subtasks.styl
  51. 15 0
      client/components/forms/datepicker.jade
  52. 17 0
      client/components/forms/datepicker.styl
  53. 7 1
      client/components/forms/forms.styl
  54. 2 2
      client/components/import/import.jade
  55. 21 4
      client/components/lists/list.js
  56. 129 8
      client/components/lists/list.styl
  57. 91 2
      client/components/lists/listBody.jade
  58. 414 16
      client/components/lists/listBody.js
  59. 18 4
      client/components/lists/listHeader.jade
  60. 47 0
      client/components/lists/listHeader.js
  61. 8 0
      client/components/lists/minilist.jade
  62. 2 1
      client/components/main/editor.jade
  63. 7 2
      client/components/main/editor.js
  64. 36 37
      client/components/main/header.jade
  65. 5 0
      client/components/main/header.js
  66. 1 3
      client/components/main/header.styl
  67. 41 18
      client/components/main/layouts.jade
  68. 115 0
      client/components/main/layouts.js
  69. 120 3
      client/components/main/layouts.styl
  70. 3 0
      client/components/main/popup.styl
  71. 11 7
      client/components/mixins/perfectScrollbar.js
  72. BIN
      client/components/rules/.DS_Store
  73. 72 0
      client/components/rules/actions/boardActions.jade
  74. 168 0
      client/components/rules/actions/boardActions.js
  75. 55 0
      client/components/rules/actions/cardActions.jade
  76. 188 0
      client/components/rules/actions/cardActions.js
  77. 70 0
      client/components/rules/actions/checklistActions.jade
  78. 151 0
      client/components/rules/actions/checklistActions.js
  79. 11 0
      client/components/rules/actions/mailActions.jade
  80. 35 0
      client/components/rules/actions/mailActions.js
  81. 20 0
      client/components/rules/ruleDetails.jade
  82. 38 0
      client/components/rules/ruleDetails.js
  83. 190 0
      client/components/rules/rules.styl
  84. 29 0
      client/components/rules/rulesActions.jade
  85. 58 0
      client/components/rules/rulesActions.js
  86. 27 0
      client/components/rules/rulesList.jade
  87. 15 0
      client/components/rules/rulesList.js
  88. 9 0
      client/components/rules/rulesMain.jade
  89. 97 0
      client/components/rules/rulesMain.js
  90. 25 0
      client/components/rules/rulesTriggers.jade
  91. 53 0
      client/components/rules/rulesTriggers.js
  92. 116 0
      client/components/rules/triggers/boardTriggers.jade
  93. 119 0
      client/components/rules/triggers/boardTriggers.js
  94. 114 0
      client/components/rules/triggers/cardTriggers.jade
  95. 131 0
      client/components/rules/triggers/cardTriggers.js
  96. 125 0
      client/components/rules/triggers/checklistTriggers.jade
  97. 146 0
      client/components/rules/triggers/checklistTriggers.js
  98. 9 0
      client/components/settings/connectionMethod.jade
  99. 37 0
      client/components/settings/connectionMethod.js
  100. 1 1
      client/components/settings/informationBody.jade

+ 7 - 3
.eslintrc.json

@@ -6,7 +6,7 @@
     "browser": true
     "browser": true
   },
   },
   "parserOptions": {
   "parserOptions": {
-    "ecmaVersion": 6,
+    "ecmaVersion": 2017,
     "sourceType": "module",
     "sourceType": "module",
     "ecmaFeatures": {
     "ecmaFeatures": {
       "experimentalObjectRestSpread": true
       "experimentalObjectRestSpread": true
@@ -14,7 +14,7 @@
   },
   },
   "rules": {
   "rules": {
     "strict": 0,
     "strict": 0,
-    "no-undef": 2,
+    "no-undef": 0,
     "accessor-pairs": 2,
     "accessor-pairs": 2,
     "comma-dangle": [2, "always-multiline"],
     "comma-dangle": [2, "always-multiline"],
     "consistent-return": 2,
     "consistent-return": 2,
@@ -100,7 +100,9 @@
     "Attachments": true,
     "Attachments": true,
     "Boards": true,
     "Boards": true,
     "CardComments": true,
     "CardComments": true,
+    "DatePicker" : true,
     "Cards": true,
     "Cards": true,
+    "CustomFields": true,
     "Lists": true,
     "Lists": true,
     "UnsavedEditCollection": true,
     "UnsavedEditCollection": true,
     "Users": true,
     "Users": true,
@@ -119,7 +121,8 @@
     "allowIsBoardAdmin": true,
     "allowIsBoardAdmin": true,
     "allowIsBoardMember": true,
     "allowIsBoardMember": true,
     "allowIsBoardMemberByCard": true,
     "allowIsBoardMemberByCard": true,
-    "allowIsBoardMemberNonComment": true,
+    "allowIsBoardMemberCommentOnly": true,
+    "allowIsBoardMemberNoComments": true,
     "Emoji": true,
     "Emoji": true,
     "Checklists": true,
     "Checklists": true,
     "Settings": true,
     "Settings": true,
@@ -132,6 +135,7 @@
     "Announcements": true,
     "Announcements": true,
     "Swimlanes": true,
     "Swimlanes": true,
     "ChecklistItems": true,
     "ChecklistItems": true,
+    "Subtasks": true,
     "Npm": true
     "Npm": true
   }
   }
 }
 }

+ 17 - 6
.github/ISSUE_TEMPLATE.md

@@ -1,16 +1,27 @@
 ## Issue
 ## Issue
 
 
+Add these issues to elsewhere:
+- Snap: https://github.com/wekan/wekan-snap/issues
+
+Other Wekan issues can be added here.
+
 **Server Setup Information**:
 **Server Setup Information**:
 
 
+* Did you test in newest Wekan?:
+* For new Wekan install, did you configure root-url correctly https://github.com/wekan/wekan/wiki/Settings ?
+* Wekan version:
+* If this is about old version of Wekan, what upgrade problem you have?:
 * Operating System:
 * Operating System:
-* Deployment Method(snap/sandstorm/mongodb bundle):
-* Http frontend (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first):
+* Deployment Method(snap/docker/sandstorm/mongodb bundle/source):
+* Http frontend if any (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first):
 * Node Version:
 * Node Version:
 * MongoDB Version:
 * MongoDB Version:
 * ROOT_URL environment variable http(s)://(subdomain).example.com(/suburl):
 * ROOT_URL environment variable http(s)://(subdomain).example.com(/suburl):
 
 
 **Problem description**:
 **Problem description**:
-- *be as explicit as you can*
-- *describe the problem and its symptoms*
-- *explain how to reproduce*
-- *attach whatever information that can help understanding the context (screen capture, log files in .zip file)*
+- *REQUIRED: Add recorded animated gif about how it works currently, and screenshot mockups how it should work. Use peek to record animgif in Linux https://github.com/phw/peek*
+- *Explain steps how to reproduce*
+- *In webbrowser, what does show Right Click / Inspect / Console ? Chrome shows more detailed info than Firefox.*
+- *If using Snap, what does show command `sudo snap logs wekan.wekan` ?*
+- *If using Docker, what does show command `sudo docker logs wekan-app` ?*
+- *If logs are very long, attach them in .zip file*

+ 16 - 2
.gitignore

@@ -4,14 +4,28 @@
 *.sublime-workspace
 *.sublime-workspace
 tmp/
 tmp/
 node_modules/
 node_modules/
+npm-debug.log
 .vscode/
 .vscode/
 .idea/
 .idea/
 .build/*
 .build/*
-packages/kadira-flow-router/
-packages/meteor-useraccounts-core/
 package-lock.json
 package-lock.json
 **/parts/
 **/parts/
 **/stage
 **/stage
 **/prime
 **/prime
 **/*.snap
 **/*.snap
 snap/.snapcraft/
 snap/.snapcraft/
+.idea
+.DS_Store
+.DS_Store?
+.build*
+*.browserify.js.cached
+*.browserify.js.map
+.build*
+versions.json
+.versions
+.npm
+.build*
+._*
+.Trashes
+Thumbs.db
+ehthumbs.db

+ 15 - 7
.meteor/packages

@@ -6,7 +6,7 @@
 meteor-base@1.2.0
 meteor-base@1.2.0
 
 
 # Build system
 # Build system
-ecmascript@0.9.0
+ecmascript
 stylus@2.513.13
 stylus@2.513.13
 standard-minifier-css@1.3.5
 standard-minifier-css@1.3.5
 standard-minifier-js@2.2.0
 standard-minifier-js@2.2.0
@@ -31,6 +31,9 @@ kenton:accounts-sandstorm
 service-configuration@1.0.11
 service-configuration@1.0.11
 useraccounts:unstyled
 useraccounts:unstyled
 useraccounts:flow-routing
 useraccounts:flow-routing
+wekan-ldap
+wekan-accounts-cas
+wekan-accounts-oidc
 
 
 # Utilities
 # Utilities
 check@1.2.5
 check@1.2.5
@@ -49,7 +52,6 @@ kadira:dochead
 meteorhacks:picker
 meteorhacks:picker
 meteorhacks:subs-manager
 meteorhacks:subs-manager
 mquandalle:autofocus
 mquandalle:autofocus
-mquandalle:moment
 ongoworks:speakingurl
 ongoworks:speakingurl
 raix:handlebar-helpers
 raix:handlebar-helpers
 tap:i18n
 tap:i18n
@@ -63,9 +65,7 @@ mousetrap:mousetrap
 mquandalle:jquery-textcomplete
 mquandalle:jquery-textcomplete
 mquandalle:jquery-ui-drag-drop-sort
 mquandalle:jquery-ui-drag-drop-sort
 mquandalle:mousetrap-bindglobal
 mquandalle:mousetrap-bindglobal
-mquandalle:perfect-scrollbar
 peerlibrary:blaze-components@=0.15.1
 peerlibrary:blaze-components@=0.15.1
-perak:markdown
 templates:tabs
 templates:tabs
 verron:autosize
 verron:autosize
 simple:json-routes
 simple:json-routes
@@ -77,10 +77,18 @@ email@1.2.3
 horka:swipebox
 horka:swipebox
 dynamic-import@0.2.0
 dynamic-import@0.2.0
 staringatlights:fast-render
 staringatlights:fast-render
-staringatlights:flow-router
 
 
 mixmax:smart-disconnect
 mixmax:smart-disconnect
 accounts-password@1.5.0
 accounts-password@1.5.0
 cfs:gridfs
 cfs:gridfs
-browser-policy
-eluck:accounts-lockout
+rzymek:fullcalendar
+momentjs:moment@2.22.2
+browser-policy-framing
+mquandalle:moment
+msavin:usercache
+wekan-scrollbar
+mquandalle:perfect-scrollbar
+mdg:meteor-apm-agent
+meteorhacks:unblock
+lucasantoniassi:accounts-lockout
+wekan-markdown

+ 16 - 5
.meteor/versions

@@ -1,5 +1,6 @@
 3stack:presence@1.1.2
 3stack:presence@1.1.2
 accounts-base@1.4.0
 accounts-base@1.4.0
+accounts-oauth@1.1.15
 accounts-password@1.5.0
 accounts-password@1.5.0
 aldeed:collection2@2.10.0
 aldeed:collection2@2.10.0
 aldeed:collection2-core@1.2.0
 aldeed:collection2-core@1.2.0
@@ -18,9 +19,7 @@ binary-heap@1.0.10
 blaze@2.3.2
 blaze@2.3.2
 blaze-tools@1.0.10
 blaze-tools@1.0.10
 boilerplate-generator@1.3.1
 boilerplate-generator@1.3.1
-browser-policy@1.1.0
 browser-policy-common@1.0.11
 browser-policy-common@1.0.11
-browser-policy-content@1.1.0
 browser-policy-framing@1.1.0
 browser-policy-framing@1.1.0
 caching-compiler@1.1.9
 caching-compiler@1.1.9
 caching-html-compiler@1.1.2
 caching-html-compiler@1.1.2
@@ -61,7 +60,6 @@ ecmascript-runtime@0.5.0
 ecmascript-runtime-client@0.5.0
 ecmascript-runtime-client@0.5.0
 ecmascript-runtime-server@0.5.0
 ecmascript-runtime-server@0.5.0
 ejson@1.1.0
 ejson@1.1.0
-eluck:accounts-lockout@0.9.0
 email@1.2.3
 email@1.2.3
 es5-shim@4.6.15
 es5-shim@4.6.15
 fastclick@1.0.13
 fastclick@1.0.13
@@ -83,8 +81,10 @@ launch-screen@1.1.1
 livedata@1.0.18
 livedata@1.0.18
 localstorage@1.2.0
 localstorage@1.2.0
 logging@1.1.19
 logging@1.1.19
+lucasantoniassi:accounts-lockout@1.0.0
 matb33:collection-hooks@0.8.4
 matb33:collection-hooks@0.8.4
 matteodem:easy-search@1.6.4
 matteodem:easy-search@1.6.4
+mdg:meteor-apm-agent@3.1.2
 mdg:validation-error@0.5.1
 mdg:validation-error@0.5.1
 meteor@1.8.2
 meteor@1.8.2
 meteor-base@1.2.0
 meteor-base@1.2.0
@@ -94,6 +94,7 @@ meteorhacks:collection-utils@1.2.0
 meteorhacks:meteorx@1.4.1
 meteorhacks:meteorx@1.4.1
 meteorhacks:picker@1.0.3
 meteorhacks:picker@1.0.3
 meteorhacks:subs-manager@1.6.4
 meteorhacks:subs-manager@1.6.4
+meteorhacks:unblock@1.1.0
 meteorspark:util@0.2.0
 meteorspark:util@0.2.0
 minifier-css@1.2.16
 minifier-css@1.2.16
 minifier-js@2.2.2
 minifier-js@2.2.2
@@ -103,6 +104,7 @@ mixmax:smart-disconnect@0.0.4
 mobile-status-bar@1.0.14
 mobile-status-bar@1.0.14
 modules@0.11.0
 modules@0.11.0
 modules-runtime@0.9.1
 modules-runtime@0.9.1
+momentjs:moment@2.22.2
 mongo@1.3.1
 mongo@1.3.1
 mongo-dev-server@1.1.0
 mongo-dev-server@1.1.0
 mongo-id@1.0.6
 mongo-id@1.0.6
@@ -117,8 +119,11 @@ mquandalle:jquery-ui-drag-drop-sort@0.2.0
 mquandalle:moment@1.0.1
 mquandalle:moment@1.0.1
 mquandalle:mousetrap-bindglobal@0.0.1
 mquandalle:mousetrap-bindglobal@0.0.1
 mquandalle:perfect-scrollbar@0.6.5_2
 mquandalle:perfect-scrollbar@0.6.5_2
+msavin:usercache@1.0.0
 npm-bcrypt@0.9.3
 npm-bcrypt@0.9.3
 npm-mongo@2.2.33
 npm-mongo@2.2.33
+oauth@1.2.1
+oauth2@1.2.0
 observe-sequence@1.0.16
 observe-sequence@1.0.16
 ongoworks:speakingurl@1.1.0
 ongoworks:speakingurl@1.1.0
 ordered-dict@1.0.9
 ordered-dict@1.0.9
@@ -127,7 +132,6 @@ peerlibrary:base-component@0.16.0
 peerlibrary:blaze-components@0.15.1
 peerlibrary:blaze-components@0.15.1
 peerlibrary:computed-field@0.7.0
 peerlibrary:computed-field@0.7.0
 peerlibrary:reactive-field@0.3.0
 peerlibrary:reactive-field@0.3.0
-perak:markdown@1.0.5
 promise@0.10.0
 promise@0.10.0
 raix:eventemitter@0.1.3
 raix:eventemitter@0.1.3
 raix:handlebar-helpers@0.2.5
 raix:handlebar-helpers@0.2.5
@@ -139,6 +143,7 @@ reactive-var@1.0.11
 reload@1.1.11
 reload@1.1.11
 retry@1.0.9
 retry@1.0.9
 routepolicy@1.0.12
 routepolicy@1.0.12
+rzymek:fullcalendar@3.8.0
 service-configuration@1.0.11
 service-configuration@1.0.11
 session@1.1.7
 session@1.1.7
 sha@1.0.9
 sha@1.0.9
@@ -155,7 +160,6 @@ srp@1.0.10
 standard-minifier-css@1.3.5
 standard-minifier-css@1.3.5
 standard-minifier-js@2.2.3
 standard-minifier-js@2.2.3
 staringatlights:fast-render@2.16.5
 staringatlights:fast-render@2.16.5
-staringatlights:flow-router@2.12.2
 staringatlights:inject-data@2.0.5
 staringatlights:inject-data@2.0.5
 stylus@2.513.13
 stylus@2.513.13
 tap:i18n@1.8.2
 tap:i18n@1.8.2
@@ -174,4 +178,11 @@ useraccounts:unstyled@1.14.2
 verron:autosize@3.0.8
 verron:autosize@3.0.8
 webapp@1.4.0
 webapp@1.4.0
 webapp-hashing@1.0.9
 webapp-hashing@1.0.9
+wekan-accounts-oidc@1.0.10
+wekan-markdown@1.0.7
+wekan-oidc@1.0.12
+wekan-scrollbar@3.1.3
+wekan-accounts-cas@0.1.0
+wekan-ldap@0.0.2
+yasaricli:slugify@0.0.7
 zimme:active-route@2.3.2
 zimme:active-route@2.3.2

+ 1 - 1
.tx/config

@@ -39,7 +39,7 @@ host = https://www.transifex.com
 # tap:i18n requires us to use `-` separator in the language identifiers whereas
 # tap:i18n requires us to use `-` separator in the language identifiers whereas
 # Transifex uses a `_` separator, without an option to customize it on one side
 # Transifex uses a `_` separator, without an option to customize it on one side
 # or the other, so we need to do a Manual mapping.
 # or the other, so we need to do a Manual mapping.
-lang_map = bg_BG:bg, en_GB:en-GB, es_AR:es-AR, el_GR:el, fi_FI:fi, hu_HU:hu, id_ID:id, mn_MN:mn, no:nb, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, zh_CN:zh-CN, zh_TW:zh-TW
+lang_map = bg_BG:bg, en_GB:en-GB, es_AR:es-AR, el_GR:el, fi_FI:fi, hu_HU:hu, id_ID:id, mn_MN:mn, no:nb, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, zh_CN:zh-CN, zh_TW:zh-TW, zh_HK:zh-HK
 
 
 [wekan.application]
 [wekan.application]
 file_filter = i18n/<lang>.i18n.json
 file_filter = i18n/<lang>.i18n.json

File diff suppressed because it is too large
+ 2145 - 1
CHANGELOG.md


+ 4 - 0
CONTRIBUTING.md

@@ -0,0 +1,4 @@
+To get started, [please sign the Contributor License Agreement](https://www.clahub.com/agreements/wekan/wekan).
+
+[Then, please read documentation at wiki](https://github.com/wekan/wekan/wiki).
+

+ 0 - 5
Contributing.md

@@ -1,5 +0,0 @@
-# Contributing
-
-Please see wiki for all documentation:
-
-<https://github.com/wekan/wekan/wiki>

+ 153 - 38
Dockerfile

@@ -1,42 +1,123 @@
-FROM debian:buster-slim
-MAINTAINER wekan
-
-# Declare Arguments
-ARG NODE_VERSION
-ARG METEOR_RELEASE
-ARG METEOR_EDGE
-ARG USE_EDGE
-ARG NPM_VERSION
-ARG FIBERS_VERSION
-ARG ARCHITECTURE
-ARG SRC_PATH
+FROM ubuntu:disco
+LABEL maintainer="wekan"
 
 
 # Set the environment variables (defaults where required)
 # Set the environment variables (defaults where required)
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # ENV BUILD_DEPS="paxctl"
 # ENV BUILD_DEPS="paxctl"
-ENV BUILD_DEPS="apt-utils gnupg gosu wget curl bzip2 build-essential python git ca-certificates gcc-7"
-ENV NODE_VERSION ${NODE_VERSION:-v8.11.1}
-ENV METEOR_RELEASE ${METEOR_RELEASE:-1.6.0.1}
-ENV USE_EDGE ${USE_EDGE:-false}
-ENV METEOR_EDGE ${METEOR_EDGE:-1.5-beta.17}
-ENV NPM_VERSION ${NPM_VERSION:-5.5.1}
-ENV FIBERS_VERSION ${FIBERS_VERSION:-2.0.0}
-ENV ARCHITECTURE ${ARCHITECTURE:-linux-x64}
-ENV SRC_PATH ${SRC_PATH:-./}
+ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential python3 python3-pip git ca-certificates gcc-8" \
+    DEBUG=false \
+    NODE_VERSION=v8.16.0 \
+    METEOR_RELEASE=1.6.0.1 \
+    USE_EDGE=false \
+    METEOR_EDGE=1.5-beta.17 \
+    NPM_VERSION=latest \
+    FIBERS_VERSION=2.0.0 \
+    ARCHITECTURE=linux-x64 \
+    SRC_PATH=./ \
+    WITH_API=true \
+    ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
+    ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
+    ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
+    ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
+    ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
+    ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
+    EMAIL_NOTIFICATION_TIMEOUT=30000 \
+    MATOMO_ADDRESS="" \
+    MATOMO_SITE_ID="" \
+    MATOMO_DO_NOT_TRACK=true \
+    MATOMO_WITH_USERNAME=false \
+    BROWSER_POLICY_ENABLED=true \
+    TRUSTED_URL="" \
+    WEBHOOKS_ATTRIBUTES="" \
+    OAUTH2_ENABLED=false \
+    OAUTH2_LOGIN_STYLE=redirect \
+    OAUTH2_CLIENT_ID="" \
+    OAUTH2_SECRET="" \
+    OAUTH2_SERVER_URL="" \
+    OAUTH2_AUTH_ENDPOINT="" \
+    OAUTH2_USERINFO_ENDPOINT="" \
+    OAUTH2_TOKEN_ENDPOINT="" \
+    OAUTH2_ID_MAP="" \
+    OAUTH2_USERNAME_MAP="" \
+    OAUTH2_FULLNAME_MAP="" \
+    OAUTH2_EMAIL_MAP="" \
+    LDAP_ENABLE=false \
+    LDAP_PORT=389 \
+    LDAP_HOST="" \
+    LDAP_BASEDN="" \
+    LDAP_LOGIN_FALLBACK=false \
+    LDAP_RECONNECT=true \
+    LDAP_TIMEOUT=10000 \
+    LDAP_IDLE_TIMEOUT=10000 \
+    LDAP_CONNECT_TIMEOUT=10000 \
+    LDAP_AUTHENTIFICATION=false \
+    LDAP_AUTHENTIFICATION_USERDN="" \
+    LDAP_AUTHENTIFICATION_PASSWORD="" \
+    LDAP_LOG_ENABLED=false \
+    LDAP_BACKGROUND_SYNC=false \
+    LDAP_BACKGROUND_SYNC_INTERVAL=100 \
+    LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false \
+    LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false \
+    LDAP_ENCRYPTION=false \
+    LDAP_CA_CERT="" \
+    LDAP_REJECT_UNAUTHORIZED=false \
+    LDAP_USER_SEARCH_FILTER="" \
+    LDAP_USER_SEARCH_SCOPE="" \
+    LDAP_USER_SEARCH_FIELD="" \
+    LDAP_SEARCH_PAGE_SIZE=0 \
+    LDAP_SEARCH_SIZE_LIMIT=0 \
+    LDAP_GROUP_FILTER_ENABLE=false \
+    LDAP_GROUP_FILTER_OBJECTCLASS="" \
+    LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE="" \
+    LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE="" \
+    LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT="" \
+    LDAP_GROUP_FILTER_GROUP_NAME="" \
+    LDAP_UNIQUE_IDENTIFIER_FIELD="" \
+    LDAP_UTF8_NAMES_SLUGIFY=true \
+    LDAP_USERNAME_FIELD="" \
+    LDAP_FULLNAME_FIELD="" \
+    LDAP_MERGE_EXISTING_USERS=false \
+    LDAP_EMAIL_FIELD="" \
+    LDAP_EMAIL_MATCH_ENABLE=false \
+    LDAP_EMAIL_MATCH_REQUIRE=false \
+    LDAP_EMAIL_MATCH_VERIFIED=false \
+    LDAP_SYNC_USER_DATA=false \
+    LDAP_SYNC_USER_DATA_FIELDMAP="" \
+    LDAP_SYNC_GROUP_ROLES="" \
+    LDAP_DEFAULT_DOMAIN="" \
+    LDAP_SYNC_ADMIN_STATUS="" \
+    LDAP_SYNC_ADMIN_GROUPS="" \
+    HEADER_LOGIN_ID="" \
+    HEADER_LOGIN_FIRSTNAME="" \
+    HEADER_LOGIN_LASTNAME="" \
+    HEADER_LOGIN_EMAIL="" \
+    LOGOUT_WITH_TIMER=false \
+    LOGOUT_IN="" \
+    LOGOUT_ON_HOURS="" \
+    LOGOUT_ON_MINUTES="" \
+    CORS="" \
+    DEFAULT_AUTHENTICATION_METHOD=""
 
 
 # Copy the app to the image
 # Copy the app to the image
 COPY ${SRC_PATH} /home/wekan/app
 COPY ${SRC_PATH} /home/wekan/app
 
 
 RUN \
 RUN \
+    set -o xtrace && \
     # Add non-root user wekan
     # Add non-root user wekan
     useradd --user-group --system --home-dir /home/wekan wekan && \
     useradd --user-group --system --home-dir /home/wekan wekan && \
     \
     \
     # OS dependencies
     # OS dependencies
     apt-get update -y && apt-get install -y --no-install-recommends ${BUILD_DEPS} && \
     apt-get update -y && apt-get install -y --no-install-recommends ${BUILD_DEPS} && \
+    pip3 install -U pip setuptools wheel && \
+    \
+    # Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
+    # https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
+    cp $(which tar) $(which tar)~ && \
+    ln -sf $(which bsdtar) $(which tar) && \
     \
     \
     # Download nodejs
     # Download nodejs
-    #wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
-    #wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
+    wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
+    wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
     #---------------------------------------------------------------------------------------------
     #---------------------------------------------------------------------------------------------
     # Node Fibers 100% CPU usage issue:
     # Node Fibers 100% CPU usage issue:
     # https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
     # https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
@@ -45,11 +126,10 @@ RUN \
     # Also see beginning of wekan/server/authentication.js
     # Also see beginning of wekan/server/authentication.js
     #   import Fiber from "fibers";
     #   import Fiber from "fibers";
     #   Fiber.poolSize = 1e9;
     #   Fiber.poolSize = 1e9;
-    # Download node version 8.11.1 that has fix included, node binary copied from Sandstorm
+    # OLD: Download node version 8.12.0 prerelease that has fix included, => Official 8.12.0 has been released
     # Description at https://releases.wekan.team/node.txt
     # Description at https://releases.wekan.team/node.txt
-    # SHA256SUM: 18c99d5e79e2fe91e75157a31be30e5420787213684d4048eb91e602e092725d
-    wget https://releases.wekan.team/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
-    echo "c85ed210a360c50d55baaf7b49419236e5241515ed21410d716f4c1f5deedb12  node-v8.11.1-linux-x64.tar.gz" >> SHASUMS256.txt.asc && \
+    #wget https://releases.wekan.team/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
+    #echo "1ed54adb8497ad8967075a0b5d03dd5d0a502be43d4a4d84e5af489c613d7795  node-v8.12.0-linux-x64.tar.gz" >> SHASUMS256.txt.asc && \
     \
     \
     # Verify nodejs authenticity
     # Verify nodejs authenticity
     grep ${NODE_VERSION}-${ARCHITECTURE}.tar.gz SHASUMS256.txt.asc | shasum -a 256 -c - && \
     grep ${NODE_VERSION}-${ARCHITECTURE}.tar.gz SHASUMS256.txt.asc | shasum -a 256 -c - && \
@@ -96,8 +176,11 @@ RUN \
     # Change user to wekan and install meteor
     # Change user to wekan and install meteor
     cd /home/wekan/ && \
     cd /home/wekan/ && \
     chown wekan:wekan --recursive /home/wekan && \
     chown wekan:wekan --recursive /home/wekan && \
-    curl https://install.meteor.com -o /home/wekan/install_meteor.sh && \
-    sed -i "s|RELEASE=.*|RELEASE=${METEOR_RELEASE}\"\"|g" ./install_meteor.sh && \
+    curl "https://install.meteor.com" -o /home/wekan/install_meteor.sh && \
+    #curl "https://install.meteor.com/?release=${METEOR_RELEASE}" -o /home/wekan/install_meteor.sh && \
+    # OLD: sed -i "s|RELEASE=.*|RELEASE=${METEOR_RELEASE}\"\"|g" ./install_meteor.sh && \
+    # Install Meteor forcing its progress
+    sed -i 's/VERBOSITY="--silent"/VERBOSITY="--progress-bar"/' ./install_meteor.sh && \
     echo "Starting meteor ${METEOR_RELEASE} installation...   \n" && \
     echo "Starting meteor ${METEOR_RELEASE} installation...   \n" && \
     chown wekan:wekan /home/wekan/install_meteor.sh && \
     chown wekan:wekan /home/wekan/install_meteor.sh && \
     \
     \
@@ -109,40 +192,72 @@ RUN \
     fi; \
     fi; \
     \
     \
     # Get additional packages
     # Get additional packages
-    mkdir -p /home/wekan/app/packages && \
+    #mkdir -p /home/wekan/app/packages && \
     chown wekan:wekan --recursive /home/wekan && \
     chown wekan:wekan --recursive /home/wekan && \
-    cd /home/wekan/app/packages && \
-    gosu wekan:wekan git clone --depth 1 -b master git://github.com/wekan/flow-router.git kadira-flow-router && \
-    gosu wekan:wekan git clone --depth 1 -b master git://github.com/meteor-useraccounts/core.git meteor-useraccounts-core && \
+    # REPOS BELOW ARE INCLUDED TO WEKAN REPO
+    #cd /home/wekan/app/packages && \
+    #gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router && \
+    #gosu wekan:wekan git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core && \
+    #gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git && \
+    #gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git && \
+    #gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git && \
+    #gosu wekan:wekan git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git && \
+    #gosu wekan:wekan git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git && \
+    #gosu wekan:wekan mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan-accounts-oidc && \
+    #gosu wekan:wekan mv meteor-accounts-oidc/packages/switch_oidc wekan-oidc && \
+    #gosu wekan:wekan rm -rf meteor-accounts-oidc && \
     sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js && \
     sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js && \
     cd /home/wekan/.meteor && \
     cd /home/wekan/.meteor && \
     gosu wekan:wekan /home/wekan/.meteor/meteor -- help; \
     gosu wekan:wekan /home/wekan/.meteor/meteor -- help; \
     \
     \
+    # extract the OpenAPI specification
+    npm install -g api2html@0.3.0 && \
+    mkdir -p /home/wekan/python && \
+    chown wekan:wekan --recursive /home/wekan/python && \
+    cd /home/wekan/python && \
+    gosu wekan:wekan git clone --depth 1 -b master https://github.com/Kronuz/esprima-python && \
+    cd /home/wekan/python/esprima-python && \
+    python3 setup.py install --record files.txt && \
+    cd /home/wekan/app &&\
+    mkdir -p ./public/api && \
+    python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml && \
+    /opt/nodejs/bin/api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml; \
     # Build app
     # Build app
     cd /home/wekan/app && \
     cd /home/wekan/app && \
     gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
     gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
     gosu wekan:wekan /home/wekan/.meteor/meteor npm install && \
     gosu wekan:wekan /home/wekan/.meteor/meteor npm install && \
     gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
     gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
     cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
     cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
+    rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
     chown wekan:wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
     chown wekan:wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
-    cd /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt && \
-    gosu wekan:wekan rm -rf node_modules/bcrypt && \
-    gosu wekan:wekan npm install bcrypt && \
+    #Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
+    #https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
+    #https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
+    #cd /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt && \
+    #gosu wekan:wekan rm -rf node_modules/bcrypt && \
+    #gosu wekan:wekan npm install bcrypt && \
     cd /home/wekan/app_build/bundle/programs/server/ && \
     cd /home/wekan/app_build/bundle/programs/server/ && \
     gosu wekan:wekan npm install && \
     gosu wekan:wekan npm install && \
-    gosu wekan:wekan npm install bcrypt && \
+    #gosu wekan:wekan npm install bcrypt && \
     mv /home/wekan/app_build/bundle /build && \
     mv /home/wekan/app_build/bundle /build && \
     \
     \
+    # Put back the original tar
+    mv $(which tar)~ $(which tar) && \
+    \
     # Cleanup
     # Cleanup
     apt-get remove --purge -y ${BUILD_DEPS} && \
     apt-get remove --purge -y ${BUILD_DEPS} && \
     apt-get autoremove -y && \
     apt-get autoremove -y && \
+    npm uninstall -g api2html &&\
     rm -R /var/lib/apt/lists/* && \
     rm -R /var/lib/apt/lists/* && \
     rm -R /home/wekan/.meteor && \
     rm -R /home/wekan/.meteor && \
     rm -R /home/wekan/app && \
     rm -R /home/wekan/app && \
     rm -R /home/wekan/app_build && \
     rm -R /home/wekan/app_build && \
+    cat /home/wekan/python/esprima-python/files.txt | xargs rm -R && \
+    rm -R /home/wekan/python && \
     rm /home/wekan/install_meteor.sh
     rm /home/wekan/install_meteor.sh
 
 
-ENV PORT=80
+ENV PORT=8080
 EXPOSE $PORT
 EXPOSE $PORT
+USER wekan
 
 
 CMD ["node", "/build/main.js"]
 CMD ["node", "/build/main.js"]

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 The MIT License (MIT)
 The MIT License (MIT)
 
 
-Copyright (c) 2014-2018 The Wekan Team
+Copyright (c) 2014-2019 The Wekan Team
 
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 of this software and associated documentation files (the "Software"), to deal

+ 79 - 43
README.md

@@ -1,9 +1,4 @@
-# Wekan
-
-[![Translate Wekan at Transifex](https://img.shields.io/badge/Translate%20Wekan-at%20Transifex-brightgreen.svg "Freenode IRC")](https://transifex.com/wekan/wekan)
-
-[![Wekan Vanila Chat][vanila_badge]][vanila_chat] 
-[![IRC #wekan](https://img.shields.io/badge/IRC%20%23wekan-on%20Freenode-brightgreen.svg "Freenode IRC")](http://webchat.freenode.net?channels=%23wekan&uio=d4)
+# Wekan - Open Source kanban
 
 
 [![Contributors](https://img.shields.io/github/contributors/wekan/wekan.svg "Contributors")](https://github.com/wekan/wekan/graphs/contributors)
 [![Contributors](https://img.shields.io/github/contributors/wekan/wekan.svg "Contributors")](https://github.com/wekan/wekan/graphs/contributors)
 [![Docker Repository on Quay](https://quay.io/repository/wekan/wekan/status "Docker Repository on Quay")](https://quay.io/repository/wekan/wekan)
 [![Docker Repository on Quay](https://quay.io/repository/wekan/wekan/status "Docker Repository on Quay")](https://quay.io/repository/wekan/wekan)
@@ -14,54 +9,85 @@
 [![Code Climate](https://codeclimate.com/github/wekan/wekan/badges/gpa.svg "Code Climate")](https://codeclimate.com/github/wekan/wekan)
 [![Code Climate](https://codeclimate.com/github/wekan/wekan/badges/gpa.svg "Code Climate")](https://codeclimate.com/github/wekan/wekan)
 [![Project Dependencies](https://david-dm.org/wekan/wekan.svg "Project Dependencies")](https://david-dm.org/wekan/wekan)
 [![Project Dependencies](https://david-dm.org/wekan/wekan.svg "Project Dependencies")](https://david-dm.org/wekan/wekan)
 [![Code analysis at Open Hub](https://img.shields.io/badge/code%20analysis-at%20Open%20Hub-brightgreen.svg "Code analysis at Open Hub")](https://www.openhub.net/p/wekan)
 [![Code analysis at Open Hub](https://img.shields.io/badge/code%20analysis-at%20Open%20Hub-brightgreen.svg "Code analysis at Open Hub")](https://www.openhub.net/p/wekan)
+[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fwekan%2Fwekan.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_shield)
 
 
-Please read [FAQ](https://github.com/wekan/wekan/wiki/FAQ).
-Please don't feed the trolls and spammers that are mentioned in the FAQ :)
+## [Translate Wekan at Transifex](https://transifex.com/wekan/wekan)
 
 
-Wekan is an completely [Open Source][open_source] and [Free software][free_software]
-collaborative kanban board application with MIT license.
+Translations to non-English languages are accepted only at [Transifex](https://transifex.com/wekan/wekan) using webbrowser.
+New English strings of new features can be added as PRs to edge branch file wekan/i18n/en.i18n.json .
 
 
-Whether you’re maintaining a personal todo list, planning your holidays with
-some friends, or working in a team on your next revolutionary idea, Kanban
-boards are an unbeatable tool to keep your things organized. They give you a
-visual overview of the current state of your project, and make you productive by
-allowing you to focus on the few items that matter the most.
+## [Wekan feature requests and bugs](https://github.com/wekan/wekan/issues)
 
 
-Wekan has real-time user interface. Not all features are implemented.
+Please add most of your questions as GitHub issue: [Wekan feature requests and bugs](https://github.com/wekan/wekan/issues).
+It's better than at chat where details get lost when chat scrolls up.
 
 
-[Features][features]
+## Chat
 
 
-Wekan supports many [Platforms][platforms], and plan is to add more.
+[![Wekan Chat][vanila_badge]][wekan_chat] - Most Wekan community and developers are here. Works on webbrowser
+and PWA app that can be added as icon on Android and bookmark on iOS, used like native app.
 
 
-[Integrations][integrations]
+[Wekan IRC FAQ](https://github.com/wekan/wekan/wiki/IRC-FAQ)
 
 
-[Team](https://github.com/wekan/wekan/wiki/Team)
+## FAQ
 
 
-You don’t have to trust us with your data and can install Wekan on your own
-computer or server. In fact we encourage you to do that by providing
-one-click installation on various platforms.
+**NOTE**: 
+- Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
+- Please don't feed the trolls and spammers that are mentioned in the FAQ :)
 
 
-## Roadmap
+## About Wekan
+
+Wekan is an completely [Open Source][open_source] and [Free software][free_software]
+collaborative kanban board application with MIT license.
 
 
-[Roadmap](https://github.com/wekan/wekan/wiki/Roadmap)
+Whether you’re maintaining a personal todo list, planning your holidays with some friends,
+or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool
+to keep your things organized. They give you a visual overview of the current state of your project,
+and make you productive by allowing you to focus on the few items that matter the most.
 
 
-Upcoming Wekan App Development Platform will make possible
-many use cases. If you don't find your feature or integration in
-GitHub issues and [Features][features] or [Integrations][integrations]
-page at wiki, please add them.
+Since Wekan is a free software, you don’t have to trust us with your data and can
+install Wekan on your own computer or server. In fact we encourage you to do
+that by providing one-click installation on various platforms.
 
 
-We are very welcoming to new developers and teams to submit new pull
-requests to devel branch to make this Wekan App Development Platform possible
-faster. Please see [Developer Documentation][dev_docs] to get started.
-We also welcome sponsors for features, although we don't have any yet.
-By working directly with Wekan you get the benefit of active maintenance
-and new features added by growing Wekan developer community.
+- Wekan is used in [most countries of the world](https://snapcraft.io/wekan).
+- Wekan largest user has 13k users using Wekan in their company.
+- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 50 languages.
+- [Features][features]: Wekan has real-time user interface.
+- [Platforms][platforms]: Wekan supports many platforms.
+  Wekan is critical part of new platforms Wekan is currently being integrated to.
+- [Integrations][integrations]: Current possible integrations and future plans.
+
+## Requirements
+
+- 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.io) /
+  [Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows).
+  [More Platforms](https://github.com/wekan/wekan/wiki/Platforms). [ARM progress](https://github.com/wekan/wekan/issues/1053#issuecomment-410919264).
+- 1 GB RAM minimum free for Wekan. Production server should have miminum total 4 GB RAM.
+  For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/devel/docker-compose.yml): 3 frontend servers,
+  each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.  
+- Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
+- SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
+  Old versions have security issues because of old versions Node.js etc. Only newest Wekan is supported.
+  Wekan on Sandstorm is not usually affected by any Standalone Wekan (Snap/Docker/Source) security issues.
+- [Reporting all new bugs immediately](https://github.com/wekan/wekan/issues).
+  New features and fixes are added to Wekan [many times a day](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md).
+- [Backups](https://github.com/wekan/wekan/wiki/Backup) of Wekan database once a day miminum.
+  Bugs, updates, users deleting list or card, harddrive full, harddrive crash etc can eat your data. There is no undo yet.
+  Some bug can cause Wekan board to not load at all, requiring manual fixing of database content.
 
 
-Actual work happens at [Wekan GitHub issues][wekan_issues].
+## Roadmap
 
 
-See [Development links on Wekan
-wiki](https://github.com/wekan/wekan/wiki#Development)
-bottom of the page for more info.
+[Roadmap Milestones](https://github.com/wekan/wekan/milestones)
+
+[Developer Documentation][dev_docs]
+
+- There is many companies and individuals contributing code to Wekan, to add features and bugfixes
+  [many times a day](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md).
+- [Please add Add new Feature Requests and Bug Reports immediately](https://github.com/wekan/wekan/issues).
+- [Commercial Support](https://wekan.team).
+- [Bounties](https://wekan.team/bounties/index.html).
+
+We also welcome sponsors for features and bugfixes.
+By working directly with Wekan you get the benefit of active maintenance and new features added by growing Wekan developer community.
 
 
 ## Demo
 ## Demo
 
 
@@ -73,9 +99,16 @@ bottom of the page for more info.
 
 
 [![Screenshot of Wekan][screenshot_wefork]][roadmap_wefork]
 [![Screenshot of Wekan][screenshot_wefork]][roadmap_wefork]
 
 
-Since Wekan is a free software, you don’t have to trust us with your data and can
-install Wekan on your own computer or server. In fact we encourage you to do
-that by providing one-click installation on various platforms.
+## Stable
+
+- master+devel branch. At release, devel is merged to master.
+- Receives fixes and features that have been tested at edge that they work.
+- If you want automatic updates, [use Snap](https://github.com/wekan/wekan-snap/wiki/Install).
+- If you want to test before update, [use Docker quay.io release tags](https://github.com/wekan/wekan/wiki/Docker).
+
+## Edge
+
+- edge branch. All new fixes and features are added to here first. [Testing Edge](https://github.com/wekan/wekan-snap/wiki/Snap-Developer-Docs).
 
 
 ## License
 ## License
 
 
@@ -100,4 +133,7 @@ with [Meteor](https://www.meteor.com).
 [open_source]: https://en.wikipedia.org/wiki/Open-source_software
 [open_source]: https://en.wikipedia.org/wiki/Open-source_software
 [free_software]: https://en.wikipedia.org/wiki/Free_software
 [free_software]: https://en.wikipedia.org/wiki/Free_software
 [vanila_badge]: https://vanila.io/img/join-chat-button2.png
 [vanila_badge]: https://vanila.io/img/join-chat-button2.png
-[vanila_chat]: https://chat.vanila.io/channel/wekan
+[wekan_chat]: https://community.vanila.io/wekan
+
+
+[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fwekan%2Fwekan.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_large)

+ 129 - 0
SECURITY.md

@@ -0,0 +1,129 @@
+Security is very important to us. If you discover any issue regarding security, please disclose
+the information responsibly by sending an email to security (at) wekan.team and not by
+creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
+
+We thank you with a place at our hall of fame page, that is
+at https://wekan.github.io/hall-of-fame . Others have just posted public GitHub issue,
+so they are not at that hall-of-fame page.
+
+## How should reports be formatted?
+
+```
+Name: %name
+Twitter: %twitter
+Bug type: %bugtype
+Domain: %domain
+Severity: %severity
+URL: %url
+PoC: %poc
+CVSS (optional): %cvss
+CWSS (optional): %cwss
+```
+
+## Who can participate in the program
+
+Anyone who reports a unique security issue in scope and does not disclose it to
+a third party before we have patched and updated may be upon their approval
+added to the Wekan Hall of Fame.
+
+## Which domains are in scope?
+
+No public domains, because all those are donated to Wekan Open Source project,
+and we don't have any permissions to do security scans on those donated servers
+
+Please don't perform research that could impact other users. Secondly, please keep
+the reports short and succinct. If we fail to understand the logics of your bug, we will tell you.
+
+You can [Install Wekan](https://github.com/wekan/wekan/releases) to your own computer
+and scan it's vulnerabilities there.
+
+## About Wekan versions
+
+There are only 2 versions of Wekan: Standalone Wekan, and Sandstorm Wekan.
+
+### Standalone Wekan Security
+
+Standalone Wekan includes all non-Sandstorm platforms. Some Standalone Wekan platforms
+like Snap and Docker have their own specific sandboxing etc features.
+
+Standalone Wekan by default does not load any files from Internet, like fonts, CSS, etc.
+This also means all Standalone Wekan functionality works in offline local networks.
+Wekan is used by companies that have [thousands of users](https://github.com/wekan/wekan/wiki/AWS) and at healthcare.
+
+Wekan uses xss package for input fields like cards, as you can see from
+[package.json](https://github.com/wekan/wekan/blob/devel/package.json). Other used versions can be seen from
+[Meteor versions file](https://github.com/wekan/wekan/blob/devel/.meteor/versions).
+Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
+It's possible to add attachments to cards, and markdown/html links to files.
+
+Wekan attachments are not accessible without logging in. Import from Trello works by copying
+Trello export JSON to Wekan Trello import page, and in Trello JSON file there is direct links to all publicly
+accessible Trello attachment files, that Standalone Wekan downloads directly to Wekan MongoDB database in
+[CollectionFS](https://github.com/wekan/wekan/pull/875) format. When Wekan board is exported in
+Wekan JSON format, all board attachments are included in Wekan JSON file as base64 encoded text.
+That Wekan JSON format file can be imported to Sandstorm Wekan with all the attachments, when we get
+latest Wekan version working on Sandstorm, only couple of bugs are left before that. In Sandstorm it's not
+possible yet to import from Trello with attachments, because Wekan does not implement Sandstorm-compatible
+access to outside of Wekan grain.
+
+Standalone Wekan only has password auth currently, there is work in progress to add
+[oauth2](https://github.com/wekan/wekan/pull/1578), [Openid](https://github.com/wekan/wekan/issues/538),
+[LDAP](https://github.com/wekan/wekan/issues/119) etc. If you need more login security for Standalone Wekan now,
+it's possible add additional [Google Auth proxybouncer](https://github.com/wekan/wekan/wiki/Let's-Encrypt-and-Google-Auth) in front of password auth, and then use Google Authenticator for Google Auth. Standalone Wekan does have [brute force protection with eluck:accounts-lockout and browser-policy clickjacking protection](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md#v080-2018-04-04-wekan-release). You can also optionally use some [WAF](https://en.wikipedia.org/wiki/Web_application_firewall)
+like for example [AWS WAF](https://aws.amazon.com/waf/).
+
+[All Wekan Platforms](https://github.com/wekan/wekan/wiki/Platforms)
+
+### Sandstorm Wekan Security
+
+On Sandstorm platform using environment variable Standalone Wekan features like Admin Panel etc are
+turned off, because Sandstorm platform provides SSO for all apps running on Sandstorm. 
+
+[Sandstorm](https://sandstorm.io) is separate Open Source platform that has been
+[security audited](https://sandstorm.io/news/2017-03-02-security-review) and found bugs fixed.
+Sandstorm also has passwordless login, LDAP, SAML, Google etc auth options already.
+At Sandstorm code is read-only and signed by app maintainers, only grain content can be modified.
+Wekan at Sandstorm runs in sandboxed grain, it does not have access elsewhere without user-visible
+PowerBox request or opening randomly-generated API key URL.
+Also read [Sandstorm Security Practices](https://docs.sandstorm.io/en/latest/using/security-practices/) and
+[Sandstorm Security non-events](https://docs.sandstorm.io/en/latest/using/security-non-events/).
+For Sandstorm specific security issues you can contact [kentonv](https://github.com/kentonv) by email. 
+
+## What Wekan bugs are eligible?
+
+Any typical web security bugs. If any of the previously mentioned is somehow problematic and
+a security issue, we'd like to know about it, and also how to fix it:
+
+- Cross-site Scripting
+- Open redirect
+- Cross-site request forgery
+- File inclusion
+- Authentication bypass
+- Server-side code execution
+
+## What Wekan bugs are NOT eligible?
+
+Typical already known or "no impact" bugs such as:
+
+- Brute force password guessign. Currently there is
+  [brute force protection with eluck:accounts-lockout](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md#v080-2018-04-04-wekan-release).
+- Security issues related to that Wekan uses Meteor 1.6.0.1 related packages, and upgrading to newer
+  Meteor 1.6.1 is complicated process that requires lots of changes to many dependency packages.
+  Upgrading [has been tried many times, spending a lot of time](https://github.com/meteor/meteor/issues/9609)
+  but there still is issues. Helping with package upgrades is very welcome.
+- [Wekan API old tokens not replaced correctly](https://github.com/wekan/wekan/issues/1437)
+- Missing Cookie flags on non-session cookies or 3rd party cookies
+- Logout CSRF
+- Social engineering
+- Denial of service
+- SSL BEAST/CRIME/etc. Wekan does not have SSL built-in, it uses Caddy/Nginx/Apache etc at front.
+  Integrated Caddy support is updated often.
+- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
+
+Wekan is Open Source with MIT license, and free to use also for commercial use.
+We welcome all fixes to improve security by email to security (at) wekan.team .
+
+## Bonus Points
+
+If your Responsible Security Disclosure includes code for fixing security issue,
+you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).

+ 9 - 0
Stackerfile.yml

@@ -0,0 +1,9 @@
+appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
+appVersion: "v2.66.0"
+files:
+  userUploads:
+    - README.md
+  userScripts:
+    build: stacksmith/user-scripts/build.sh
+    boot: stacksmith/user-scripts/boot.sh
+    run: stacksmith/user-scripts/run.sh

+ 1 - 1
app.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Wekan",
   "name": "Wekan",
-  "description": "The open-source Trello-like kanban",
+  "description": "The open-source kanban",
   "repository": "https://github.com/wekan/wekan",
   "repository": "https://github.com/wekan/wekan",
   "logo": "https://raw.githubusercontent.com/wekan/wekan/master/meta/icons/wekan-150.png",
   "logo": "https://raw.githubusercontent.com/wekan/wekan/master/meta/icons/wekan-150.png",
   "keywords": ["productivity", "tool", "team", "kanban"],
   "keywords": ["productivity", "tool", "team", "kanban"],

+ 68 - 1
client/components/activities/activities.jade

@@ -14,6 +14,9 @@ template(name="boardActivities")
       p.activity-desc
       p.activity-desc
         +memberName(user=user)
         +memberName(user=user)
 
 
+        if($eq activityType 'deleteAttachment')
+          | {{{_ 'activity-delete-attach' cardLink}}}.
+
         if($eq activityType 'addAttachment')
         if($eq activityType 'addAttachment')
           | {{{_ 'activity-attached' attachmentLink cardLink}}}.
           | {{{_ 'activity-attached' attachmentLink cardLink}}}.
 
 
@@ -31,12 +34,28 @@ template(name="boardActivities")
           .activity-checklist(href="{{ card.absoluteUrl }}")
           .activity-checklist(href="{{ card.absoluteUrl }}")
             +viewer
             +viewer
               = checklist.title
               = checklist.title
+        if($eq activityType 'removeChecklist')
+          | {{{_ 'activity-checklist-removed' cardLink}}}.
+
+        if($eq activityType 'checkedItem')
+          | {{{_ 'activity-checked-item' checkItem checklist.title cardLink}}}.
+
+        if($eq activityType 'uncheckedItem')
+          | {{{_ 'activity-unchecked-item' checkItem checklist.title cardLink}}}.
+
+        if($eq activityType 'checklistCompleted')
+          | {{{_ 'activity-checklist-completed' checklist.title cardLink}}}.
+
+        if($eq activityType 'checklistUncompleted')
+          | {{{_ 'activity-checklist-uncompleted' checklist.title cardLink}}}.
 
 
         if($eq activityType 'addChecklistItem')
         if($eq activityType 'addChecklistItem')
           | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
           | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
           .activity-checklist(href="{{ card.absoluteUrl }}")
           .activity-checklist(href="{{ card.absoluteUrl }}")
             +viewer
             +viewer
               = checklistItem.title
               = checklistItem.title
+        if($eq activityType 'removedChecklistItem')
+          | {{{_ 'activity-checklist-item-removed' checklist.title cardLink}}}.
 
 
         if($eq activityType 'archivedCard')
         if($eq activityType 'archivedCard')
           | {{{_ 'activity-archived' cardLink}}}.
           | {{{_ 'activity-archived' cardLink}}}.
@@ -53,6 +72,9 @@ template(name="boardActivities")
         if($eq activityType 'createCard')
         if($eq activityType 'createCard')
           | {{{_ 'activity-added' cardLink boardLabel}}}.
           | {{{_ 'activity-added' cardLink boardLabel}}}.
 
 
+        if($eq activityType 'createCustomField')
+          | {{_ 'activity-customfield-created' customField}}.
+
         if($eq activityType 'createList')
         if($eq activityType 'createList')
           | {{_ 'activity-added' list.title boardLabel}}.
           | {{_ 'activity-added' list.title boardLabel}}.
 
 
@@ -77,6 +99,9 @@ template(name="boardActivities")
           else
           else
             | {{{_ 'activity-added' memberLink cardLink}}}.
             | {{{_ 'activity-added' memberLink cardLink}}}.
 
 
+        if($eq activityType 'moveCardBoard')
+          | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
+
         if($eq activityType 'moveCard')
         if($eq activityType 'moveCard')
           | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
           | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
 
 
@@ -86,6 +111,18 @@ template(name="boardActivities")
         if($eq activityType 'restoredCard')
         if($eq activityType 'restoredCard')
           | {{{_ 'activity-sent' cardLink boardLabel}}}.
           | {{{_ 'activity-sent' cardLink boardLabel}}}.
 
 
+        if($eq activityType 'addedLabel')
+          | {{{_ 'activity-added-label' lastLabel cardLink}}}.
+
+        if($eq activityType 'removedLabel')
+          | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
+
+        if($eq activityType 'setCustomField')
+          | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
+
+        if($eq activityType 'unsetCustomField')
+          | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
+
         if($eq activityType 'unjoinMember')
         if($eq activityType 'unjoinMember')
           if($eq user._id member._id)
           if($eq user._id member._id)
             | {{{_ 'activity-unjoined' cardLink}}}.
             | {{{_ 'activity-unjoined' cardLink}}}.
@@ -101,7 +138,7 @@ template(name="cardActivities")
       p.activity-desc
       p.activity-desc
         +memberName(user=user)
         +memberName(user=user)
         if($eq activityType 'createCard')
         if($eq activityType 'createCard')
-          | {{_ 'activity-added' cardLabel list.title}}.
+          | {{_ 'activity-added' cardLabel listName}}.
         if($eq activityType 'importCard')
         if($eq activityType 'importCard')
           | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
           | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
         if($eq activityType 'joinMember')
         if($eq activityType 'joinMember')
@@ -116,14 +153,44 @@ template(name="cardActivities")
             | {{{_ 'activity-removed' cardLabel memberLink}}}.
             | {{{_ 'activity-removed' cardLabel memberLink}}}.
         if($eq activityType 'archivedCard')
         if($eq activityType 'archivedCard')
           | {{_ 'activity-archived' cardLabel}}.
           | {{_ 'activity-archived' cardLabel}}.
+
+        if($eq activityType 'addedLabel')
+          | {{{_ 'activity-added-label-card' lastLabel }}}.
+
+        if($eq activityType 'removedLabel')
+          | {{{_ 'activity-removed-label-card' lastLabel }}}.
+
+        if($eq activityType 'removeChecklist')
+          | {{{_ 'activity-checklist-removed' cardLabel}}}.
+
+        if($eq activityType 'checkedItem')
+          | {{{_ 'activity-checked-item-card' checkItem checklist.title }}}.
+
+        if($eq activityType 'uncheckedItem')
+          | {{{_ 'activity-unchecked-item-card' checkItem checklist.title }}}.
+
+        if($eq activityType 'checklistCompleted')
+          | {{{_ 'activity-checklist-completed-card' checklist.title }}}.
+
+        if($eq activityType 'checklistUncompleted')
+          | {{{_ 'activity-checklist-uncompleted-card' checklist.title }}}.
+
         if($eq activityType 'restoredCard')
         if($eq activityType 'restoredCard')
           | {{_ 'activity-sent' cardLabel boardLabel}}.
           | {{_ 'activity-sent' cardLabel boardLabel}}.
         if($eq activityType 'moveCard')
         if($eq activityType 'moveCard')
           | {{_ 'activity-moved' cardLabel oldList.title list.title}}.
           | {{_ 'activity-moved' cardLabel oldList.title list.title}}.
+
+        if($eq activityType 'moveCardBoard')
+          | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
+
         if($eq activityType 'addAttachment')
         if($eq activityType 'addAttachment')
           | {{{_ 'activity-attached' attachmentLink cardLabel}}}.
           | {{{_ 'activity-attached' attachmentLink cardLabel}}}.
           if attachment.isImage
           if attachment.isImage
             img.attachment-image-preview(src=attachment.url)
             img.attachment-image-preview(src=attachment.url)
+        if($eq activityType 'deleteAttachment')
+          | {{{_ 'activity-delete-attach'  cardLabel}}}.
+        if($eq activityType 'removedChecklist')
+          | {{{_ 'activity-checklist-removed' cardLabel}}}.
         if($eq activityType 'addChecklist')
         if($eq activityType 'addChecklist')
           | {{{_ 'activity-checklist-added' cardLabel}}}.
           | {{{_ 'activity-checklist-added' cardLabel}}}.
           .activity-checklist
           .activity-checklist

+ 59 - 4
client/components/activities/activities.js

@@ -8,16 +8,24 @@ BlazeComponent.extendComponent({
     const sidebar = this.parentComponent(); // XXX for some reason not working
     const sidebar = this.parentComponent(); // XXX for some reason not working
     sidebar.callFirstWith(null, 'resetNextPeak');
     sidebar.callFirstWith(null, 'resetNextPeak');
     this.autorun(() => {
     this.autorun(() => {
-      const mode = this.data().mode;
+      let mode = this.data().mode;
       const capitalizedMode = Utils.capitalize(mode);
       const capitalizedMode = Utils.capitalize(mode);
-      const id = Session.get(`current${capitalizedMode}`);
+      let thisId, searchId;
+      if (mode === 'linkedcard' || mode === 'linkedboard') {
+        thisId = Session.get('currentCard');
+        searchId = Cards.findOne({_id: thisId}).linkedId;
+        mode = mode.replace('linked', '');
+      } else {
+        thisId = Session.get(`current${capitalizedMode}`);
+        searchId = thisId;
+      }
       const limit = this.page.get() * activitiesPerPage;
       const limit = this.page.get() * activitiesPerPage;
       const user = Meteor.user();
       const user = Meteor.user();
       const hideSystem = user ? user.hasHiddenSystemMessages() : false;
       const hideSystem = user ? user.hasHiddenSystemMessages() : false;
-      if (id === null)
+      if (searchId === null)
         return;
         return;
 
 
-      this.subscribe('activities', mode, id, limit, hideSystem, () => {
+      this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
         this.loadNextPageLocked = false;
         this.loadNextPageLocked = false;
 
 
         // If the sibear peak hasn't increased, that mean that there are no more
         // If the sibear peak hasn't increased, that mean that there are no more
@@ -42,6 +50,12 @@ BlazeComponent.extendComponent({
     }
     }
   },
   },
 
 
+  checkItem(){
+    const checkItemId = this.currentData().checklistItemId;
+    const checkItem = ChecklistItems.findOne({_id:checkItemId});
+    return checkItem.title;
+  },
+
   boardLabel() {
   boardLabel() {
     return TAPi18n.__('this-board');
     return TAPi18n.__('this-board');
   },
   },
@@ -58,6 +72,40 @@ BlazeComponent.extendComponent({
     }, card.title));
     }, card.title));
   },
   },
 
 
+  lastLabel(){
+    const lastLabelId = this.currentData().labelId;
+    if (!lastLabelId)
+      return null;
+    const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(lastLabelId);
+    if(lastLabel.name === undefined || lastLabel.name === ''){
+      return lastLabel.color;
+    }else{
+      return lastLabel.name;
+    }
+  },
+
+  lastCustomField(){
+    const lastCustomField = CustomFields.findOne(this.currentData().customFieldId);
+    if (!lastCustomField)
+      return null;
+    return lastCustomField.name;
+  },
+
+  lastCustomFieldValue(){
+    const lastCustomField = CustomFields.findOne(this.currentData().customFieldId);
+    if (!lastCustomField)
+      return null;
+    const value = this.currentData().value;
+    if (lastCustomField.settings.dropdownItems && lastCustomField.settings.dropdownItems.length > 0) {
+      const dropDownValue = _.find(lastCustomField.settings.dropdownItems, (item) => {
+        return item._id === value;
+      });
+      if (dropDownValue)
+        return dropDownValue.name;
+    }
+    return value;
+  },
+
   listLabel() {
   listLabel() {
     return this.currentData().list().title;
     return this.currentData().list().title;
   },
   },
@@ -91,6 +139,13 @@ BlazeComponent.extendComponent({
     }, attachment.name()));
     }, attachment.name()));
   },
   },
 
 
+  customField() {
+    const customField = this.currentData().customField();
+    if (!customField)
+      return null;
+    return customField.name;
+  },
+
   events() {
   events() {
     return [{
     return [{
       // XXX We should use Popup.afterConfirmation here
       // XXX We should use Popup.afterConfirmation here

+ 9 - 2
client/components/activities/comments.js

@@ -21,11 +21,18 @@ BlazeComponent.extendComponent({
       'submit .js-new-comment-form'(evt) {
       'submit .js-new-comment-form'(evt) {
         const input = this.getInput();
         const input = this.getInput();
         const text = input.val().trim();
         const text = input.val().trim();
+        const card = this.currentData();
+        let boardId = card.boardId;
+        let cardId = card._id;
+        if (card.isLinkedCard()) {
+          boardId = Cards.findOne(card.linkedId).boardId;
+          cardId = card.linkedId;
+        }
         if (text) {
         if (text) {
           CardComments.insert({
           CardComments.insert({
             text,
             text,
-            boardId: this.currentData().boardId,
-            cardId: this.currentData()._id,
+            boardId,
+            cardId,
           });
           });
           resetCommentInput(input);
           resetCommentInput(input);
           Tracker.flush();
           Tracker.flush();

+ 12 - 4
client/components/boards/boardArchive.jade

@@ -6,9 +6,17 @@ template(name="archivedBoards")
   ul.archived-lists
   ul.archived-lists
     each archivedBoards
     each archivedBoards
       li.archived-lists-item
       li.archived-lists-item
-        button.js-restore-board
-          i.fa.fa-undo
-          | {{_ 'restore-board'}}
-        = title
+        div.board-header-btns
+          button.board-header-btn.js-delete-board
+            i.fa.fa-trash-o
+            | {{_ 'delete-board'}}
+          button.board-header-btn.js-restore-board
+            i.fa.fa-undo
+            | {{_ 'restore-board'}}
+          = title
     else
     else
       li.no-items-message {{_ 'no-archived-boards'}}
       li.no-items-message {{_ 'no-archived-boards'}}
+
+template(name="boardDeletePopup")
+  p {{_ 'delete-board-confirm-popup'}}
+  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}

+ 11 - 6
client/components/boards/boardArchive.js

@@ -1,9 +1,3 @@
-Template.boardListHeaderBar.events({
-  'click .js-open-archived-board'() {
-    Modal.open('archivedBoards');
-  },
-});
-
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     this.subscribe('archivedBoards');
     this.subscribe('archivedBoards');
@@ -29,6 +23,17 @@ BlazeComponent.extendComponent({
         board.restore();
         board.restore();
         Utils.goBoardId(board._id);
         Utils.goBoardId(board._id);
       },
       },
+      'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
+        Popup.close();
+        const isSandstorm = Meteor.settings && Meteor.settings.public &&
+          Meteor.settings.public.sandstorm;
+        if (isSandstorm && Session.get('currentBoard')) {
+          const currentBoard = Boards.findOne(Session.get('currentBoard'));
+          Boards.remove(currentBoard._id);
+        }
+        Boards.remove(this._id);
+        FlowRouter.go('home');
+      }),
     }];
     }];
   },
   },
 }).register('archivedBoards');
 }).register('archivedBoards');

+ 17 - 3
client/components/boards/boardBody.jade

@@ -20,8 +20,22 @@ template(name="boardBody")
       class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
       class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
       if showOverlay.get
       if showOverlay.get
         .board-overlay
         .board-overlay
-      if isViewSwimlanes
+      if currentBoard.isTemplatesBoard
         each currentBoard.swimlanes
         each currentBoard.swimlanes
           +swimlane(this)
           +swimlane(this)
-      if isViewLists
-        +listsGroup
+      else if isViewSwimlanes
+        each currentBoard.swimlanes
+          +swimlane(this)
+      else if isViewLists
+        +listsGroup(currentBoard)
+      else if isViewCalendar
+        +calendarView
+      else
+        +listsGroup(currentBoard)
+
+template(name="calendarView")
+  if isViewCalendar
+    .calendar-view.swimlane
+      if currentCard
+        +cardDetails(currentCard)
+      +fullcalendar(calendarOptions)

+ 208 - 8
client/components/boards/boardBody.js

@@ -1,5 +1,6 @@
 const subManager = new SubsManager();
 const subManager = new SubsManager();
-const { calculateIndex } = Utils;
+const { calculateIndex, enableClickOnTouch } = Utils;
+const swimlaneWhileSortingHeight = 150;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
@@ -35,6 +36,37 @@ BlazeComponent.extendComponent({
     this._isDragging = false;
     this._isDragging = false;
     // Used to set the overlay
     // Used to set the overlay
     this.mouseHasEnterCardDetails = false;
     this.mouseHasEnterCardDetails = false;
+
+    // fix swimlanes sort field if there are null values
+    const currentBoardData = Boards.findOne(Session.get('currentBoard'));
+    const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
+    if (nullSortSwimlanes.count() > 0) {
+      const swimlanes = currentBoardData.swimlanes();
+      let count = 0;
+      swimlanes.forEach((s) => {
+        Swimlanes.update(s._id, {
+          $set: {
+            sort: count,
+          },
+        });
+        count += 1;
+      });
+    }
+
+    // fix lists sort field if there are null values
+    const nullSortLists = currentBoardData.nullSortLists();
+    if (nullSortLists.count() > 0) {
+      const lists = currentBoardData.lists();
+      let count = 0;
+      lists.forEach((l) => {
+        Lists.update(l._id, {
+          $set: {
+            sort: count,
+          },
+        });
+        count += 1;
+      });
+    }
   },
   },
   onRendered() {
   onRendered() {
     const boardComponent = this;
     const boardComponent = this;
@@ -43,21 +75,64 @@ BlazeComponent.extendComponent({
     $swimlanesDom.sortable({
     $swimlanesDom.sortable({
       tolerance: 'pointer',
       tolerance: 'pointer',
       appendTo: '.board-canvas',
       appendTo: '.board-canvas',
-      helper: 'clone',
+      helper(evt, item) {
+        const helper = $(`<div class="swimlane"
+                               style="flex-direction: column;
+                                      height: ${swimlaneWhileSortingHeight}px;
+                                      width: $(boardComponent.width)px;
+                                      overflow: hidden;"/>`);
+        helper.append(item.clone());
+        // Also grab the list of lists of cards
+        const list = item.next();
+        helper.append(list.clone());
+        return helper;
+      },
       handle: '.js-swimlane-header',
       handle: '.js-swimlane-header',
-      items: '.js-swimlane:not(.placeholder)',
+      items: '.swimlane:not(.placeholder)',
       placeholder: 'swimlane placeholder',
       placeholder: 'swimlane placeholder',
       distance: 7,
       distance: 7,
       start(evt, ui) {
       start(evt, ui) {
+        const listDom = ui.placeholder.next('.js-swimlane');
+        const parentOffset = ui.item.parent().offset();
+
         ui.placeholder.height(ui.helper.height());
         ui.placeholder.height(ui.helper.height());
         EscapeActions.executeUpTo('popup-close');
         EscapeActions.executeUpTo('popup-close');
+        listDom.addClass('moving-swimlane');
         boardComponent.setIsDragging(true);
         boardComponent.setIsDragging(true);
+
+        ui.placeholder.insertAfter(ui.placeholder.next());
+        boardComponent.origPlaceholderIndex = ui.placeholder.index();
+
+        // resize all swimlanes + headers to be a total of 150 px per row
+        // this could be achieved by setIsDragging(true) but we want immediate
+        // result
+        ui.item.siblings('.js-swimlane').css('height', `${swimlaneWhileSortingHeight - 26}px`);
+
+        // set the new scroll height after the resize and insertion of
+        // the placeholder. We want the element under the cursor to stay
+        // at the same place on the screen
+        ui.item.parent().get(0).scrollTop = ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
+      },
+      beforeStop(evt, ui) {
+        const parentOffset = ui.item.parent().offset();
+        const siblings = ui.item.siblings('.js-swimlane');
+        siblings.css('height', '');
+
+        // compute the new scroll height after the resize and removal of
+        // the placeholder
+        const scrollTop = ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
+
+        // then reset the original view of the swimlane
+        siblings.removeClass('moving-swimlane');
+
+        // and apply the computed scrollheight
+        ui.item.parent().get(0).scrollTop = scrollTop;
       },
       },
       stop(evt, ui) {
       stop(evt, ui) {
         // To attribute the new index number, we need to get the DOM element
         // To attribute the new index number, we need to get the DOM element
         // of the previous and the following card -- if any.
         // of the previous and the following card -- if any.
-        const prevSwimlaneDom = ui.item.prev('.js-swimlane').get(0);
-        const nextSwimlaneDom = ui.item.next('.js-swimlane').get(0);
+        const prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0);
+        const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0);
         const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
         const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
 
 
         $swimlanesDom.sortable('cancel');
         $swimlanesDom.sortable('cancel');
@@ -72,8 +147,35 @@ BlazeComponent.extendComponent({
 
 
         boardComponent.setIsDragging(false);
         boardComponent.setIsDragging(false);
       },
       },
+      sort(evt, ui) {
+        // get the mouse position in the sortable
+        const parentOffset = ui.item.parent().offset();
+        const cursorY = evt.pageY - parentOffset.top + ui.item.parent().scrollTop();
+
+        // compute the intended index of the placeholder (we need to skip the
+        // slots between the headers and the list of cards)
+        const newplaceholderIndex = Math.floor(cursorY / swimlaneWhileSortingHeight);
+        let destPlaceholderIndex = (newplaceholderIndex + 1) * 2;
+
+        // if we are scrolling far away from the bottom of the list
+        if (destPlaceholderIndex >= ui.item.parent().get(0).childElementCount) {
+          destPlaceholderIndex = ui.item.parent().get(0).childElementCount - 1;
+        }
+
+        // update the placeholder position in the DOM tree
+        if (destPlaceholderIndex !== ui.placeholder.index()) {
+          if (destPlaceholderIndex < boardComponent.origPlaceholderIndex) {
+            ui.placeholder.insertBefore(ui.placeholder.siblings().slice(destPlaceholderIndex - 2, destPlaceholderIndex - 1));
+          } else {
+            ui.placeholder.insertAfter(ui.placeholder.siblings().slice(destPlaceholderIndex - 1, destPlaceholderIndex));
+          }
+        }
+      },
     });
     });
 
 
+    // ugly touch event hotfix
+    enableClickOnTouch('.js-swimlane:not(.placeholder)');
+
     function userIsMember() {
     function userIsMember() {
       return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
       return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
     }
     }
@@ -88,12 +190,20 @@ BlazeComponent.extendComponent({
 
 
   isViewSwimlanes() {
   isViewSwimlanes() {
     const currentUser = Meteor.user();
     const currentUser = Meteor.user();
-    return (currentUser.profile.boardView === 'board-view-swimlanes');
+    if (!currentUser) return false;
+    return ((currentUser.profile || {}).boardView === 'board-view-swimlanes');
   },
   },
 
 
   isViewLists() {
   isViewLists() {
     const currentUser = Meteor.user();
     const currentUser = Meteor.user();
-    return (currentUser.profile.boardView === 'board-view-lists');
+    if (!currentUser) return true;
+    return ((currentUser.profile || {}).boardView === 'board-view-lists');
+  },
+
+  isViewCalendar() {
+    const currentUser = Meteor.user();
+    if (!currentUser) return false;
+    return ((currentUser.profile || {}).boardView === 'board-view-cal');
   },
   },
 
 
   openNewListForm() {
   openNewListForm() {
@@ -105,7 +215,6 @@ BlazeComponent.extendComponent({
         .childComponents('addListForm')[0].open();
         .childComponents('addListForm')[0].open();
     }
     }
   },
   },
-
   events() {
   events() {
     return [{
     return [{
       // XXX The board-overlay div should probably be moved to the parent
       // XXX The board-overlay div should probably be moved to the parent
@@ -137,4 +246,95 @@ BlazeComponent.extendComponent({
     });
     });
   },
   },
 
 
+  scrollTop(position = 0) {
+    const swimlanes = this.$('.js-swimlanes');
+    swimlanes && swimlanes.animate({
+      scrollTop: position,
+    });
+  },
+
 }).register('boardBody');
 }).register('boardBody');
+
+BlazeComponent.extendComponent({
+  onRendered() {
+    this.autorun(function(){
+      $('#calendar-view').fullCalendar('refetchEvents');
+    });
+  },
+  calendarOptions() {
+    return {
+      id: 'calendar-view',
+      defaultView: 'agendaDay',
+      editable: true,
+      timezone: 'local',
+      header: {
+        left: 'title   today prev,next',
+        center: 'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,timelineMonth timelineYear',
+        right: '',
+      },
+      // height: 'parent', nope, doesn't work as the parent might be small
+      height: 'auto',
+      /* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */
+      navLinks: true,
+      nowIndicator: true,
+      businessHours: {
+        // days of week. an array of zero-based day of week integers (0=Sunday)
+        dow: [ 1, 2, 3, 4, 5 ], // Monday - Friday
+        start: '8:00',
+        end: '18:00',
+      },
+      locale: TAPi18n.getLanguage(),
+      events(start, end, timezone, callback) {
+        const currentBoard = Boards.findOne(Session.get('currentBoard'));
+        const events = [];
+        currentBoard.cardsInInterval(start.toDate(), end.toDate()).forEach(function(card){
+          events.push({
+            id: card._id,
+            title: card.title,
+            start: card.startAt,
+            end: card.endAt,
+            allDay: Math.abs(card.endAt.getTime() - card.startAt.getTime()) / 1000 === 24*3600,
+            url: FlowRouter.url('card', {
+              boardId: currentBoard._id,
+              slug: currentBoard.slug,
+              cardId: card._id,
+            }),
+          });
+        });
+        callback(events);
+      },
+      eventResize(event, delta, revertFunc) {
+        let isOk = false;
+        const card = Cards.findOne(event.id);
+
+        if (card) {
+          card.setEnd(event.end.toDate());
+          isOk = true;
+        }
+        if (!isOk) {
+          revertFunc();
+        }
+      },
+      eventDrop(event, delta, revertFunc) {
+        let isOk = false;
+        const card = Cards.findOne(event.id);
+        if (card) {
+          // TODO: add a flag for allDay events
+          if (!event.allDay) {
+            card.setStart(event.start.toDate());
+            card.setEnd(event.end.toDate());
+            isOk = true;
+          }
+        }
+        if (!isOk) {
+          revertFunc();
+        }
+      },
+    };
+  },
+  isViewCalendar() {
+    const currentUser = Meteor.user();
+    if (!currentUser) return false;
+    return ((currentUser.profile || {}).boardView === 'board-view-cal');
+  },
+}).register('calendarView');

+ 4 - 3
client/components/boards/boardBody.styl

@@ -15,12 +15,13 @@ position()
 
 
 .board-wrapper
 .board-wrapper
   position: cover
   position: cover
-  overflow-y: hidden;
+  overflow-x: hidden
+  overflow-y: hidden
 
 
   .board-canvas
   .board-canvas
     position: cover
     position: cover
     transition: margin .1s
     transition: margin .1s
-    overflow-y: auto;
+    overflow-y: auto
 
 
     &.is-sibling-sidebar-open
     &.is-sibling-sidebar-open
       margin-right: 248px
       margin-right: 248px
@@ -49,6 +50,6 @@ position()
         display: flex
         display: flex
         flex-direction: column
         flex-direction: column
         margin: 0
         margin: 0
-        padding: 0 40px 0px 0
+        padding: 0 0px 0px 0
         overflow-x: hidden
         overflow-x: hidden
         overflow-y: auto
         overflow-y: auto

+ 75 - 128
client/components/boards/boardHeader.jade

@@ -7,71 +7,69 @@ template(name="boardHeaderBar")
 
 
   .board-header-btns.left
   .board-header-btns.left
     unless isMiniScreen
     unless isMiniScreen
-      unless isSandstorm
-        if currentBoard
-          if currentUser
-            a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
-              title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
-              i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
-              if showStarCounter
-                span
-                  = currentBoard.stars
-
-            a.board-header-btn(
-              class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
-              title="{{_ currentBoard.permission}}")
-              i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
-              span {{_ currentBoard.permission}}
-
-            a.board-header-btn.js-watch-board(
-              title="{{_ watchLevel }}")
-              if $eq watchLevel "watching"
-                i.fa.fa-eye
-              if $eq watchLevel "tracking"
-                i.fa.fa-bell
-              if $eq watchLevel "muted"
-                i.fa.fa-bell-slash
-              span {{_ watchLevel}}
+      if currentBoard
+        if currentUser
+          a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
+            title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
+            i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
+            if showStarCounter
+              span
+                = currentBoard.stars
+
+          a.board-header-btn(
+            class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
+            title="{{_ currentBoard.permission}}")
+            i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
+            span {{_ currentBoard.permission}}
+
+          a.board-header-btn.js-watch-board(
+            title="{{_ watchLevel }}")
+            if $eq watchLevel "watching"
+              i.fa.fa-eye
+            if $eq watchLevel "tracking"
+              i.fa.fa-bell
+            if $eq watchLevel "muted"
+              i.fa.fa-bell-slash
+            span {{_ watchLevel}}
 
 
-          else
-            a.board-header-btn.js-log-in(
-              title="{{_ 'log-in'}}")
-              i.fa.fa-sign-in
-              span {{_ 'log-in'}}
+        else
+          a.board-header-btn.js-log-in(
+            title="{{_ 'log-in'}}")
+            i.fa.fa-sign-in
+            span {{_ 'log-in'}}
 
 
   .board-header-btns.right
   .board-header-btns.right
     if currentBoard
     if currentBoard
       if isMiniScreen
       if isMiniScreen
-        unless isSandstorm
-          if currentUser
-            a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
-              title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
-              i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
-              if showStarCounter
-                span
-                  = currentBoard.stars
-
-            a.board-header-btn(
-              class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
-              title="{{_ currentBoard.permission}}")
-              i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
-              span {{_ currentBoard.permission}}
-
-            a.board-header-btn.js-watch-board(
-              title="{{_ watchLevel }}")
-              if $eq watchLevel "watching"
-                i.fa.fa-eye
-              if $eq watchLevel "tracking"
-                i.fa.fa-bell
-              if $eq watchLevel "muted"
-                i.fa.fa-bell-slash
-              span {{_ watchLevel}}
+        if currentUser
+          a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
+            title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
+            i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
+            if showStarCounter
+              span
+                = currentBoard.stars
+
+          a.board-header-btn(
+            class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
+            title="{{_ currentBoard.permission}}")
+            i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
+            span {{_ currentBoard.permission}}
+
+          a.board-header-btn.js-watch-board(
+            title="{{_ watchLevel }}")
+            if $eq watchLevel "watching"
+              i.fa.fa-eye
+            if $eq watchLevel "tracking"
+              i.fa.fa-bell
+            if $eq watchLevel "muted"
+              i.fa.fa-bell-slash
+            span {{_ watchLevel}}
 
 
-          else
-            a.board-header-btn.js-log-in(
-              title="{{_ 'log-in'}}")
-              i.fa.fa-sign-in
-              span {{_ 'log-in'}}
+        else
+          a.board-header-btn.js-log-in(
+            title="{{_ 'log-in'}}")
+            i.fa.fa-sign-in
+            span {{_ 'log-in'}}
 
 
       if isSandstorm
       if isSandstorm
         if currentUser
         if currentUser
@@ -87,15 +85,20 @@ template(name="boardHeaderBar")
         if Filter.isActive
         if Filter.isActive
           a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
           a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
             i.fa.fa-times-thin
             i.fa.fa-times-thin
+      if currentUser.isAdmin
+        a.board-header-btn.js-open-rules-view(title="{{_ 'rules'}}")
+          i.fa.fa-magic
+          span {{_ 'rules'}}
 
 
       a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
       a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
         i.fa.fa-search
         i.fa.fa-search
         span {{_ 'search'}}
         span {{_ 'search'}}
 
 
-      a.board-header-btn.js-toggle-board-view(
-        title="{{_ 'board-view'}}")
-        i.fa.fa-th-large
-        span {{_ currentUser.profile.boardView}}
+      unless currentBoard.isTemplatesBoard
+        a.board-header-btn.js-toggle-board-view(
+          title="{{_ 'board-view'}}")
+          i.fa.fa-th-large
+          span {{#if currentUser.profile.boardView}}{{_ currentUser.profile.boardView}}{{else}}{{_ 'board-view-lists'}}{{/if}}
 
 
       if canModifyBoard
       if canModifyBoard
         a.board-header-btn.js-multiselection-activate(
         a.board-header-btn.js-multiselection-activate(
@@ -108,32 +111,8 @@ template(name="boardHeaderBar")
               i.fa.fa-times-thin
               i.fa.fa-times-thin
 
 
       .separator
       .separator
-      a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
-        i.board-header-btn-icon.fa.fa-navicon
-
-template(name="boardMenuPopup")
-  ul.pop-over-list
-    li: a.js-open-archives {{_ 'archived-items'}}
-    if currentUser.isBoardAdmin
-      li: a.js-change-board-color {{_ 'board-change-color'}}
-    //-
-      XXX Language should be handled by sandstorm, but for now display a
-      language selection link in the board menu. This link is normally present
-      in the header bar that is not displayed on sandstorm.
-    if isSandstorm
-      li: a.js-change-language {{_ 'language'}}
-  unless isSandstorm
-    if currentUser.isBoardAdmin
-      hr
-      ul.pop-over-list
-        li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
-        li: a.js-archive-board {{_ 'archive-board'}}
-        li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
-  if isSandstorm
-    hr
-    ul.pop-over-list
-      li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
-      li: a.js-import-board {{_ 'import-board-c'}}
+      a.board-header-btn.js-toggle-sidebar
+        i.fa.fa-navicon
 
 
 template(name="boardVisibilityList")
 template(name="boardVisibilityList")
   ul.pop-over-list
   ul.pop-over-list
@@ -184,14 +163,6 @@ template(name="boardChangeWatchPopup")
             i.fa.fa-check
             i.fa.fa-check
           span.sub-name {{_ 'muted-info'}}
           span.sub-name {{_ 'muted-info'}}
 
 
-template(name="boardChangeColorPopup")
-  .board-backgrounds-list.clearfix
-    each backgroundColors
-      .board-background-select.js-select-background
-        span.background-box(class="board-color-{{this}}")
-          if isSelected
-            i.fa.fa-check
-
 template(name="createBoard")
 template(name="createBoard")
   form
   form
     label
     label
@@ -213,45 +184,21 @@ template(name="createBoard")
     input.primary.wide(type="submit" value="{{_ 'create'}}")
     input.primary.wide(type="submit" value="{{_ 'create'}}")
     span.quiet
     span.quiet
       | {{_ 'or'}}
       | {{_ 'or'}}
-      a.js-import-board {{_ 'import-board'}}
-
-template(name="chooseBoardSource")
-  ul.pop-over-list
-    li
-      a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
-    li
-      a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
+      a.js-import-board {{_ 'import'}}
+    span.quiet
+      | /
+      a.js-board-template {{_ 'template'}}
 
 
 template(name="boardChangeTitlePopup")
 template(name="boardChangeTitlePopup")
   form
   form
     label
     label
       | {{_ 'title'}}
       | {{_ 'title'}}
-      input.js-board-name(type="text" value=title autofocus)
+      input.js-board-name(type="text" value=title autofocus dir="auto")
     label
     label
       | {{_ 'description'}}
       | {{_ 'description'}}
-      textarea.js-board-desc= description
+      textarea.js-board-desc(dir="auto")= description
     input.primary.wide(type="submit" value="{{_ 'rename'}}")
     input.primary.wide(type="submit" value="{{_ 'rename'}}")
 
 
-template(name="archiveBoardPopup")
+template(name="boardCreateRulePopup")
   p {{_ 'close-board-pop'}}
   p {{_ 'close-board-pop'}}
   button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
   button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
-
-template(name="outgoingWebhooksPopup")
-  each integrations
-    form.integration-form
-      if title
-        h4 {{title}}
-      else
-        h4 {{_ 'no-name'}}
-      label
-        | URL
-        input.js-outgoing-webhooks-url(type="text" name="url" value=url)
-        input(type="hidden" value=_id name="id")
-      input.primary.wide(type="submit" value="{{_ 'save'}}")
-  form.integration-form
-    h4
-      | {{_ 'new-outgoing-webhook'}}
-    label
-      | URL
-      input.js-outgoing-webhooks-url(type="text" name="url" autofocus)
-    input.primary.wide(type="submit" value="{{_ 'save'}}")

+ 24 - 77
client/components/boards/boardHeader.js

@@ -1,5 +1,9 @@
 Template.boardMenuPopup.events({
 Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
   'click .js-rename-board': Popup.open('boardChangeTitle'),
+  'click .js-custom-fields'() {
+    Sidebar.setView('customFields');
+    Popup.close();
+  },
   'click .js-open-archives'() {
   'click .js-open-archives'() {
     Sidebar.setView('archives');
     Sidebar.setView('archives');
     Popup.close();
     Popup.close();
@@ -13,8 +17,15 @@ Template.boardMenuPopup.events({
     // confirm that the board was successfully archived.
     // confirm that the board was successfully archived.
     FlowRouter.go('home');
     FlowRouter.go('home');
   }),
   }),
+  'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    Popup.close();
+    Boards.remove(currentBoard._id);
+    FlowRouter.go('home');
+  }),
   'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
   'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
   'click .js-import-board': Popup.open('chooseBoardSource'),
   'click .js-import-board': Popup.open('chooseBoardSource'),
+  'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
 });
 });
 
 
 Template.boardMenuPopup.helpers({
 Template.boardMenuPopup.helpers({
@@ -78,12 +89,19 @@ BlazeComponent.extendComponent({
       },
       },
       'click .js-toggle-board-view'() {
       'click .js-toggle-board-view'() {
         const currentUser = Meteor.user();
         const currentUser = Meteor.user();
-        if (currentUser.profile.boardView === 'board-view-swimlanes') {
+        if ((currentUser.profile || {}).boardView === 'board-view-swimlanes') {
+          currentUser.setBoardView('board-view-cal');
+        } else if ((currentUser.profile || {}).boardView === 'board-view-lists') {
+          currentUser.setBoardView('board-view-swimlanes');
+        } else if ((currentUser.profile || {}).boardView === 'board-view-cal') {
           currentUser.setBoardView('board-view-lists');
           currentUser.setBoardView('board-view-lists');
-        } else if (currentUser.profile.boardView === 'board-view-lists') {
+        } else {
           currentUser.setBoardView('board-view-swimlanes');
           currentUser.setBoardView('board-view-swimlanes');
         }
         }
       },
       },
+      'click .js-toggle-sidebar'() {
+        Sidebar.toggle();
+      },
       'click .js-open-filter-view'() {
       'click .js-open-filter-view'() {
         Sidebar.setView('filter');
         Sidebar.setView('filter');
       },
       },
@@ -95,6 +113,9 @@ BlazeComponent.extendComponent({
       'click .js-open-search-view'() {
       'click .js-open-search-view'() {
         Sidebar.setView('search');
         Sidebar.setView('search');
       },
       },
+      'click .js-open-rules-view'() {
+        Modal.openWide('rulesMain');
+      },
       'click .js-multiselection-activate'() {
       'click .js-multiselection-activate'() {
         const currentCard = Session.get('currentCard');
         const currentCard = Session.get('currentCard');
         MultiSelection.activate();
         MultiSelection.activate();
@@ -119,28 +140,6 @@ Template.boardHeaderBar.helpers({
   },
   },
 });
 });
 
 
-BlazeComponent.extendComponent({
-  backgroundColors() {
-    return Boards.simpleSchema()._schema.color.allowedValues;
-  },
-
-  isSelected() {
-    const currentBoard = Boards.findOne(Session.get('currentBoard'));
-    return currentBoard.color === this.currentData().toString();
-  },
-
-  events() {
-    return [{
-      'click .js-select-background'(evt) {
-        const currentBoard = Boards.findOne(Session.get('currentBoard'));
-        const newColor = this.currentData().toString();
-        currentBoard.setColor(newColor);
-        evt.preventDefault();
-      },
-    }];
-  },
-}).register('boardChangeColorPopup');
-
 const CreateBoard = BlazeComponent.extendComponent({
 const CreateBoard = BlazeComponent.extendComponent({
   template() {
   template() {
     return 'createBoard';
     return 'createBoard';
@@ -192,16 +191,11 @@ const CreateBoard = BlazeComponent.extendComponent({
       'click .js-import': Popup.open('boardImportBoard'),
       'click .js-import': Popup.open('boardImportBoard'),
       submit: this.onSubmit,
       submit: this.onSubmit,
       'click .js-import-board': Popup.open('chooseBoardSource'),
       'click .js-import-board': Popup.open('chooseBoardSource'),
+      'click .js-board-template': Popup.open('searchElement'),
     }];
     }];
   },
   },
 }).register('createBoardPopup');
 }).register('createBoardPopup');
 
 
-BlazeComponent.extendComponent({
-  template() {
-    return 'chooseBoardSource';
-  },
-}).register('chooseBoardSourcePopup');
-
 (class HeaderBarCreateBoard extends CreateBoard {
 (class HeaderBarCreateBoard extends CreateBoard {
   onSubmit(evt) {
   onSubmit(evt) {
     super.onSubmit(evt);
     super.onSubmit(evt);
@@ -251,50 +245,3 @@ BlazeComponent.extendComponent({
     }];
     }];
   },
   },
 }).register('boardChangeWatchPopup');
 }).register('boardChangeWatchPopup');
-
-BlazeComponent.extendComponent({
-  integrations() {
-    const boardId = Session.get('currentBoard');
-    return Integrations.find({ boardId: `${boardId}` }).fetch();
-  },
-
-  integration(id) {
-    const boardId = Session.get('currentBoard');
-    return Integrations.findOne({ _id: id, boardId: `${boardId}` });
-  },
-
-  events() {
-    return [{
-      'submit'(evt) {
-        evt.preventDefault();
-        const url = evt.target.url.value;
-        const boardId = Session.get('currentBoard');
-        let id = null;
-        let integration = null;
-        if (evt.target.id) {
-          id = evt.target.id.value;
-          integration = this.integration(id);
-          if (url) {
-            Integrations.update(integration._id, {
-              $set: {
-                url: `${url}`,
-              },
-            });
-          } else {
-            Integrations.remove(integration._id);
-          }
-        } else if (url) {
-          Integrations.insert({
-            userId: Meteor.userId(),
-            enabled: true,
-            type: 'outgoing-webhooks',
-            url: `${url}`,
-            boardId: `${boardId}`,
-            activities: ['all'],
-          });
-        }
-        Popup.close();
-      },
-    }];
-  },
-}).register('outgoingWebhooksPopup');

+ 19 - 0
client/components/boards/boardHeader.styl

@@ -1,3 +1,22 @@
 .integration-form
 .integration-form
   padding: 5px
   padding: 5px
   border-bottom: 1px solid #ccc
   border-bottom: 1px solid #ccc
+
+.flex
+  display: -webkit-box
+  display: -moz-box
+  display: -webkit-flex
+  display: -moz-flex
+  display: -ms-flexbox
+  display: flex
+
+.option
+  @extends .flex
+  -webkit-border-radius: 3px;
+  border-radius: 3px;
+  background: #fff;
+  text-decoration: none;
+  -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+  margin-top: 5px;
+  padding: 5px;

+ 9 - 4
client/components/boards/boardsList.jade

@@ -1,6 +1,8 @@
 template(name="boardList")
 template(name="boardList")
   .wrapper
   .wrapper
     ul.board-list.clearfix
     ul.board-list.clearfix
+      li.js-add-board
+        a.board-list-item.label {{_ 'add-board'}}
       each boards
       each boards
         li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
         li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
           if isInvited
           if isInvited
@@ -20,15 +22,15 @@ template(name="boardList")
                 i.fa.js-star-board(
                 i.fa.js-star-board(
                   class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
                   class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
                   title="{{_ 'star-board-title'}}")
                   title="{{_ 'star-board-title'}}")
-
+                p.board-list-item-desc= description
                 if hasSpentTimeCards
                 if hasSpentTimeCards
                   i.fa.js-has-spenttime-cards(
                   i.fa.js-has-spenttime-cards(
                     class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
                     class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
                     title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
                     title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
+                i.fa.js-clone-board(
+                    class="fa-clone"
+                    title="{{_ 'duplicate-board'}}")
 
 
-                p.board-list-item-desc= description
-      li.js-add-board
-        a.board-list-item.label {{_ 'add-board'}}
 
 
 
 
 template(name="boardListHeaderBar")
 template(name="boardListHeaderBar")
@@ -37,3 +39,6 @@ template(name="boardListHeaderBar")
     a.board-header-btn.js-open-archived-board
     a.board-header-btn.js-open-archived-board
       i.fa.fa-archive
       i.fa.fa-archive
       span {{_ 'archives'}}
       span {{_ 'archives'}}
+    a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
+      i.fa.fa-clone
+      span {{_ 'templates'}}

+ 32 - 4
client/components/boards/boardsList.js

@@ -1,5 +1,20 @@
 const subManager = new SubsManager();
 const subManager = new SubsManager();
 
 
+Template.boardListHeaderBar.events({
+  'click .js-open-archived-board'() {
+    Modal.open('archivedBoards');
+  },
+});
+
+Template.boardListHeaderBar.helpers({
+  templatesBoardId() {
+    return Meteor.user().getTemplatesBoardId();
+  },
+  templatesBoardSlug() {
+    return Meteor.user().getTemplatesBoardSlug();
+  },
+});
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     Meteor.subscribe('setting');
     Meteor.subscribe('setting');
@@ -9,11 +24,9 @@ BlazeComponent.extendComponent({
     return Boards.find({
     return Boards.find({
       archived: false,
       archived: false,
       'members.userId': Meteor.userId(),
       'members.userId': Meteor.userId(),
-    }, {
-      sort: ['title'],
-    });
+      type: 'board',
+    }, { sort: ['title'] });
   },
   },
-
   isStarred() {
   isStarred() {
     const user = Meteor.user();
     const user = Meteor.user();
     return user && user.hasStarred(this.currentData()._id);
     return user && user.hasStarred(this.currentData()._id);
@@ -42,6 +55,21 @@ BlazeComponent.extendComponent({
         Meteor.user().toggleBoardStar(boardId);
         Meteor.user().toggleBoardStar(boardId);
         evt.preventDefault();
         evt.preventDefault();
       },
       },
+      'click .js-clone-board'(evt) {
+        Meteor.call('cloneBoard',
+          this.currentData()._id,
+          Session.get('fromBoard'),
+          (err, res) => {
+            if (err) {
+              this.setError(err.error);
+            } else {
+              Session.set('fromBoard', null);
+              Utils.goBoardId(res);
+            }
+          }
+        );
+        evt.preventDefault();
+      },
       'click .js-accept-invite'() {
       'click .js-accept-invite'() {
         const boardId = this.currentData()._id;
         const boardId = this.currentData()._id;
         Meteor.user().removeInvite(boardId);
         Meteor.user().removeInvite(boardId);

+ 14 - 0
client/components/boards/boardsList.styl

@@ -94,13 +94,27 @@ $spaceBetweenTiles = 16px
   .is-star-active
   .is-star-active
     color: white
     color: white
 
 
+  .fa-clone
+    position: absolute;
+    bottom: 0
+    font-size: 14px
+    height: 18px
+    line-height: 18px
+    opacity: 0
+    right: 0
+    padding: 9px 9px
+    transition-duration: .15s
+    transition-property: color, font-size, background
+
   li:hover a
   li:hover a
     &:hover
     &:hover
       .fa-star,
       .fa-star,
+      .fa-clone,
       .fa-star-o
       .fa-star-o
         color: white
         color: white
 
 
     .fa-star,
     .fa-star,
+    .fa-clone,
     .fa-star-o
     .fa-star-o
       color: white
       color: white
       opacity: .75
       opacity: .75

+ 8 - 0
client/components/boards/miniboard.jade

@@ -0,0 +1,8 @@
+template(name="miniboard")
+  .minicard(
+    class="minicard-{{colorClass}}")
+    .minicard-title
+      .handle
+        .fa.fa-arrows
+      +viewer
+        = title

+ 7 - 2
client/components/cards/attachments.js

@@ -57,8 +57,13 @@ Template.cardAttachmentsPopup.events({
     const card = this;
     const card = this;
     FS.Utility.eachFile(evt, (f) => {
     FS.Utility.eachFile(evt, (f) => {
       const file = new FS.File(f);
       const file = new FS.File(f);
-      file.boardId = card.boardId;
-      file.cardId = card._id;
+      if (card.isLinkedCard()) {
+        file.boardId = Cards.findOne(card.linkedId).boardId;
+        file.cardId = card.linkedId;
+      } else {
+        file.boardId = card.boardId;
+        file.cardId = card._id;
+      }
       file.userId = Meteor.userId();
       file.userId = Meteor.userId();
 
 
       const attachment = Attachments.insert(file);
       const attachment = Attachments.insert(file);

+ 76 - 0
client/components/cards/cardCustomFields.jade

@@ -0,0 +1,76 @@
+template(name="cardCustomFieldsPopup")
+    ul.pop-over-list
+        each board.customFields
+            li.item(class="")
+                a.name.js-select-field(href="#")
+                    span.full-name
+                        = name
+                    if hasCustomField
+                        i.fa.fa-check
+    hr
+    a.quiet-button.full.js-settings
+        i.fa.fa-cog
+        span {{_ 'settings'}}
+
+template(name="cardCustomField")
+    +Template.dynamic(template=getTemplate)
+
+template(name="cardCustomField-text")
+    if canModifyCard
+        +inlinedForm(classNames="js-card-customfield-text")
+            +editor(autofocus=true)
+                = value
+            .edit-controls.clearfix
+                button.primary(type="submit") {{_ 'save'}}
+                a.fa.fa-times-thin.js-close-inlined-form
+        else
+            a.js-open-inlined-form
+                if value
+                    +viewer
+                        = value
+                else
+                    | {{_ 'edit'}}
+
+template(name="cardCustomField-number")
+    if canModifyCard
+        +inlinedForm(classNames="js-card-customfield-number")
+            input(type="number" value=data.value)
+            .edit-controls.clearfix
+                button.primary(type="submit") {{_ 'save'}}
+                a.fa.fa-times-thin.js-close-inlined-form
+        else
+            a.js-open-inlined-form
+                if value
+                    = value
+                else
+                    | {{_ 'edit'}}
+
+template(name="cardCustomField-date")
+    if canModifyCard
+        a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
+            if value
+                div.card-date
+                    time(datetime="{{showISODate}}")
+                        | {{showDate}}
+            else
+                | {{_ 'edit'}}
+
+template(name="cardCustomField-dropdown")
+    if canModifyCard
+        +inlinedForm(classNames="js-card-customfield-dropdown")
+            select.inline
+                each items
+                    if($eq data.value this._id)
+                        option(value=_id selected="selected") {{name}}
+                    else
+                        option(value=_id) {{name}}
+            .edit-controls.clearfix
+                button.primary(type="submit") {{_ 'save'}}
+                a.fa.fa-times-thin.js-close-inlined-form
+        else
+            a.js-open-inlined-form
+                if value
+                    +viewer
+                        = selectedItem
+                else
+                    | {{_ 'edit'}}

+ 179 - 0
client/components/cards/cardCustomFields.js

@@ -0,0 +1,179 @@
+Template.cardCustomFieldsPopup.helpers({
+  hasCustomField() {
+    const card = Cards.findOne(Session.get('currentCard'));
+    const customFieldId = this._id;
+    return card.customFieldIndex(customFieldId) > -1;
+  },
+});
+
+Template.cardCustomFieldsPopup.events({
+  'click .js-select-field'(evt) {
+    const card = Cards.findOne(Session.get('currentCard'));
+    const customFieldId = this._id;
+    card.toggleCustomField(customFieldId);
+    evt.preventDefault();
+  },
+  'click .js-settings'(evt) {
+    EscapeActions.executeUpTo('detailsPane');
+    Sidebar.setView('customFields');
+    evt.preventDefault();
+  },
+});
+
+// cardCustomField
+const CardCustomField = BlazeComponent.extendComponent({
+
+  getTemplate() {
+    return `cardCustomField-${this.data().definition.type}`;
+  },
+
+  onCreated() {
+    const self = this;
+    self.card = Cards.findOne(Session.get('currentCard'));
+    self.customFieldId = this.data()._id;
+  },
+
+  canModifyCard() {
+    return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+  },
+});
+CardCustomField.register('cardCustomField');
+
+// cardCustomField-text
+(class extends CardCustomField {
+
+  onCreated() {
+    super.onCreated();
+  }
+
+  events() {
+    return [{
+      'submit .js-card-customfield-text'(evt) {
+        evt.preventDefault();
+        const value = this.currentComponent().getValue();
+        this.card.setCustomField(this.customFieldId, value);
+      },
+    }];
+  }
+
+}).register('cardCustomField-text');
+
+// cardCustomField-number
+(class extends CardCustomField {
+
+  onCreated() {
+    super.onCreated();
+  }
+
+  events() {
+    return [{
+      'submit .js-card-customfield-number'(evt) {
+        evt.preventDefault();
+        const value = parseInt(this.find('input').value, 10);
+        this.card.setCustomField(this.customFieldId, value);
+      },
+    }];
+  }
+
+}).register('cardCustomField-number');
+
+// cardCustomField-date
+(class extends CardCustomField {
+
+  onCreated() {
+    super.onCreated();
+    const self = this;
+    self.date = ReactiveVar();
+    self.now = ReactiveVar(moment());
+    window.setInterval(() => {
+      self.now.set(moment());
+    }, 60000);
+
+    self.autorun(() => {
+      self.date.set(moment(self.data().value));
+    });
+  }
+
+  showDate() {
+    // this will start working once mquandalle:moment
+    // is updated to at least moment.js 2.10.5
+    // until then, the date is displayed in the "L" format
+    return this.date.get().calendar(null, {
+      sameElse: 'llll',
+    });
+  }
+
+  showISODate() {
+    return this.date.get().toISOString();
+  }
+
+  classes() {
+    if (this.date.get().isBefore(this.now.get(), 'minute') &&
+            this.now.get().isBefore(this.data().value)) {
+      return 'current';
+    }
+    return '';
+  }
+
+  showTitle() {
+    return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
+  }
+
+  events() {
+    return [{
+      'click .js-edit-date': Popup.open('cardCustomField-date'),
+    }];
+  }
+
+}).register('cardCustomField-date');
+
+// cardCustomField-datePopup
+(class extends DatePicker {
+  onCreated() {
+    super.onCreated();
+    const self = this;
+    self.card = Cards.findOne(Session.get('currentCard'));
+    self.customFieldId = this.data()._id;
+    this.data().value && this.date.set(moment(this.data().value));
+  }
+
+  _storeDate(date) {
+    this.card.setCustomField(this.customFieldId, date);
+  }
+
+  _deleteDate() {
+    this.card.setCustomField(this.customFieldId, '');
+  }
+}).register('cardCustomField-datePopup');
+
+// cardCustomField-dropdown
+(class extends CardCustomField {
+
+  onCreated() {
+    super.onCreated();
+    this._items = this.data().definition.settings.dropdownItems;
+    this.items = this._items.slice(0);
+    this.items.unshift({
+      _id: '',
+      name: TAPi18n.__('custom-field-dropdown-none'),
+    });
+  }
+
+  selectedItem() {
+    const selected = this._items.find((item) => {
+      return item._id === this.data().value;
+    });
+    return (selected) ? selected.name : TAPi18n.__('custom-field-dropdown-unknown');
+  }
+
+  events() {
+    return [{
+      'submit .js-card-customfield-dropdown'(evt) {
+        evt.preventDefault();
+        const value = this.find('select').value;
+        this.card.setCustomField(this.customFieldId, value);
+      },
+    }];
+  }
+
+}).register('cardCustomField-dropdown');

+ 0 - 16
client/components/cards/cardDate.jade

@@ -1,19 +1,3 @@
-template(name="editCardDate")
-  .edit-card-date
-    form.edit-date
-      .fields
-        .left
-          label(for="date") {{_ 'date'}}
-          input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
-        .right
-          label(for="time") {{_ 'time'}}
-          input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
-      .js-datepicker
-      if error.get
-        .warning {{_ error.get}}
-      button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
-      button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
-
 template(name="dateBadge")
 template(name="dateBadge")
   if canModifyCard
   if canModifyCard
     a.js-edit-date.card-date(title="{{showTitle}}" class="{{classes}}")
     a.js-edit-date.card-date(title="{{showTitle}}" class="{{classes}}")

+ 62 - 37
client/components/cards/cardDate.js

@@ -1,5 +1,5 @@
 // Edit received, start, due & end dates
 // Edit received, start, due & end dates
-const EditCardDate = BlazeComponent.extendComponent({
+BlazeComponent.extendComponent({
   template() {
   template() {
     return 'editCardDate';
     return 'editCardDate';
   },
   },
@@ -93,10 +93,10 @@ Template.dateBadge.helpers({
 });
 });
 
 
 // editCardReceivedDatePopup
 // editCardReceivedDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   onCreated() {
   onCreated() {
     super.onCreated();
     super.onCreated();
-    this.data().receivedAt && this.date.set(moment(this.data().receivedAt));
+    this.data().getReceived() && this.date.set(moment(this.data().getReceived()));
   }
   }
 
 
   _storeDate(date) {
   _storeDate(date) {
@@ -104,22 +104,22 @@ Template.dateBadge.helpers({
   }
   }
 
 
   _deleteDate() {
   _deleteDate() {
-    this.card.unsetReceived();
+    this.card.setReceived(null);
   }
   }
 }).register('editCardReceivedDatePopup');
 }).register('editCardReceivedDatePopup');
 
 
 
 
 // editCardStartDatePopup
 // editCardStartDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   onCreated() {
   onCreated() {
     super.onCreated();
     super.onCreated();
-    this.data().startAt && this.date.set(moment(this.data().startAt));
+    this.data().getStart() && this.date.set(moment(this.data().getStart()));
   }
   }
 
 
   onRendered() {
   onRendered() {
     super.onRendered();
     super.onRendered();
-    if (moment.isDate(this.card.receivedAt)) {
-      this.$('.js-datepicker').datepicker('setStartDate', this.card.receivedAt);
+    if (moment.isDate(this.card.getReceived())) {
+      this.$('.js-datepicker').datepicker('setStartDate', this.card.getReceived());
     }
     }
   }
   }
 
 
@@ -128,21 +128,21 @@ Template.dateBadge.helpers({
   }
   }
 
 
   _deleteDate() {
   _deleteDate() {
-    this.card.unsetStart();
+    this.card.setStart(null);
   }
   }
 }).register('editCardStartDatePopup');
 }).register('editCardStartDatePopup');
 
 
 // editCardDueDatePopup
 // editCardDueDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   onCreated() {
   onCreated() {
     super.onCreated();
     super.onCreated();
-    this.data().dueAt && this.date.set(moment(this.data().dueAt));
+    this.data().getDue() && this.date.set(moment(this.data().getDue()));
   }
   }
 
 
   onRendered() {
   onRendered() {
     super.onRendered();
     super.onRendered();
-    if (moment.isDate(this.card.startAt)) {
-      this.$('.js-datepicker').datepicker('setStartDate', this.card.startAt);
+    if (moment.isDate(this.card.getStart())) {
+      this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
     }
     }
   }
   }
 
 
@@ -151,21 +151,21 @@ Template.dateBadge.helpers({
   }
   }
 
 
   _deleteDate() {
   _deleteDate() {
-    this.card.unsetDue();
+    this.card.setDue(null);
   }
   }
 }).register('editCardDueDatePopup');
 }).register('editCardDueDatePopup');
 
 
 // editCardEndDatePopup
 // editCardEndDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   onCreated() {
   onCreated() {
     super.onCreated();
     super.onCreated();
-    this.data().endAt && this.date.set(moment(this.data().endAt));
+    this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
   }
   }
 
 
   onRendered() {
   onRendered() {
     super.onRendered();
     super.onRendered();
-    if (moment.isDate(this.card.startAt)) {
-      this.$('.js-datepicker').datepicker('setStartDate', this.card.startAt);
+    if (moment.isDate(this.card.getStart())) {
+      this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
     }
     }
   }
   }
 
 
@@ -174,7 +174,7 @@ Template.dateBadge.helpers({
   }
   }
 
 
   _deleteDate() {
   _deleteDate() {
-    this.card.unsetEnd();
+    this.card.setEnd(null);
   }
   }
 }).register('editCardEndDatePopup');
 }).register('editCardEndDatePopup');
 
 
@@ -213,16 +213,23 @@ class CardReceivedDate extends CardDate {
     super.onCreated();
     super.onCreated();
     const self = this;
     const self = this;
     self.autorun(() => {
     self.autorun(() => {
-      self.date.set(moment(self.data().receivedAt));
+      self.date.set(moment(self.data().getReceived()));
     });
     });
   }
   }
 
 
   classes() {
   classes() {
-    let classes = 'received-date' + ' ';
-    if (this.date.get().isBefore(this.now.get(), 'minute') &&
-        this.now.get().isBefore(this.data().dueAt)) {
+    let classes = 'received-date ';
+    const dueAt = this.data().getDue();
+    const endAt = this.data().getEnd();
+    const startAt = this.data().getStart();
+    const theDate = this.date.get();
+    // if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
+    if (((startAt) && (theDate.isAfter(dueAt))) ||
+       ((endAt) && (theDate.isAfter(endAt))) ||
+       ((dueAt) && (theDate.isAfter(dueAt))))
+      classes += 'long-overdue';
+    else
       classes += 'current';
       classes += 'current';
-    }
     return classes;
     return classes;
   }
   }
 
 
@@ -243,16 +250,24 @@ class CardStartDate extends CardDate {
     super.onCreated();
     super.onCreated();
     const self = this;
     const self = this;
     self.autorun(() => {
     self.autorun(() => {
-      self.date.set(moment(self.data().startAt));
+      self.date.set(moment(self.data().getStart()));
     });
     });
   }
   }
 
 
   classes() {
   classes() {
     let classes = 'start-date' + ' ';
     let classes = 'start-date' + ' ';
-    if (this.date.get().isBefore(this.now.get(), 'minute') &&
-        this.now.get().isBefore(this.data().dueAt)) {
+    const dueAt = this.data().getDue();
+    const endAt = this.data().getEnd();
+    const theDate = this.date.get();
+    const now = this.now.get();
+    // if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
+    if (((endAt) && (theDate.isAfter(endAt))) ||
+       ((dueAt) && (theDate.isAfter(dueAt))))
+      classes += 'long-overdue';
+    else if (theDate.isBefore(now, 'minute'))
+      classes += 'almost-due';
+    else
       classes += 'current';
       classes += 'current';
-    }
     return classes;
     return classes;
   }
   }
 
 
@@ -273,17 +288,26 @@ class CardDueDate extends CardDate {
     super.onCreated();
     super.onCreated();
     const self = this;
     const self = this;
     self.autorun(() => {
     self.autorun(() => {
-      self.date.set(moment(self.data().dueAt));
+      self.date.set(moment(self.data().getDue()));
     });
     });
   }
   }
 
 
   classes() {
   classes() {
     let classes = 'due-date' + ' ';
     let classes = 'due-date' + ' ';
-    if (this.now.get().diff(this.date.get(), 'days') >= 2)
+    const endAt = this.data().getEnd();
+    const theDate = this.date.get();
+    const now = this.now.get();
+    // if the due date is after the end date, green - done early
+    if ((endAt) && (theDate.isAfter(endAt)))
+      classes += 'current';
+    // if there is an end date, don't need to flag the due date
+    else if (endAt)
+      classes += '';
+    else if (now.diff(theDate, 'days') >= 2)
       classes += 'long-overdue';
       classes += 'long-overdue';
-    else if (this.now.get().diff(this.date.get(), 'minute') >= 0)
+    else if (now.diff(theDate, 'minute') >= 0)
       classes += 'due';
       classes += 'due';
-    else if (this.now.get().diff(this.date.get(), 'days') >= -1)
+    else if (now.diff(theDate, 'days') >= -1)
       classes += 'almost-due';
       classes += 'almost-due';
     return classes;
     return classes;
   }
   }
@@ -305,17 +329,19 @@ class CardEndDate extends CardDate {
     super.onCreated();
     super.onCreated();
     const self = this;
     const self = this;
     self.autorun(() => {
     self.autorun(() => {
-      self.date.set(moment(self.data().endAt));
+      self.date.set(moment(self.data().getEnd()));
     });
     });
   }
   }
 
 
   classes() {
   classes() {
     let classes = 'end-date' + ' ';
     let classes = 'end-date' + ' ';
-    if (this.data.dueAt.diff(this.date.get(), 'days') >= 2)
+    const dueAt = this.data().getDue();
+    const theDate = this.date.get();
+    if (theDate.diff(dueAt, 'days') >= 2)
       classes += 'long-overdue';
       classes += 'long-overdue';
-    else if (this.data.dueAt.diff(this.date.get(), 'days') >= 0)
+    else if (theDate.diff(dueAt, 'days') >= 0)
       classes += 'due';
       classes += 'due';
-    else if (this.data.dueAt.diff(this.date.get(), 'days') >= -2)
+    else if (theDate.diff(dueAt, 'days') >= -2)
       classes += 'almost-due';
       classes += 'almost-due';
     return classes;
     return classes;
   }
   }
@@ -355,4 +381,3 @@ CardEndDate.register('cardEndDate');
     return this.date.get().format('l');
     return this.date.get().format('l');
   }
   }
 }).register('minicardEndDate');
 }).register('minicardEndDate');
-

+ 2 - 21
client/components/cards/cardDate.styl

@@ -1,22 +1,3 @@
-.edit-card-date
-  .fields
-    .left
-      width: 56%
-    .right
-      width: 38%
-  .datepicker
-    width: 100%
-    table
-      width: 100%
-      border: none
-      border-spacing: 0
-      border-collapse: collapse
-      thead
-        background: none
-      td, th
-        box-sizing: border-box
-
-
 .card-date
 .card-date
   display: block
   display: block
   border-radius: 4px
   border-radius: 4px
@@ -62,12 +43,12 @@
   &.start-date
   &.start-date
     time
     time
       &::before
       &::before
-        content: "\f08b"  // symbol: fa-sign-out
+        content: "\f251"  // symbol: fa-hourglass-start
 
 
   &.received-date
   &.received-date
     time
     time
       &::before
       &::before
-        content: "\f251"  // symbol: fa-hourglass-start
+        content: "\f08b"  // symbol: fa-sign-out
 
 
   time
   time
     &::before
     &::before

+ 163 - 46
client/components/cards/cardDetails.jade

@@ -1,6 +1,6 @@
 template(name="cardDetails")
 template(name="cardDetails")
   section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
   section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
-    .card-details-header
+    .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
       +inlinedForm(classNames="js-card-details-title")
       +inlinedForm(classNames="js-card-details-title")
         +editCardTitleForm
         +editCardTitleForm
       else
       else
@@ -10,46 +10,63 @@ template(name="cardDetails")
         h2.card-details-title.js-card-title(
         h2.card-details-title.js-card-title(
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
             +viewer
             +viewer
-              = title
+              = getTitle
               if isWatching
               if isWatching
                 i.fa.fa-eye.card-details-watch
                 i.fa.fa-eye.card-details-watch
+        .card-details-path
+          each parentList
+            | &nbsp; &gt; &nbsp;
+            a.js-parent-card(href=linkForCard) {{title}}
+          // else
+            {{_ 'top-level-card'}}
+        if isLinkedCard
+          h3.linked-card-location
+            +viewer
+              | {{getBoardTitle}} > {{getTitle}}
 
 
-    if archived
-      p.warning {{_ 'card-archived'}}
+    if getArchived
+      if isLinkedBoard
+        p.warning {{_ 'board-archived'}}
+      else
+        p.warning {{_ 'card-archived'}}
 
 
     .card-details-items
     .card-details-items
       .card-details-item.card-details-item-received
       .card-details-item.card-details-item-received
         h3.card-details-item-title {{_ 'card-received'}}
         h3.card-details-item-title {{_ 'card-received'}}
-        if receivedAt
+        if getReceived
           +cardReceivedDate
           +cardReceivedDate
         else
         else
-          a.js-received-date {{_ 'add'}}
+          if canModifyCard
+            a.js-received-date {{_ 'add'}}
 
 
       .card-details-item.card-details-item-start
       .card-details-item.card-details-item-start
         h3.card-details-item-title {{_ 'card-start'}}
         h3.card-details-item-title {{_ 'card-start'}}
-        if startAt
+        if getStart
           +cardStartDate
           +cardStartDate
         else
         else
-          a.js-start-date {{_ 'add'}}
+          if canModifyCard
+            a.js-start-date {{_ 'add'}}
 
 
       .card-details-item.card-details-item-due
       .card-details-item.card-details-item-due
         h3.card-details-item-title {{_ 'card-due'}}
         h3.card-details-item-title {{_ 'card-due'}}
-        if dueAt
+        if getDue
           +cardDueDate
           +cardDueDate
         else
         else
-          a.js-due-date {{_ 'add'}}
+          if canModifyCard
+            a.js-due-date {{_ 'add'}}
 
 
       .card-details-item.card-details-item-end
       .card-details-item.card-details-item-end
         h3.card-details-item-title {{_ 'card-end'}}
         h3.card-details-item-title {{_ 'card-end'}}
-        if endAt
+        if getEnd
           +cardEndDate
           +cardEndDate
         else
         else
-          a.js-end-date {{_ 'add'}}
+          if canModifyCard
+            a.js-end-date {{_ 'add'}}
 
 
     .card-details-items
     .card-details-items
       .card-details-item.card-details-item-members
       .card-details-item.card-details-item-members
         h3.card-details-item-title {{_ 'members'}}
         h3.card-details-item-title {{_ 'members'}}
-        each members
+        each getMembers
           +userAvatar(userId=this cardId=../_id)
           +userAvatar(userId=this cardId=../_id)
           | {{! XXX Hack to hide syntaxic coloration /// }}
           | {{! XXX Hack to hide syntaxic coloration /// }}
         if canModifyCard
         if canModifyCard
@@ -66,9 +83,16 @@ template(name="cardDetails")
             i.fa.fa-plus
             i.fa.fa-plus
 
 
     .card-details-items
     .card-details-items
-      if spentTime
+      each customFieldsWD
+        .card-details-item.card-details-item-customfield
+          h3.card-details-item-title
+            = definition.name
+          +cardCustomField
+
+    .card-details-items
+      if getSpentTime
         .card-details-item.card-details-item-spent
         .card-details-item.card-details-item-spent
-          if isOvertime
+          if getIsOvertime
             h3.card-details-item-title {{_ 'overtime-hours'}}
             h3.card-details-item-title {{_ 'overtime-hours'}}
           else
           else
             h3.card-details-item-title {{_ 'spent-time-hours'}}
             h3.card-details-item-title {{_ 'spent-time-hours'}}
@@ -79,15 +103,15 @@ template(name="cardDetails")
       h3.card-details-item-title {{_ 'description'}}
       h3.card-details-item-title {{_ 'description'}}
       +inlinedCardDescription(classNames="card-description js-card-description")
       +inlinedCardDescription(classNames="card-description js-card-description")
         +editor(autofocus=true)
         +editor(autofocus=true)
-          | {{getUnsavedValue 'cardDescription' _id description}}
+          | {{getUnsavedValue 'cardDescription' _id getDescription}}
         .edit-controls.clearfix
         .edit-controls.clearfix
           button.primary(type="submit") {{_ 'save'}}
           button.primary(type="submit") {{_ 'save'}}
           a.fa.fa-times-thin.js-close-inlined-form
           a.fa.fa-times-thin.js-close-inlined-form
       else
       else
         a.js-open-inlined-form
         a.js-open-inlined-form
-          if description
+          if getDescription
             +viewer
             +viewer
-              = description
+              = getDescription
           else
           else
             | {{_ 'edit'}}
             | {{_ 'edit'}}
         if (hasUnsavedValue 'cardDescription' _id)
         if (hasUnsavedValue 'cardDescription' _id)
@@ -96,14 +120,51 @@ template(name="cardDetails")
             a.js-open-inlined-form {{_ 'view-it'}}
             a.js-open-inlined-form {{_ 'view-it'}}
             = ' - '
             = ' - '
             a.js-close-inlined-form {{_ 'discard'}}
             a.js-close-inlined-form {{_ 'discard'}}
-    else if description
+    else if getDescription
       h3.card-details-item-title {{_ 'description'}}
       h3.card-details-item-title {{_ 'description'}}
       +viewer
       +viewer
-        = description
+        = getDescription
+
+    .card-details-items
+      .card-details-item.card-details-item-name
+        h3.card-details-item-title {{_ 'requested-by'}}
+        if canModifyCard
+          +inlinedForm(classNames="js-card-details-requester")
+            +editCardRequesterForm
+          else
+            a.js-open-inlined-form
+              if getRequestedBy
+                +viewer
+                  = getRequestedBy
+              else
+                | {{_ 'add'}}
+        else if getRequestedBy
+          +viewer
+            = getRequestedBy
+
+      .card-details-item.card-details-item-name
+        h3.card-details-item-title {{_ 'assigned-by'}}
+        if canModifyCard
+          +inlinedForm(classNames="js-card-details-assigner")
+            +editCardAssignerForm
+          else
+            a.js-open-inlined-form
+              if getAssignedBy
+                +viewer
+                  = getAssignedBy
+              else
+                | {{_ 'add'}}
+        else if getRequestedBy
+          +viewer
+            = getAssignedBy
 
 
     hr
     hr
     +checklists(cardId = _id)
     +checklists(cardId = _id)
 
 
+    if currentBoard.allowsSubtasks
+      hr
+      +subtasks(cardId = _id)
+
     hr
     hr
     h3
     h3
       i.fa.fa-paperclip
       i.fa.fa-paperclip
@@ -112,42 +173,64 @@ template(name="cardDetails")
     +attachmentsGalery
     +attachmentsGalery
 
 
     hr
     hr
-    .activity-title
-      h3 {{ _ 'activity'}}
-      if currentUser.isBoardMember
-        .material-toggle-switch
-          span.toggle-switch-title {{_ 'hide-system-messages'}}
-          if hiddenSystemMessages
-            input.toggle-switch(type="checkbox" id="toggleButton" checked="checked")
-          else
-            input.toggle-switch(type="checkbox" id="toggleButton")
-          label.toggle-label(for="toggleButton")
+    unless currentUser.isNoComments
+      .activity-title
+        h3 {{ _ 'activity'}}
+        if currentUser.isBoardMember
+          .material-toggle-switch
+            span.toggle-switch-title {{_ 'hide-system-messages'}}
+            if hiddenSystemMessages
+              input.toggle-switch(type="checkbox" id="toggleButton" checked="checked")
+            else
+              input.toggle-switch(type="checkbox" id="toggleButton")
+            label.toggle-label(for="toggleButton")
     if currentUser.isBoardMember
     if currentUser.isBoardMember
-      +commentForm
-    if isLoaded.get
-      +activities(card=this mode="card")
+      unless currentUser.isNoComments
+        +commentForm
+    unless currentUser.isNoComments
+      if isLoaded.get
+        if isLinkedCard
+          +activities(card=this mode="linkedcard")
+        else if isLinkedBoard
+          +activities(card=this mode="linkedboard")
+        else
+          +activities(card=this mode="card")
 
 
 template(name="editCardTitleForm")
 template(name="editCardTitleForm")
-  textarea.js-edit-card-title(rows='1' autofocus)
-    = title
+  textarea.js-edit-card-title(rows='1' autofocus dir="auto")
+    = getTitle
   .edit-controls.clearfix
   .edit-controls.clearfix
     button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
     button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
     a.fa.fa-times-thin.js-close-inlined-form
     a.fa.fa-times-thin.js-close-inlined-form
 
 
+template(name="editCardRequesterForm")
+  input.js-edit-card-requester(type='text' autofocus value=getRequestedBy dir="auto")
+  .edit-controls.clearfix
+    button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
+    a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="editCardAssignerForm")
+  input.js-edit-card-assigner(type='text' autofocus value=getAssignedBy dir="auto")
+  .edit-controls.clearfix
+    button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
+    a.fa.fa-times-thin.js-close-inlined-form
+
 template(name="cardDetailsActionsPopup")
 template(name="cardDetailsActionsPopup")
   ul.pop-over-list
   ul.pop-over-list
     li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
     li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
   if canModifyCard
   if canModifyCard
     hr
     hr
     ul.pop-over-list
     ul.pop-over-list
-      li: a.js-members {{_ 'card-edit-members'}}
-      li: a.js-labels {{_ 'card-edit-labels'}}
-      li: a.js-attachments {{_ 'card-edit-attachments'}}
-      li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
-      li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
-      li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
-      li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
+      //li: a.js-members {{_ 'card-edit-members'}}
+      //li: a.js-labels {{_ 'card-edit-labels'}}
+      //li: a.js-attachments {{_ 'card-edit-attachments'}}
+      li: a.js-custom-fields {{_ 'card-edit-custom-fields'}}
+      //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
+      //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
+      //li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
+      //li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
       li: a.js-spent-time {{_ 'editCardSpentTimePopup-title'}}
       li: a.js-spent-time {{_ 'editCardSpentTimePopup-title'}}
+      li: a.js-set-card-color {{_ 'setCardColorPopup-title'}}
     hr
     hr
     ul.pop-over-list
     ul.pop-over-list
       li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
       li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
@@ -167,10 +250,9 @@ template(name="moveCardPopup")
 template(name="copyCardPopup")
 template(name="copyCardPopup")
   label(for='copy-card-title') {{_ 'title'}}:
   label(for='copy-card-title') {{_ 'title'}}:
   textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
   textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
-    = title
+    = getTitle
   +boardsAndLists
   +boardsAndLists
 
 
-
 template(name="copyChecklistToManyCardsPopup")
 template(name="copyChecklistToManyCardsPopup")
   label(for='copy-checklist-cards-title') {{_ 'copyChecklistToManyCardsPopup-instructions'}}:
   label(for='copy-checklist-cards-title') {{_ 'copyChecklistToManyCardsPopup-instructions'}}:
   textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
   textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
@@ -179,7 +261,7 @@ template(name="copyChecklistToManyCardsPopup")
 
 
 template(name="boardsAndLists")
 template(name="boardsAndLists")
   label {{_ 'boards'}}:
   label {{_ 'boards'}}:
-  select.js-select-boards
+  select.js-select-boards(autofocus)
     each boards
     each boards
       if $eq _id currentBoard._id
       if $eq _id currentBoard._id
         option(value="{{_id}}" selected) {{_ 'current'}}
         option(value="{{_id}}" selected) {{_ 'current'}}
@@ -217,14 +299,49 @@ template(name="cardMorePopup")
       span {{_ 'link-card'}}
       span {{_ 'link-card'}}
       = ' '
       = ' '
       i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
       i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
-      input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}")
+      input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}" autofocus="autofocus")
       button.js-copy-card-link-to-clipboard(class="btn") {{_ 'copy-card-link-to-clipboard'}}
       button.js-copy-card-link-to-clipboard(class="btn") {{_ 'copy-card-link-to-clipboard'}}
     span.clearfix
     span.clearfix
     br
     br
+    h2 {{_ 'change-card-parent'}}
+    label {{_ 'source-board'}}:
+      select.js-field-parent-board
+        if isTopLevel
+          option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
+        else
+          option(value="none") {{_ 'custom-field-dropdown-none'}}
+        each boards
+          if isParentBoard
+            option(value="{{_id}}" selected) {{title}}
+          else
+            option(value="{{_id}}") {{title}}
+
+    label {{_ 'parent-card'}}:
+      select.js-field-parent-card
+        if isTopLevel
+          option(value="none" selected) {{_ 'custom-field-dropdown-none'}}
+        else
+          option(value="none") {{_ 'custom-field-dropdown-none'}}
+          each cards
+            if isParentCard
+              option(value="{{_id}}" selected) {{title}}
+            else
+              option(value="{{_id}}") {{title}}
+    br
     | {{_ 'added'}}
     | {{_ 'added'}}
     span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
     span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
     a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
     a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
 
 
+template(name="setCardColorPopup")
+  form.edit-label
+    .palette-colors: each colors
+      unless $eq color 'white'
+        span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
+          if(isSelected color)
+            i.fa.fa-check
+    button.primary.confirm.js-submit {{_ 'save'}}
+    button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
+
 template(name="cardDeletePopup")
 template(name="cardDeletePopup")
   p {{_ "card-delete-pop"}}
   p {{_ "card-delete-pop"}}
   unless archived
   unless archived

+ 319 - 90
client/components/cards/cardDetails.js

@@ -1,5 +1,10 @@
 const subManager = new SubsManager();
 const subManager = new SubsManager();
-const { calculateIndexData } = Utils;
+const { calculateIndexData, enableClickOnTouch } = Utils;
+
+let cardColors;
+Meteor.startup(() => {
+  cardColors = Cards.simpleSchema()._schema.color.allowedValues;
+});
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   mixins() {
   mixins() {
@@ -20,9 +25,14 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   onCreated() {
   onCreated() {
+    this.currentBoard = Boards.findOne(Session.get('currentBoard'));
     this.isLoaded = new ReactiveVar(false);
     this.isLoaded = new ReactiveVar(false);
-    this.parentComponent().parentComponent().showOverlay.set(true);
-    this.parentComponent().parentComponent().mouseHasEnterCardDetails = false;
+    const boardBody =  this.parentComponent().parentComponent();
+    //in Miniview parent is Board, not BoardBody.
+    if (boardBody !== null) {
+      boardBody.showOverlay.set(true);
+      boardBody.mouseHasEnterCardDetails = false;
+    }
     this.calculateNextPeak();
     this.calculateNextPeak();
 
 
     Meteor.subscribe('unsaved-edits');
     Meteor.subscribe('unsaved-edits');
@@ -44,7 +54,8 @@ BlazeComponent.extendComponent({
   scrollParentContainer() {
   scrollParentContainer() {
     const cardPanelWidth = 510;
     const cardPanelWidth = 510;
     const bodyBoardComponent = this.parentComponent().parentComponent();
     const bodyBoardComponent = this.parentComponent().parentComponent();
-
+    //On Mobile View Parent is Board, Not Board Body. I cant see how this funciton should work then.
+    if (bodyBoardComponent === null) return;
     const $cardView = this.$(this.firstNode());
     const $cardView = this.$(this.firstNode());
     const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
     const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
     const cardContainerScroll = $cardContainer.scrollLeft();
     const cardContainerScroll = $cardContainer.scrollLeft();
@@ -63,10 +74,52 @@ BlazeComponent.extendComponent({
     if (offset) {
     if (offset) {
       bodyBoardComponent.scrollLeft(cardContainerScroll + offset);
       bodyBoardComponent.scrollLeft(cardContainerScroll + offset);
     }
     }
+
+    //Scroll top
+    const cardViewStartTop = $cardView.offset().top;
+    const cardContainerScrollTop = $cardContainer.scrollTop();
+
+    let topOffset = false;
+    if(cardViewStartTop !== 100){
+      topOffset = cardViewStartTop - 100;
+    }
+    if(topOffset !== false) {
+      bodyBoardComponent.scrollTop(cardContainerScrollTop + topOffset);
+    }
+
+  },
+
+  presentParentTask() {
+    let result = this.currentBoard.presentParentTask;
+    if ((result === null) || (result === undefined)) {
+      result = 'no-parent';
+    }
+    return result;
+  },
+
+  linkForCard() {
+    const card = this.currentData();
+    let result = '#';
+    if (card) {
+      const board = Boards.findOne(card.boardId);
+      if (board) {
+        result = FlowRouter.url('card', {
+          boardId: card.boardId,
+          slug: board.slug,
+          cardId: card._id,
+        });
+      }
+    }
+    return result;
   },
   },
 
 
   onRendered() {
   onRendered() {
-    if (!Utils.isMiniScreen()) this.scrollParentContainer();
+    if (!Utils.isMiniScreen()) {
+      Meteor.setTimeout(() => {
+        $('.card-details').mCustomScrollbar({theme:'minimal-dark', setWidth: false, setLeft: 0, scrollbarPosition: 'outside', mouseWheel: true });
+        this.scrollParentContainer();
+      }, 500);
+    }
     const $checklistsDom = this.$('.card-checklist-items');
     const $checklistsDom = this.$('.card-checklist-items');
 
 
     $checklistsDom.sortable({
     $checklistsDom.sortable({
@@ -102,6 +155,47 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
+    // ugly touch event hotfix
+    enableClickOnTouch('.card-checklist-items .js-checklist');
+
+    const $subtasksDom = this.$('.card-subtasks-items');
+
+    $subtasksDom.sortable({
+      tolerance: 'pointer',
+      helper: 'clone',
+      handle: '.subtask-title',
+      items: '.js-subtasks',
+      placeholder: 'subtasks placeholder',
+      distance: 7,
+      start(evt, ui) {
+        ui.placeholder.height(ui.helper.height());
+        EscapeActions.executeUpTo('popup-close');
+      },
+      stop(evt, ui) {
+        let prevChecklist = ui.item.prev('.js-subtasks').get(0);
+        if (prevChecklist) {
+          prevChecklist = Blaze.getData(prevChecklist).subtask;
+        }
+        let nextChecklist = ui.item.next('.js-subtasks').get(0);
+        if (nextChecklist) {
+          nextChecklist = Blaze.getData(nextChecklist).subtask;
+        }
+        const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
+
+        $subtasksDom.sortable('cancel');
+        const subtask = Blaze.getData(ui.item.get(0)).subtask;
+
+        Subtasks.update(subtask._id, {
+          $set: {
+            subtaskSort: sortIndex.base,
+          },
+        });
+      },
+    });
+
+    // ugly touch event hotfix
+    enableClickOnTouch('.card-subtasks-items .js-subtasks');
+
     function userIsMember() {
     function userIsMember() {
       return Meteor.user() && Meteor.user().isBoardMember();
       return Meteor.user() && Meteor.user().isBoardMember();
     }
     }
@@ -111,11 +205,17 @@ BlazeComponent.extendComponent({
       if ($checklistsDom.data('sortable')) {
       if ($checklistsDom.data('sortable')) {
         $checklistsDom.sortable('option', 'disabled', !userIsMember());
         $checklistsDom.sortable('option', 'disabled', !userIsMember());
       }
       }
+      if ($subtasksDom.data('sortable')) {
+        $subtasksDom.sortable('option', 'disabled', !userIsMember());
+      }
     });
     });
   },
   },
 
 
   onDestroyed() {
   onDestroyed() {
-    this.parentComponent().parentComponent().showOverlay.set(false);
+    const parentComponent =  this.parentComponent().parentComponent();
+    //on mobile view parent is Board, not board body.
+    if (parentComponent === null) return;
+    parentComponent.showOverlay.set(false);
   },
   },
 
 
   events() {
   events() {
@@ -146,6 +246,20 @@ BlazeComponent.extendComponent({
           this.data().setTitle(title);
           this.data().setTitle(title);
         }
         }
       },
       },
+      'submit .js-card-details-assigner'(evt) {
+        evt.preventDefault();
+        const assigner = this.currentComponent().getValue().trim();
+        if (assigner) {
+          this.data().setAssignedBy(assigner);
+        }
+      },
+      'submit .js-card-details-requester'(evt) {
+        evt.preventDefault();
+        const requester = this.currentComponent().getValue().trim();
+        if (requester) {
+          this.data().setRequestedBy(requester);
+        }
+      },
       'click .js-member': Popup.open('cardMember'),
       'click .js-member': Popup.open('cardMember'),
       'click .js-add-members': Popup.open('cardMembers'),
       'click .js-add-members': Popup.open('cardMembers'),
       'click .js-add-labels': Popup.open('cardLabels'),
       'click .js-add-labels': Popup.open('cardLabels'),
@@ -154,8 +268,11 @@ BlazeComponent.extendComponent({
       'click .js-due-date': Popup.open('editCardDueDate'),
       'click .js-due-date': Popup.open('editCardDueDate'),
       'click .js-end-date': Popup.open('editCardEndDate'),
       'click .js-end-date': Popup.open('editCardEndDate'),
       'mouseenter .js-card-details' () {
       'mouseenter .js-card-details' () {
-        this.parentComponent().parentComponent().showOverlay.set(true);
-        this.parentComponent().parentComponent().mouseHasEnterCardDetails = true;
+        const parentComponent =  this.parentComponent().parentComponent();
+        //on mobile view parent is Board, not BoardBody.
+        if (parentComponent === null) return;
+        parentComponent.showOverlay.set(true);
+        parentComponent.mouseHasEnterCardDetails = true;
       },
       },
       'click #toggleButton'() {
       'click #toggleButton'() {
         Meteor.call('toggleSystemMessages');
         Meteor.call('toggleSystemMessages');
@@ -180,7 +297,7 @@ BlazeComponent.extendComponent({
   close(isReset = false) {
   close(isReset = false) {
     if (this.isOpen.get() && !isReset) {
     if (this.isOpen.get() && !isReset) {
       const draft = this.getValue().trim();
       const draft = this.getValue().trim();
-      if (draft !== Cards.findOne(Session.get('currentCard')).description) {
+      if (draft !== Cards.findOne(Session.get('currentCard')).getDescription()) {
         UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
         UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
       }
       }
     }
     }
@@ -215,6 +332,7 @@ Template.cardDetailsActionsPopup.events({
   'click .js-members': Popup.open('cardMembers'),
   'click .js-members': Popup.open('cardMembers'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
   'click .js-attachments': Popup.open('cardAttachments'),
+  'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
   'click .js-due-date': Popup.open('editCardDueDate'),
   'click .js-due-date': Popup.open('editCardDueDate'),
@@ -223,6 +341,7 @@ Template.cardDetailsActionsPopup.events({
   'click .js-move-card': Popup.open('moveCard'),
   'click .js-move-card': Popup.open('moveCard'),
   'click .js-copy-card': Popup.open('copyCard'),
   'click .js-copy-card': Popup.open('copyCard'),
   'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
   'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
+  'click .js-set-card-color': Popup.open('setCardColor'),
   'click .js-move-card-to-top' (evt) {
   'click .js-move-card-to-top' (evt) {
     evt.preventDefault();
     evt.preventDefault();
     const minOrder = _.min(this.list().cards(this.swimlaneId).map((c) => c.sort));
     const minOrder = _.min(this.list().cards(this.swimlaneId).map((c) => c.sort));
@@ -262,20 +381,47 @@ Template.editCardTitleForm.events({
   },
   },
 });
 });
 
 
+Template.editCardRequesterForm.onRendered(function() {
+  autosize(this.$('.js-edit-card-requester'));
+});
+
+Template.editCardRequesterForm.events({
+  'keydown .js-edit-card-requester'(evt) {
+    // If enter key was pressed, submit the data
+    if (evt.keyCode === 13) {
+      $('.js-submit-edit-card-requester-form').click();
+    }
+  },
+});
+
+Template.editCardAssignerForm.onRendered(function() {
+  autosize(this.$('.js-edit-card-assigner'));
+});
+
+Template.editCardAssignerForm.events({
+  'keydown .js-edit-card-assigner'(evt) {
+    // If enter key was pressed, submit the data
+    if (evt.keyCode === 13) {
+      $('.js-submit-edit-card-assigner-form').click();
+    }
+  },
+});
+
 Template.moveCardPopup.events({
 Template.moveCardPopup.events({
   'click .js-done' () {
   'click .js-done' () {
     // XXX We should *not* get the currentCard from the global state, but
     // XXX We should *not* get the currentCard from the global state, but
     // instead from a “component” state.
     // instead from a “component” state.
     const card = Cards.findOne(Session.get('currentCard'));
     const card = Cards.findOne(Session.get('currentCard'));
+    const bSelect = $('.js-select-boards')[0];
+    const boardId = bSelect.options[bSelect.selectedIndex].value;
     const lSelect = $('.js-select-lists')[0];
     const lSelect = $('.js-select-lists')[0];
-    const newListId = lSelect.options[lSelect.selectedIndex].value;
+    const listId = lSelect.options[lSelect.selectedIndex].value;
     const slSelect = $('.js-select-swimlanes')[0];
     const slSelect = $('.js-select-swimlanes')[0];
-    card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
-    card.move(card.swimlaneId, newListId, 0);
+    const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+    card.move(boardId, swimlaneId, listId, 0);
     Popup.close();
     Popup.close();
   },
   },
 });
 });
-
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     subManager.subscribe('board', Session.get('currentBoard'));
     subManager.subscribe('board', Session.get('currentBoard'));
@@ -286,6 +432,7 @@ BlazeComponent.extendComponent({
     const boards = Boards.find({
     const boards = Boards.find({
       archived: false,
       archived: false,
       'members.userId': Meteor.userId(),
       'members.userId': Meteor.userId(),
+      _id: {$ne: Meteor.user().getTemplatesBoardId()},
     }, {
     }, {
       sort: ['title'],
       sort: ['title'],
     });
     });
@@ -315,14 +462,12 @@ BlazeComponent.extendComponent({
 Template.copyCardPopup.events({
 Template.copyCardPopup.events({
   'click .js-done'() {
   'click .js-done'() {
     const card = Cards.findOne(Session.get('currentCard'));
     const card = Cards.findOne(Session.get('currentCard'));
-    const oldId = card._id;
-    card._id = null;
     const lSelect = $('.js-select-lists')[0];
     const lSelect = $('.js-select-lists')[0];
-    card.listId = lSelect.options[lSelect.selectedIndex].value;
+    listId = lSelect.options[lSelect.selectedIndex].value;
     const slSelect = $('.js-select-swimlanes')[0];
     const slSelect = $('.js-select-swimlanes')[0];
-    card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+    const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
     const bSelect = $('.js-select-boards')[0];
     const bSelect = $('.js-select-boards')[0];
-    card.boardId = bSelect.options[bSelect.selectedIndex].value;
+    const boardId = bSelect.options[bSelect.selectedIndex].value;
     const textarea = $('#copy-card-title');
     const textarea = $('#copy-card-title');
     const title = textarea.val().trim();
     const title = textarea.val().trim();
     // insert new card to the bottom of new list
     // insert new card to the bottom of new list
@@ -331,39 +476,13 @@ Template.copyCardPopup.events({
     if (title) {
     if (title) {
       card.title = title;
       card.title = title;
       card.coverId = '';
       card.coverId = '';
-      const _id = Cards.insert(card);
+      const _id = card.copy(boardId, swimlaneId, listId);
       // In case the filter is active we need to add the newly inserted card in
       // 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
       // the list of exceptions -- cards that are not filtered. Otherwise the
       // card will disappear instantly.
       // card will disappear instantly.
       // See https://github.com/wekan/wekan/issues/80
       // See https://github.com/wekan/wekan/issues/80
       Filter.addException(_id);
       Filter.addException(_id);
 
 
-      // copy checklists
-      let cursor = Checklists.find({cardId: oldId});
-      cursor.forEach(function() {
-        'use strict';
-        const checklist = arguments[0];
-        const checklistId = checklist._id;
-        checklist.cardId = _id;
-        checklist._id = null;
-        const newChecklistId = Checklists.insert(checklist);
-        ChecklistItems.find({checklistId}).forEach(function(item) {
-          item._id = null;
-          item.checklistId = newChecklistId;
-          item.cardId = _id;
-          ChecklistItems.insert(item);
-        });
-      });
-
-      // copy card comments
-      cursor = CardComments.find({cardId: oldId});
-      cursor.forEach(function () {
-        'use strict';
-        const comment = arguments[0];
-        comment.cardId = _id;
-        comment._id = null;
-        CardComments.insert(comment);
-      });
       Popup.close();
       Popup.close();
     }
     }
   },
   },
@@ -400,30 +519,23 @@ Template.copyChecklistToManyCardsPopup.events({
         Filter.addException(_id);
         Filter.addException(_id);
 
 
         // copy checklists
         // copy checklists
-        let cursor = Checklists.find({cardId: oldId});
+        Checklists.find({cardId: oldId}).forEach((ch) => {
+          ch.copy(_id);
+        });
+
+        // copy subtasks
+        cursor = Cards.find({parentId: oldId});
         cursor.forEach(function() {
         cursor.forEach(function() {
           'use strict';
           'use strict';
-          const checklist = arguments[0];
-          const checklistId = checklist._id;
-          checklist.cardId = _id;
-          checklist._id = null;
-          const newChecklistId = Checklists.insert(checklist);
-          ChecklistItems.find({checklistId}).forEach(function(item) {
-            item._id = null;
-            item.checklistId = newChecklistId;
-            item.cardId = _id;
-            ChecklistItems.insert(item);
-          });
+          const subtask = arguments[0];
+          subtask.parentId = _id;
+          subtask._id = null;
+          /* const newSubtaskId = */ Cards.insert(subtask);
         });
         });
 
 
         // copy card comments
         // copy card comments
-        cursor = CardComments.find({cardId: oldId});
-        cursor.forEach(function () {
-          'use strict';
-          const comment = arguments[0];
-          comment.cardId = _id;
-          comment._id = null;
-          CardComments.insert(comment);
+        CardComments.find({cardId: oldId}).forEach((cmt) => {
+          cmt.copy(_id);
         });
         });
       }
       }
       Popup.close();
       Popup.close();
@@ -431,36 +543,153 @@ Template.copyChecklistToManyCardsPopup.events({
   },
   },
 });
 });
 
 
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentCard = this.currentData();
+    this.currentColor = new ReactiveVar(this.currentCard.color);
+  },
+
+  colors() {
+    return cardColors.map((color) => ({ color, name: '' }));
+  },
 
 
-Template.cardMorePopup.events({
-  'click .js-copy-card-link-to-clipboard' () {
-    // Clipboard code from:
-    // https://stackoverflow.com/questions/6300213/copy-selected-text-to-the-clipboard-without-using-flash-must-be-cross-browser
-    const StringToCopyElement = document.getElementById('cardURL');
-    StringToCopyElement.select();
-    if (document.execCommand('copy')) {
-      StringToCopyElement.blur();
+  isSelected(color) {
+    if (this.currentColor.get() === null) {
+      return color === 'white';
+    }
+    return this.currentColor.get() === color;
+  },
+
+  events() {
+    return [{
+      'click .js-palette-color'() {
+        this.currentColor.set(this.currentData().color);
+      },
+      'click .js-submit' () {
+        this.currentCard.setColor(this.currentColor.get());
+        Popup.close();
+      },
+      'click .js-remove-color'() {
+        this.currentCard.setColor(null);
+        Popup.close();
+      },
+    }];
+  },
+}).register('setCardColorPopup');
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentCard = this.currentData();
+    this.parentBoard = new ReactiveVar(null);
+    this.parentCard = this.currentCard.parentCard();
+    if (this.parentCard) {
+      const list = $('.js-field-parent-card');
+      list.val(this.parentCard._id);
+      this.parentBoard.set(this.parentCard.board()._id);
     } else {
     } else {
-      document.getElementById('cardURL').selectionStart = 0;
-      document.getElementById('cardURL').selectionEnd = 999;
-      document.execCommand('copy');
-      if (window.getSelection) {
-        if (window.getSelection().empty) { // Chrome
-          window.getSelection().empty();
-        } else if (window.getSelection().removeAllRanges) { // Firefox
-          window.getSelection().removeAllRanges();
-        }
-      } else if (document.selection) { // IE?
-        document.selection.empty();
-      }
+      this.parentBoard.set(null);
     }
     }
   },
   },
-  'click .js-delete': Popup.afterConfirm('cardDelete', function () {
-    Popup.close();
-    Cards.remove(this._id);
-    Utils.goBoardId(this.boardId);
-  }),
-});
+
+  boards() {
+    const boards = Boards.find({
+      archived: false,
+      'members.userId': Meteor.userId(),
+      _id: {
+        $ne: Meteor.user().getTemplatesBoardId(),
+      },
+    }, {
+      sort: ['title'],
+    });
+    return boards;
+  },
+
+  cards() {
+    const currentId = Session.get('currentCard');
+    if (this.parentBoard.get()) {
+      return Cards.find({
+        boardId: this.parentBoard.get(),
+        _id: {$ne: currentId},
+      });
+    } else {
+      return [];
+    }
+  },
+
+  isParentBoard() {
+    const board = this.currentData();
+    if (this.parentBoard.get()) {
+      return board._id === this.parentBoard.get();
+    }
+    return false;
+  },
+
+  isParentCard() {
+    const card = this.currentData();
+    if (this.parentCard) {
+      return card._id === this.parentCard;
+    }
+    return false;
+  },
+
+  setParentCardId(cardId) {
+    if (cardId) {
+      this.parentCard = Cards.findOne(cardId);
+    } else {
+      this.parentCard = null;
+    }
+    this.currentCard.setParentId(cardId);
+  },
+
+  events() {
+    return [{
+      'click .js-copy-card-link-to-clipboard' () {
+        // Clipboard code from:
+        // https://stackoverflow.com/questions/6300213/copy-selected-text-to-the-clipboard-without-using-flash-must-be-cross-browser
+        const StringToCopyElement = document.getElementById('cardURL');
+        StringToCopyElement.select();
+        if (document.execCommand('copy')) {
+          StringToCopyElement.blur();
+        } else {
+          document.getElementById('cardURL').selectionStart = 0;
+          document.getElementById('cardURL').selectionEnd = 999;
+          document.execCommand('copy');
+          if (window.getSelection) {
+            if (window.getSelection().empty) { // Chrome
+              window.getSelection().empty();
+            } else if (window.getSelection().removeAllRanges) { // Firefox
+              window.getSelection().removeAllRanges();
+            }
+          } else if (document.selection) { // IE?
+            document.selection.empty();
+          }
+        }
+      },
+      'click .js-delete': Popup.afterConfirm('cardDelete', function () {
+        Popup.close();
+        Cards.remove(this._id);
+        Utils.goBoardId(this.boardId);
+      }),
+      'change .js-field-parent-board'(evt) {
+        const selection = $(evt.currentTarget).val();
+        const list = $('.js-field-parent-card');
+        if (selection === 'none') {
+          this.parentBoard.set(null);
+        } else {
+          subManager.subscribe('board', $(evt.currentTarget).val());
+          this.parentBoard.set(selection);
+          list.prop('disabled', false);
+        }
+        this.setParentCardId(null);
+      },
+      'change .js-field-parent-card'(evt) {
+        const selection = $(evt.currentTarget).val();
+        this.setParentCardId(selection);
+      },
+    }];
+  },
+}).register('cardMorePopup');
+
 
 
 // Close the card details pane by pressing escape
 // Close the card details pane by pressing escape
 EscapeActions.register('detailsPane',
 EscapeActions.register('detailsPane',

+ 107 - 7
client/components/cards/cardDetails.styl

@@ -1,11 +1,12 @@
 @import 'nib'
 @import 'nib'
 
 
 .card-details
 .card-details
-  padding: 0 20px
+  padding: 0
   flex-shrink: 0
   flex-shrink: 0
-  flex-basis: 470px
+  flex-basis: 510px
   will-change: flex-basis
   will-change: flex-basis
-  overflow: hidden
+  overflow-y: scroll
+  overflow-x: hidden
   background: darken(white, 3%)
   background: darken(white, 3%)
   border-radius: bottom 3px
   border-radius: bottom 3px
   z-index: 20 !important
   z-index: 20 !important
@@ -13,8 +14,16 @@
   box-shadow: 0 0 7px 0 darken(white, 30%)
   box-shadow: 0 0 7px 0 darken(white, 30%)
   transition: flex-basis 0.1s
   transition: flex-basis 0.1s
 
 
+  .mCustomScrollBox
+    padding-left: 0
+
+  .ps-scrollbar-y-rail
+    pointer-event: all
+    position: absolute;
+
   .card-details-canvas
   .card-details-canvas
     width: 470px
     width: 470px
+    padding-left: 20px;
 
 
   .card-details-header
   .card-details-header
     margin: 0 -20px 5px
     margin: 0 -20px 5px
@@ -46,6 +55,12 @@
       margin: 7px 0 0
       margin: 7px 0 0
       padding: 0
       padding: 0
 
 
+    .linked-card-location
+      font-style: italic
+      font-size: 1em
+      margin-bottom: 0
+      & p
+        margin-bottom: 0
 
 
     form.inlined-form
     form.inlined-form
       margin-top: 5px
       margin-top: 5px
@@ -69,6 +84,7 @@
 
 
   .card-details-items
   .card-details-items
     display: flex
     display: flex
+    flex-wrap: wrap
     margin: 15px 0
     margin: 15px 0
 
 
     .card-details-item
     .card-details-item
@@ -80,9 +96,11 @@
       &.card-details-item-received,
       &.card-details-item-received,
       &.card-details-item-start,
       &.card-details-item-start,
       &.card-details-item-due,
       &.card-details-item-due,
-      &.card-details-item-end
-        width: 50%
-        flex-shrink: 1
+      &.card-details-item-end,
+      &.card-details-item-customfield,
+      &.card-details-item-name
+        max-width: 50%
+        flex-grow: 1
 
 
   .card-details-item-title
   .card-details-item-title
     font-size: 16px
     font-size: 16px
@@ -115,10 +133,92 @@ input[type="submit"].attachment-add-link-submit
 
 
     .card-details-canvas
     .card-details-canvas
       width: 100%
       width: 100%
-
+      padding-left: 0px;
+ 
     .card-details-header
     .card-details-header
       .close-card-details
       .close-card-details
         margin-right: 0px
         margin-right: 0px
 
 
       .card-details-menu
       .card-details-menu
         margin-right: 10px
         margin-right: 10px
+
+card-details-color(background, color...)
+  background: background !important
+  if color
+    color: color !important //overwrite text for better visibility
+
+.card-details-white
+  card-details-color(unset, #000) //Black text for better visibility
+  border: 1px solid #eee
+
+.card-details-green
+  card-details-color(#3cb500, #ffffff) //White text for better visibility
+
+.card-details-yellow
+  card-details-color(#fad900, #000) //Black text for better visibility
+
+.card-details-orange
+  card-details-color(#ff9f19, #000) //Black text for better visibility
+
+.card-details-red
+  card-details-color(#eb4646, #ffffff) //White text for better visibility
+
+.card-details-purple
+  card-details-color(#a632db, #ffffff) //White text for better visibility
+
+.card-details-blue
+  card-details-color(#0079bf, #ffffff) //White text for better visibility
+
+.card-details-pink
+  card-details-color(#ff78cb, #000) //Black text for better visibility
+
+.card-details-sky
+  card-details-color(#00c2e0, #ffffff) //White text for better visibility
+
+.card-details-black
+  card-details-color(#4d4d4d, #ffffff) //White text for better visibility
+
+.card-details-lime
+  card-details-color(#51e898, #000) //Black text for better visibility
+
+.card-details-silver
+  card-details-color(#c0c0c0, #000) //Black text for better visibility
+
+.card-details-peachpuff
+  card-details-color(#ffdab9, #000) //Black text for better visibility
+
+.card-details-crimson
+  card-details-color(#dc143c, #ffffff) //White text for better visibility
+
+.card-details-plum
+  card-details-color(#dda0dd, #000) //Black text for better visibility
+
+.card-details-darkgreen
+  card-details-color(#006400, #ffffff) //White text for better visibility
+
+.card-details-slateblue
+  card-details-color(#6a5acd, #ffffff) //White text for better visibility
+
+.card-details-magenta
+  card-details-color(#ff00ff, #ffffff) //White text for better visibility
+
+.card-details-gold
+  card-details-color(#ffd700, #000) //Black text for better visibility
+
+.card-details-navy
+  card-details-color(#000080, #ffffff) //White text for better visibility
+
+.card-details-gray
+  card-details-color(#808080, #ffffff) //White text for better visibility
+
+.card-details-saddlebrown
+  card-details-color(#8b4513, #ffffff) //White text for better visibility
+
+.card-details-paleturquoise
+  card-details-color(#afeeee, #000) //Black text for better visibility
+
+.card-details-mistyrose
+  card-details-color(#ffe4e1, #000) //Black text for better visibility
+
+.card-details-indigo
+  card-details-color(#4b0082, #ffffff) //White text for better visibility

+ 4 - 4
client/components/cards/cardTime.jade

@@ -3,10 +3,10 @@ template(name="editCardSpentTime")
     form.edit-time
     form.edit-time
       .fields
       .fields
         label(for="time") {{_ 'time'}}
         label(for="time") {{_ 'time'}}
-        input.js-time-field#time(type="number" step="0.01" name="time" value="{{card.spentTime}}" placeholder=timeFormat autofocus)
+        input.js-time-field#time(type="number" step="0.01" name="time" value="{{card.getSpentTime}}" placeholder=timeFormat autofocus)
         label(for="overtime") {{_ 'overtime'}}
         label(for="overtime") {{_ 'overtime'}}
         a.js-toggle-overtime
         a.js-toggle-overtime
-          .materialCheckBox#overtime(class="{{#if card.isOvertime}}is-checked{{/if}}" name="overtime")
+          .materialCheckBox#overtime(class="{{#if getIsOvertime}}is-checked{{/if}}" name="overtime")
 
 
       if error.get
       if error.get
         .warning {{_ error.get}}
         .warning {{_ error.get}}
@@ -15,8 +15,8 @@ template(name="editCardSpentTime")
 
 
 template(name="timeBadge")
 template(name="timeBadge")
   if canModifyCard
   if canModifyCard
-    a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if isOvertime}}card-label-red{{else}}card-label-green{{/if}}")
+    a.js-edit-time.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
       | {{showTime}}
       | {{showTime}}
   else
   else
-    a.card-time(title="{{showTitle}}" class="{{#if isOvertime}}card-label-red{{else}}card-label-green{{/if}}")
+    a.card-time(title="{{showTitle}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
       | {{showTime}}
       | {{showTime}}

+ 8 - 11
client/components/cards/cardTime.js

@@ -7,17 +7,17 @@ BlazeComponent.extendComponent({
     this.card = this.data();
     this.card = this.data();
   },
   },
   toggleOvertime() {
   toggleOvertime() {
-    this.card.isOvertime = !this.card.isOvertime;
+    this.card.setIsOvertime(!this.card.getIsOvertime());
     $('#overtime .materialCheckBox').toggleClass('is-checked');
     $('#overtime .materialCheckBox').toggleClass('is-checked');
 
 
     $('#overtime').toggleClass('is-checked');
     $('#overtime').toggleClass('is-checked');
   },
   },
   storeTime(spentTime, isOvertime) {
   storeTime(spentTime, isOvertime) {
     this.card.setSpentTime(spentTime);
     this.card.setSpentTime(spentTime);
-    this.card.setOvertime(isOvertime);
+    this.card.setIsOvertime(isOvertime);
   },
   },
   deleteTime() {
   deleteTime() {
-    this.card.unsetSpentTime();
+    this.card.setSpentTime(null);
   },
   },
   events() {
   events() {
     return [{
     return [{
@@ -26,7 +26,7 @@ BlazeComponent.extendComponent({
         evt.preventDefault();
         evt.preventDefault();
 
 
         const spentTime = parseFloat(evt.target.time.value);
         const spentTime = parseFloat(evt.target.time.value);
-        const isOvertime = this.card.isOvertime;
+        const isOvertime = this.card.getIsOvertime();
 
 
         if (spentTime >= 0) {
         if (spentTime >= 0) {
           this.storeTime(spentTime, isOvertime);
           this.storeTime(spentTime, isOvertime);
@@ -55,17 +55,14 @@ BlazeComponent.extendComponent({
     self.time = ReactiveVar();
     self.time = ReactiveVar();
   },
   },
   showTitle() {
   showTitle() {
-    if (this.data().isOvertime) {
-      return `${TAPi18n.__('overtime')} ${this.data().spentTime} ${TAPi18n.__('hours')}`;
+    if (this.data().getIsOvertime()) {
+      return `${TAPi18n.__('overtime')} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
     } else {
     } else {
-      return `${TAPi18n.__('card-spent')} ${this.data().spentTime} ${TAPi18n.__('hours')}`;
+      return `${TAPi18n.__('card-spent')} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
     }
     }
   },
   },
   showTime() {
   showTime() {
-    return this.data().spentTime;
-  },
-  isOvertime() {
-    return this.data().isOvertime;
+    return this.data().getSpentTime();
   },
   },
   events() {
   events() {
     return [{
     return [{

+ 3 - 4
client/components/cards/checklists.jade

@@ -27,7 +27,6 @@ template(name="checklistDetail")
         if canModifyCard
         if canModifyCard
           a.js-delete-checklist.toggle-delete-checklist-dialog {{_ "delete"}}...
           a.js-delete-checklist.toggle-delete-checklist-dialog {{_ "delete"}}...
 
 
-        span.checklist-stat(class="{{#if checklist.isFinished}}is-finished{{/if}}") {{checklist.finishedCount}}/{{checklist.itemCount}}
         if canModifyCard
         if canModifyCard
           h2.title.js-open-inlined-form.is-editable
           h2.title.js-open-inlined-form.is-editable
             +viewer
             +viewer
@@ -57,7 +56,7 @@ template(name="addChecklistItemForm")
     a.fa.fa-times-thin.js-close-inlined-form
     a.fa.fa-times-thin.js-close-inlined-form
 
 
 template(name="editChecklistItemForm")
 template(name="editChecklistItemForm")
-  textarea.js-edit-checklist-item(rows='1' autofocus)
+  textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
     if $eq type 'item'
     if $eq type 'item'
       = item.title
       = item.title
     else
     else
@@ -75,7 +74,7 @@ template(name="checklistItems")
       +inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
       +inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
         +editChecklistItemForm(type = 'item' item = item checklist = checklist)
         +editChecklistItemForm(type = 'item' item = item checklist = checklist)
       else
       else
-        +itemDetail(item = item checklist = checklist)
+        +checklistItemDetail(item = item checklist = checklist)
     if canModifyCard
     if canModifyCard
       +inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
       +inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
         +addChecklistItemForm
         +addChecklistItemForm
@@ -84,7 +83,7 @@ template(name="checklistItems")
           i.fa.fa-plus
           i.fa.fa-plus
           | {{_ 'add-checklist-item'}}...
           | {{_ 'add-checklist-item'}}...
 
 
-template(name='itemDetail')
+template(name='checklistItemDetail')
   .js-checklist-item.checklist-item
   .js-checklist-item.checklist-item
     if canModifyCard
     if canModifyCard
       .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
       .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")

+ 9 - 4
client/components/cards/checklists.js

@@ -1,4 +1,4 @@
-const { calculateIndexData } = Utils;
+const { calculateIndexData, enableClickOnTouch } = Utils;
 
 
 function initSorting(items) {
 function initSorting(items) {
   items.sortable({
   items.sortable({
@@ -36,6 +36,9 @@ function initSorting(items) {
       checklistItem.move(checklistId, sortIndex.base);
       checklistItem.move(checklistId, sortIndex.base);
     },
     },
   });
   });
+
+  // ugly touch event hotfix
+  enableClickOnTouch('.js-checklist-item:not(.placeholder)');
 }
 }
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
@@ -71,8 +74,10 @@ BlazeComponent.extendComponent({
     event.preventDefault();
     event.preventDefault();
     const textarea = this.find('textarea.js-add-checklist-item');
     const textarea = this.find('textarea.js-add-checklist-item');
     const title = textarea.value.trim();
     const title = textarea.value.trim();
-    const cardId = this.currentData().cardId;
+    let cardId = this.currentData().cardId;
     const card = Cards.findOne(cardId);
     const card = Cards.findOne(cardId);
+    if (card.isLinked())
+      cardId = card.linkedId;
 
 
     if (title) {
     if (title) {
       Checklists.insert({
       Checklists.insert({
@@ -204,7 +209,7 @@ Template.checklistDeleteDialog.onDestroyed(() => {
   $cardDetails.animate( { scrollTop: this.scrollState.position });
   $cardDetails.animate( { scrollTop: this.scrollState.position });
 });
 });
 
 
-Template.itemDetail.helpers({
+Template.checklistItemDetail.helpers({
   canModifyCard() {
   canModifyCard() {
     return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
     return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
   },
   },
@@ -223,4 +228,4 @@ BlazeComponent.extendComponent({
       'click .js-checklist-item .check-box': this.toggleItem,
       'click .js-checklist-item .check-box': this.toggleItem,
     }];
     }];
   },
   },
-}).register('itemDetail');
+}).register('checklistItemDetail');

+ 53 - 1
client/components/cards/labels.styl

@@ -3,7 +3,7 @@
 // XXX Use .board-widget-labels as a flexbox container
 // XXX Use .board-widget-labels as a flexbox container
 .card-label
 .card-label
   border-radius: 4px
   border-radius: 4px
-  color: white
+  color: white  //Default white text, in select cases,  changed to black to improve contrast between label colour and text
   display: inline-block
   display: inline-block
   font-weight: 700
   font-weight: 700
   font-size: 13px
   font-size: 13px
@@ -48,9 +48,11 @@
 
 
 .card-label-yellow
 .card-label-yellow
   background-color: #fad900
   background-color: #fad900
+  color: #000000 //Black text for better visibility
 
 
 .card-label-orange
 .card-label-orange
   background-color: #ff9f19
   background-color: #ff9f19
+  color: #000000 //Black text for better visibility
 
 
 .card-label-red
 .card-label-red
   background-color: #eb4646
   background-color: #eb4646
@@ -63,6 +65,7 @@
 
 
 .card-label-pink
 .card-label-pink
   background-color: #ff78cb
   background-color: #ff78cb
+  color: #000000 //Black text for better visibility
 
 
 .card-label-sky
 .card-label-sky
   background-color: #00c2e0
   background-color: #00c2e0
@@ -72,6 +75,55 @@
 
 
 .card-label-lime
 .card-label-lime
   background-color: #51e898
   background-color: #51e898
+  color: #000000 //Black text for better visibility
+
+.card-label-silver
+  background-color: #c0c0c0
+  color: #000000 //Black text for better visibility
+
+.card-label-peachpuff
+  background-color: #ffdab9
+  color: #000000 //Black text for better visibility
+
+.card-label-crimson
+  background-color: #dc143c
+
+.card-label-plum
+  background-color: #dda0dd
+  color: #000000 //Black text for better visibility
+
+.card-label-darkgreen
+  background-color: #006400
+
+.card-label-slateblue
+  background-color: #6a5acd
+
+.card-label-magenta
+  background-color: #ff00ff
+
+.card-label-gold
+  background-color: #ffd700
+  color: #000000 //Black text for better visibility
+
+.card-label-navy
+  background-color: #000080
+
+.card-label-gray
+  background-color: #808080
+
+.card-label-saddlebrown
+  background-color: #8b4513
+
+.card-label-paleturquoise
+  background-color: #afeeee
+  color: #000000 //Black text for better visibility
+
+.card-label-mistyrose
+  background-color: #ffe4e1
+  color: #000000 //Black text for better visibility
+
+.card-label-indigo
+  background-color: #4b0082
 
 
 .edit-label,
 .edit-label,
 .create-label
 .create-label

+ 58 - 13
client/components/cards/minicard.jade

@@ -1,5 +1,8 @@
 template(name="minicard")
 template(name="minicard")
-  .minicard
+  .minicard(
+    class="{{#if isLinkedCard}}linked-card{{/if}}"
+    class="{{#if isLinkedBoard}}linked-board{{/if}}"
+    class="minicard-{{colorClass}}")
     if cover
     if cover
       .minicard-cover(style="background-image: url('{{cover.url}}');")
       .minicard-cover(style="background-image: url('{{cover.url}}');")
     if labels
     if labels
@@ -7,30 +10,72 @@ template(name="minicard")
         each labels
         each labels
           .minicard-label(class="card-label-{{color}}" title="{{name}}")
           .minicard-label(class="card-label-{{color}}" title="{{name}}")
     .minicard-title
     .minicard-title
+      .handle
+        .fa.fa-arrows
+      if $eq 'prefix-with-full-path' currentBoard.presentParentTask
+        .parent-prefix
+          | {{ parentString ' > ' }}
+      if $eq 'prefix-with-parent' currentBoard.presentParentTask
+        .parent-prefix
+          | {{ parentCardName }}
+      if isLinkedBoard
+        a.js-linked-link
+          span.linked-icon.fa.fa-folder
+      else if isLinkedCard
+        a.js-linked-link
+          span.linked-icon.fa.fa-id-card
+      if getArchived
+        span.linked-icon.linked-archived.fa.fa-archive
       +viewer
       +viewer
-        = title
+        = getTitle
+      if $eq 'subtext-with-full-path' currentBoard.presentParentTask
+        .parent-subtext
+          | {{ parentString ' > ' }}
+      if $eq 'subtext-with-parent' currentBoard.presentParentTask
+        .parent-subtext
+          | {{ parentCardName }}
+
     .dates
     .dates
-      if startAt
+      if getReceived
+        unless getStart
+          unless getDue
+            unless getEnd
+              .date
+                +minicardReceivedDate
+      if getStart
         .date
         .date
           +minicardStartDate
           +minicardStartDate
-      if dueAt
+      if getDue
         .date
         .date
           +minicardDueDate
           +minicardDueDate
-      if spentTime
+      if getSpentTime
         .date
         .date
           +cardSpentTime
           +cardSpentTime
 
 
-    if members
+    .minicard-custom-fields
+      each customFieldsWD
+        if definition.showOnCard
+          .minicard-custom-field
+            if definition.showLabelOnMiniCard
+              .minicard-custom-field-item
+                = definition.name
+            .minicard-custom-field-item
+              +viewer
+                = trueValue
+
+    if getMembers
       .minicard-members.js-minicard-members
       .minicard-members.js-minicard-members
-        each members
+        each getMembers
           +userAvatar(userId=this)
           +userAvatar(userId=this)
+
     .badges
     .badges
-      if comments.count
-        .badge(title="{{_ 'card-comments-title' comments.count }}")
-          span.badge-icon.fa.fa-comment-o.badge-comment
-          span.badge-text= comments.count
-      if description
-        .badge.badge-state-image-only(title=description)
+      unless currentUser.isNoComments
+        if comments.count
+          .badge(title="{{_ 'card-comments-title' comments.count }}")
+            span.badge-icon.fa.fa-comment-o.badge-comment
+            span.badge-text= comments.count
+      if getDescription
+        .badge.badge-state-image-only(title=getDescription)
           span.badge-icon.fa.fa-align-left
           span.badge-icon.fa.fa-align-left
       if attachments.count
       if attachments.count
         .badge
         .badge

+ 11 - 0
client/components/cards/minicard.js

@@ -6,4 +6,15 @@ BlazeComponent.extendComponent({
   template() {
   template() {
     return 'minicard';
     return 'minicard';
   },
   },
+
+  events() {
+    return [{
+      'click .js-linked-link' () {
+        if (this.data().isLinkedCard())
+          Utils.goCardId(this.data().linkedId);
+        else if (this.data().isLinkedBoard())
+          Utils.goBoardId(this.data().linkedId);
+      },
+    }];
+  },
 }).register('minicard');
 }).register('minicard');

+ 123 - 1
client/components/cards/minicard.styl

@@ -9,7 +9,7 @@
 
 
   &.placeholder
   &.placeholder
     background: darken(white, 20%)
     background: darken(white, 20%)
-    border-radius: 2px
+    border-radius: 9px
 
 
   &.ui-sortable-helper
   &.ui-sortable-helper
     cursor: grabbing
     cursor: grabbing
@@ -44,6 +44,16 @@
   transition: transform 0.2s,
   transition: transform 0.2s,
               border-radius 0.2s
               border-radius 0.2s
 
 
+  &.linked-board
+  &.linked-card
+    .linked-icon
+      display: inline-block
+      margin-right: 11px
+      vertical-align: baseline
+      font-size: 0.9em
+    .linked-archived
+      color: #937760
+
   .is-selected &
   .is-selected &
     transform: translateX(11px)
     transform: translateX(11px)
     border-bottom-right-radius: 0
     border-bottom-right-radius: 0
@@ -77,9 +87,31 @@
       height: @width
       height: @width
       border-radius: 2px
       border-radius: 2px
       margin-left: 3px
       margin-left: 3px
+  .minicard-custom-fields
+    display:block;
+  .minicard-custom-field
+    display:flex;
+  .minicard-custom-field-item
+    max-width:50%;
+    flex-grow:1;
+  .handle
+    width: 20px;
+    height: 20px;
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    display:none;
+    @media only screen and (max-width: 1199px) {
+      display:block;
+    }
+    .fa-arrows
+      font-size:20px;
+      color: #ccc;
   .minicard-title
   .minicard-title
     p:last-child
     p:last-child
       margin-bottom: 0
       margin-bottom: 0
+    .viewer
+      display: inline-block
   .dates
   .dates
     display: flex;
     display: flex;
     flex-direction: row;
     flex-direction: row;
@@ -155,6 +187,13 @@
       margin-bottom: 20px
       margin-bottom: 20px
       overflow-y: auto
       overflow-y: auto
 
 
+.parent-prefix
+  color: darken(white, 30%)
+  font-size: 0.9em
+.parent-subtext
+  color: darken(white, 30%)
+  font-size: 0.9em
+
 @media screen and (max-width: 800px)
 @media screen and (max-width: 800px)
   .minicard
   .minicard
     .is-selected &
     .is-selected &
@@ -163,3 +202,86 @@
       border-top-right-radius: 0
       border-top-right-radius: 0
       z-index: 15
       z-index: 15
       box-shadow: 0 1px 2px rgba(0,0,0,.15)
       box-shadow: 0 1px 2px rgba(0,0,0,.15)
+
+minicard-color(background, color...)
+  background-color: background
+  if color
+    color: color //overwrite text for better visibility
+  &:hover:not(.minicard-composer),
+  .is-selected &,
+  .draggable-hover-card &
+    background: darken(background, 3%)
+  .draggable-hover-card &
+    background: darken(background, 7%)
+
+.minicard-green
+  minicard-color(#3cb500, #ffffff) //White text for better visibility
+
+.minicard-yellow
+  minicard-color(#fad900)
+
+.minicard-orange
+  minicard-color(#ff9f19)
+
+.minicard-red
+  minicard-color(#eb4646, #ffffff) //White text for better visibility
+
+.minicard-purple
+  minicard-color(#a632db, #ffffff) //White text for better visibility
+
+.minicard-blue
+  minicard-color(#0079bf, #ffffff) //White text for better visibility
+
+.minicard-pink
+  minicard-color(#ff78cb)
+
+.minicard-sky
+  minicard-color(#00c2e0, #ffffff) //White text for better visibility
+
+.minicard-black
+  minicard-color(#4d4d4d, #ffffff) //White text for better visibility
+
+.minicard-lime
+  minicard-color(#51e898)
+
+.minicard-silver
+  minicard-color(#c0c0c0)
+
+.minicard-peachpuff
+  minicard-color(#ffdab9)
+
+.minicard-crimson
+  minicard-color(#dc143c, #ffffff) //White text for better visibility
+
+.minicard-plum
+  minicard-color(#dda0dd)
+
+.minicard-darkgreen
+  minicard-color(#006400, #ffffff) //White text for better visibility
+
+.minicard-slateblue
+  minicard-color(#6a5acd, #ffffff) //White text for better visibility
+
+.minicard-magenta
+  minicard-color(#ff00ff, #ffffff) //White text for better visibility
+
+.minicard-gold
+  minicard-color(#ffd700)
+
+.minicard-navy
+  minicard-color(#000080, #ffffff) //White text for better visibility
+
+.minicard-gray
+  minicard-color(#808080, #ffffff) //White text for better visibility
+
+.minicard-saddlebrown
+  minicard-color(#8b4513, #ffffff) //White text for better visibility
+
+.minicard-paleturquoise
+  minicard-color(#afeeee)
+
+.minicard-mistyrose
+  minicard-color(#ffe4e1)
+
+.minicard-indigo
+  minicard-color(#4b0082, #ffffff) //White text for better visibility

+ 97 - 0
client/components/cards/subtasks.jade

@@ -0,0 +1,97 @@
+template(name="subtasks")
+  h3 {{_ 'subtasks'}}
+  if toggleDeleteDialog.get
+    .board-overlay#card-details-overlay
+    +subtaskDeleteDialog(subtask = subtaskToDelete)
+
+
+  .card-subtasks-items
+    each subtask in currentCard.subtasks
+      +subtaskDetail(subtask = subtask)
+
+  if canModifyCard
+    +inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
+      +addSubtaskItemForm
+    else
+      a.js-open-inlined-form
+        i.fa.fa-plus
+        | {{_ 'add-subtask'}}...
+
+template(name="subtaskDetail")
+  .js-subtasks.subtask
+    +inlinedForm(classNames="js-edit-subtask-title" subtask = subtask)
+      +editSubtaskItemForm(subtask = subtask)
+    else
+      .subtask-title
+        span
+        a.js-view-subtask(title="{{ subtask.title }}") {{_ "view-it"}}
+        if canModifyCard
+          a.js-delete-subtask.toggle-delete-subtask-dialog {{_ "delete"}}...
+
+        if canModifyCard
+          h2.title.js-open-inlined-form.is-editable
+            +viewer
+              = subtask.title
+        else
+          h2.title
+            +viewer
+                = subtask.title
+
+template(name="subtaskDeleteDialog")
+  .js-confirm-subtask-delete
+    p
+      i(class="fa fa-exclamation-triangle" aria-hidden="true")
+    p
+      | {{_ 'confirm-subtask-delete-dialog'}}
+      span {{subtask.title}}
+      | ?
+    .js-subtask-delete-buttons
+      button.confirm-subtask-delete(type="button") {{_ 'delete'}}
+      button.toggle-delete-subtask-dialog(type="button") {{_ 'cancel'}}
+
+template(name="addSubtaskItemForm")
+  textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
+  .edit-controls.clearfix
+    button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
+    a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="editSubtaskItemForm")
+  textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
+    if $eq type 'item'
+      = item.title
+    else
+      = subtask.title
+  .edit-controls.clearfix
+    button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
+    a.fa.fa-times-thin.js-close-inlined-form
+    span(title=createdAt) {{ moment createdAt }}
+    if canModifyCard
+      a.js-delete-subtask-item {{_ "delete"}}...
+
+template(name="subtasksItems")
+  .subtasks-items.js-subtasks-items
+    each item in subtasks.items
+      +inlinedForm(classNames="js-edit-subtask-item" item = item subtasks = subtasks)
+        +editSubtaskItemForm(type = 'item' item = item subtasks = subtasks)
+      else
+        +subtaskItemDetail(item = item subtasks = subtasks)
+    if canModifyCard
+      +inlinedForm(autoclose=false classNames="js-add-subtask-item" subtasks = subtasks dir="auto")
+        +addSubtaskItemForm
+      else
+        a.add-subtask-item.js-open-inlined-form
+          i.fa.fa-plus
+          | {{_ 'add-subtask-item'}}...
+
+template(name='subtaskItemDetail')
+  .js-subtasks-item.subtasks-item
+    if canModifyCard
+      .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+        +viewer
+          = item.title
+    else
+      .materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+        +viewer
+          = item.title

+ 146 - 0
client/components/cards/subtasks.js

@@ -0,0 +1,146 @@
+BlazeComponent.extendComponent({
+  canModifyCard() {
+    return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+  },
+}).register('subtaskDetail');
+
+BlazeComponent.extendComponent({
+
+  addSubtask(event) {
+    event.preventDefault();
+    const textarea = this.find('textarea.js-add-subtask-item');
+    const title = textarea.value.trim();
+    const cardId = this.currentData().cardId;
+    const card = Cards.findOne(cardId);
+    const sortIndex = -1;
+    const crtBoard = Boards.findOne(card.boardId);
+    const targetBoard = crtBoard.getDefaultSubtasksBoard();
+    const listId = targetBoard.getDefaultSubtasksListId();
+    const swimlaneId = targetBoard.getDefaultSwimline()._id;
+
+    if (title) {
+      const _id = Cards.insert({
+        title,
+        parentId: cardId,
+        members: [],
+        labelIds: [],
+        customFields: [],
+        listId,
+        boardId: targetBoard._id,
+        sort: sortIndex,
+        swimlaneId,
+        type: 'cardType-card',
+      });
+
+      // 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);
+
+
+      setTimeout(() => {
+        this.$('.add-subtask-item').last().click();
+      }, 100);
+    }
+    textarea.value = '';
+    textarea.focus();
+  },
+
+  canModifyCard() {
+    return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+  },
+
+  deleteSubtask() {
+    const subtask = this.currentData().subtask;
+    if (subtask && subtask._id) {
+      subtask.archive();
+      this.toggleDeleteDialog.set(false);
+    }
+  },
+
+  editSubtask(event) {
+    event.preventDefault();
+    const textarea = this.find('textarea.js-edit-subtask-item');
+    const title = textarea.value.trim();
+    const subtask = this.currentData().subtask;
+    subtask.setTitle(title);
+  },
+
+  onCreated() {
+    this.toggleDeleteDialog = new ReactiveVar(false);
+    this.subtaskToDelete = null; //Store data context to pass to subtaskDeleteDialog template
+  },
+
+  pressKey(event) {
+    //If user press enter key inside a form, submit it
+    //Unless the user is also holding down the 'shift' key
+    if (event.keyCode === 13 && !event.shiftKey) {
+      event.preventDefault();
+      const $form = $(event.currentTarget).closest('form');
+      $form.find('button[type=submit]').click();
+    }
+  },
+
+  events() {
+    const events = {
+      'click .toggle-delete-subtask-dialog'(event) {
+        if($(event.target).hasClass('js-delete-subtask')){
+          this.subtaskToDelete = this.currentData().subtask; //Store data context
+        }
+        this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
+      },
+      'click .js-view-subtask'(event) {
+        if($(event.target).hasClass('js-view-subtask')){
+          const subtask = this.currentData().subtask;
+          const board = subtask.board();
+          FlowRouter.go('card', {
+            boardId: board._id,
+            slug: board.slug,
+            cardId: subtask._id,
+          });
+        }
+      },
+    };
+
+    return [{
+      ...events,
+      'submit .js-add-subtask': this.addSubtask,
+      'submit .js-edit-subtask-title': this.editSubtask,
+      'click .confirm-subtask-delete': this.deleteSubtask,
+      keydown: this.pressKey,
+    }];
+  },
+}).register('subtasks');
+
+Template.subtaskDeleteDialog.onCreated(() => {
+  const $cardDetails = this.$('.card-details');
+  this.scrollState = { position: $cardDetails.scrollTop(), //save current scroll position
+    top: false, //required for smooth scroll animation
+  };
+  //Callback's purpose is to only prevent scrolling after animation is complete
+  $cardDetails.animate({ scrollTop: 0 }, 500, () => { this.scrollState.top = true; });
+
+  //Prevent scrolling while dialog is open
+  $cardDetails.on('scroll', () => {
+    if(this.scrollState.top) { //If it's already in position, keep it there. Otherwise let animation scroll
+      $cardDetails.scrollTop(0);
+    }
+  });
+});
+
+Template.subtaskDeleteDialog.onDestroyed(() => {
+  const $cardDetails = this.$('.card-details');
+  $cardDetails.off('scroll'); //Reactivate scrolling
+  $cardDetails.animate( { scrollTop: this.scrollState.position });
+});
+
+Template.subtaskItemDetail.helpers({
+  canModifyCard() {
+    return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
+  },
+});
+
+BlazeComponent.extendComponent({
+  // ...
+}).register('subtaskItemDetail');

+ 142 - 0
client/components/cards/subtasks.styl

@@ -0,0 +1,142 @@
+.js-add-subtask
+  color: #8c8c8c
+
+textarea.js-add-subtask-item, textarea.js-edit-subtask-item
+  overflow: hidden
+  word-wrap: break-word
+  resize: none
+  height: 34px
+
+.delete-text
+  color: #8c8c8c
+  text-decoration: underline
+  word-wrap: break-word
+  float: right
+  padding-top: 6px
+  &:hover
+    color: inherit
+
+.subtask-title
+  .checkbox
+    float: left
+    width: 30px
+    height 30px
+    font-size: 18px
+    line-height: 30px
+
+  .title
+    font-size: 18px
+    line-height: 25px
+
+  .subtasks-stat
+    margin: 0 0.5em
+    float: right
+    padding-top: 6px
+    &.is-finished
+      color: #3cb500
+
+  .js-delete-subtask
+    @extends .delete-text
+    margin: 0 0.5em
+
+  .js-view-subtask
+    @extends .delete-text
+
+.js-confirm-subtask-delete
+  background-color: darken(white, 3%)
+  position: absolute
+  float: left;
+  width: 60%
+  margin-top: 0
+  margin-left: 13%
+  padding-bottom: 2%
+  padding-left: 3%
+  padding-right: 3%
+  z-index: 17
+  border-radius: 3px
+
+  p
+    position: relative
+    margin-top: 3%
+    width: 100%
+    text-align: center
+    span
+      font-weight: bold
+
+    i
+      font-size: 2em
+
+  .js-subtask-delete-buttons
+    position: relative
+    padding: left 2% right 2%
+    .confirm-subtask-delete
+      margin-left: 12%
+      float: left
+    .toggle-delete-subtask-dialog
+      margin-right: 12%
+      float: right
+
+#card-details-overlay
+  top: 0
+  bottom: -600px
+  right: 0
+
+.subtasks
+  background: darken(white, 3%)
+
+  &.placeholder
+    background: darken(white, 20%)
+    border-radius: 2px
+
+  &.ui-sortable-helper
+    box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
+                0 0 1px rgba(0, 0, 0, .5)
+    transform: rotate(4deg)
+    cursor: grabbing
+
+
+.subtasks-item
+  margin: 0 0 0 0.1em
+  line-height: 18px
+  font-size: 1.1em
+  margin-top: 3px
+  display: flex
+  background: darken(white, 3%)
+
+  &.placeholder
+    background: darken(white, 20%)
+    border-radius: 2px
+
+  &.ui-sortable-helper
+    box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
+                0 0 1px rgba(0, 0, 0, .5)
+    transform: rotate(4deg)
+    cursor: grabbing
+
+  &:hover
+    background-color: darken(white, 8%)
+
+  .check-box
+    margin: 0.1em 0 0 0;
+    &.is-checked
+      border-bottom: 2px solid #3cb500
+      border-right: 2px solid #3cb500
+
+  .item-title
+    flex: 1
+    padding-left: 10px;
+    &.is-checked
+      color: #8c8c8c
+      font-style: italic
+    & .viewer
+      p
+        margin-bottom: 2px
+
+.js-delete-subtask-item
+  margin: 0 0 0.5em 1.33em
+  @extends .delete-text
+  padding: 12px 0 0 0
+
+.add-subtask-item
+  margin: 0.2em 0 0.5em 1.33em
+  display: inline-block

+ 15 - 0
client/components/forms/datepicker.jade

@@ -0,0 +1,15 @@
+template(name="datepicker")
+    .datepicker-container
+        form.edit-date
+            .fields
+                .left
+                    label(for="date") {{_ 'date'}}
+                    input.js-date-field#date(type="text" name="date" value=showDate placeholder=dateFormat autofocus)
+                .right
+                    label(for="time") {{_ 'time'}}
+                    input.js-time-field#time(type="text" name="time" value=showTime placeholder=timeFormat)
+            .js-datepicker
+            if error.get
+                .warning {{_ error.get}}
+            button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
+            button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}

+ 17 - 0
client/components/forms/datepicker.styl

@@ -0,0 +1,17 @@
+.datepicker-container
+  .fields
+    .left
+      width: 56%
+    .right
+      width: 38%
+  .datepicker
+    width: 100%
+    table
+      width: 100%
+      border: none
+      border-spacing: 0
+      border-collapse: collapse
+      thead
+        background: none
+      td, th
+        box-sizing: border-box

+ 7 - 1
client/components/forms/forms.styl

@@ -1,10 +1,10 @@
 @import 'nib'
 @import 'nib'
 
 
+select,
 textarea,
 textarea,
 input:not([type=file]),
 input:not([type=file]),
 button
 button
   box-sizing: border-box
   box-sizing: border-box
-  -webkit-appearance: none
   background-color: #ebebeb
   background-color: #ebebeb
   border: 1px solid #ccc
   border: 1px solid #ccc
   border-radius: 3px
   border-radius: 3px
@@ -85,6 +85,9 @@ select
   width: 256px
   width: 256px
   margin-bottom: 8px
   margin-bottom: 8px
 
 
+  &.inline
+  	width: 100%
+
 option[disabled]
 option[disabled]
   color: #8c8c8c
   color: #8c8c8c
 
 
@@ -222,9 +225,12 @@ textarea
 
 
 .edit-controls,
 .edit-controls,
 .add-controls
 .add-controls
+  display: flex
+  align-items: baseline
   margin-top: 0
   margin-top: 0
 
 
   button[type=submit]
   button[type=submit]
+  input[type=button]
     float: left
     float: left
     height: 32px
     height: 32px
     margin-top: -2px
     margin-top: -2px

+ 2 - 2
client/components/import/import.jade

@@ -12,11 +12,11 @@ template(name="import")
 
 
 template(name="importTextarea")
 template(name="importTextarea")
   form
   form
-    p: label(for='import-textarea') {{_ instruction}}
+    p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
     textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
     textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
       | {{jsonText}}
       | {{jsonText}}
     if isSandstorm
     if isSandstorm
-      h1.warning DANGER !!! THIS DESTROYS YOUR IMPORTED DATA, CAUSES BOARD NOT FOUND ERROR WHEN YOU OPEN THIS GRAIN AGAIN https://github.com/wekan/wekan/issues/1430
+      h1.warning {{_ 'import-sandstorm-backup-warning'}}
       p.warning {{_ 'import-sandstorm-warning'}}
       p.warning {{_ 'import-sandstorm-warning'}}
     input.primary.wide(type="submit" value="{{_ 'import'}}")
     input.primary.wide(type="submit" value="{{_ 'import'}}")
 
 

+ 21 - 4
client/components/lists/list.js

@@ -1,4 +1,4 @@
-const { calculateIndex } = Utils;
+const { calculateIndex, enableClickOnTouch } = Utils;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   // Proxy
   // Proxy
@@ -26,6 +26,13 @@ BlazeComponent.extendComponent({
 
 
     const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
     const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
     const $cards = this.$('.js-minicards');
     const $cards = this.$('.js-minicards');
+
+    if(window.matchMedia('(max-width: 1199px)').matches) {
+      $( '.js-minicards' ).sortable({
+        handle: '.handle',
+      });
+    }
+
     $cards.sortable({
     $cards.sortable({
       connectWith: '.js-minicards:not(.js-list-full)',
       connectWith: '.js-minicards:not(.js-list-full)',
       tolerance: 'pointer',
       tolerance: 'pointer',
@@ -47,6 +54,7 @@ BlazeComponent.extendComponent({
       items: itemsSelector,
       items: itemsSelector,
       placeholder: 'minicard-wrapper placeholder',
       placeholder: 'minicard-wrapper placeholder',
       start(evt, ui) {
       start(evt, ui) {
+        ui.helper.css('z-index', 1000);
         ui.placeholder.height(ui.helper.height());
         ui.placeholder.height(ui.helper.height());
         EscapeActions.executeUpTo('popup-close');
         EscapeActions.executeUpTo('popup-close');
         boardComponent.setIsDragging(true);
         boardComponent.setIsDragging(true);
@@ -59,7 +67,13 @@ BlazeComponent.extendComponent({
         const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
         const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
         const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
         const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
         const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
         const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
-        const swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
+        const currentBoard = Boards.findOne(Session.get('currentBoard'));
+        let swimlaneId = '';
+        const boardView = (Meteor.user().profile || {}).boardView;
+        if (boardView === 'board-view-swimlanes' || currentBoard.isTemplatesBoard())
+          swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
+        else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal') || !boardView)
+          swimlaneId = currentBoard.getDefaultSwimline()._id;
 
 
         // Normally the jquery-ui sortable library moves the dragged DOM element
         // Normally the jquery-ui sortable library moves the dragged DOM element
         // to its new position, which disrupts Blaze reactive updates mechanism
         // to its new position, which disrupts Blaze reactive updates mechanism
@@ -72,17 +86,20 @@ BlazeComponent.extendComponent({
 
 
         if (MultiSelection.isActive()) {
         if (MultiSelection.isActive()) {
           Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
           Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
-            card.move(swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
+            card.move(currentBoard._id, swimlaneId, listId, sortIndex.base + i * sortIndex.increment);
           });
           });
         } else {
         } else {
           const cardDomElement = ui.item.get(0);
           const cardDomElement = ui.item.get(0);
           const card = Blaze.getData(cardDomElement);
           const card = Blaze.getData(cardDomElement);
-          card.move(swimlaneId, listId, sortIndex.base);
+          card.move(currentBoard._id, swimlaneId, listId, sortIndex.base);
         }
         }
         boardComponent.setIsDragging(false);
         boardComponent.setIsDragging(false);
       },
       },
     });
     });
 
 
+    // ugly touch event hotfix
+    enableClickOnTouch(itemsSelector);
+
     // Disable drag-dropping if the current user is not a board member or is comment only
     // Disable drag-dropping if the current user is not a board member or is comment only
     this.autorun(() => {
     this.autorun(() => {
       $cards.sortable('option', 'disabled', !userIsMember());
       $cards.sortable('option', 'disabled', !userIsMember());

+ 129 - 8
client/components/lists/list.styl

@@ -10,7 +10,6 @@
   // transparent, because that won't work during a list drag.
   // transparent, because that won't work during a list drag.
   background: darken(white, 13%)
   background: darken(white, 13%)
   border-left: 1px solid darken(white, 20%)
   border-left: 1px solid darken(white, 20%)
-  border-bottom: 1px solid #CCC
   padding: 0
   padding: 0
   float: left
   float: left
 
 
@@ -44,11 +43,23 @@
       background: white
       background: white
       margin: -3px 0 8px
       margin: -3px 0 8px
 
 
+.list-header-card-count
+  height: 35px
+
+.list-header-add
+  flex: 0 0 auto
+  padding: 20px 12px 4px
+  position: relative
+  min-height: 20px
+
 .list-header
 .list-header
   flex: 0 0 auto
   flex: 0 0 auto
-  margin: 20px 12px 4px
+  padding: 20px 12px 4px
   position: relative
   position: relative
   min-height: 20px
   min-height: 20px
+  background-color: #e4e4e4;
+  border-bottom: 6px solid #e4e4e4;
+
 
 
   &.ui-sortable-handle
   &.ui-sortable-handle
     cursor: grab
     cursor: grab
@@ -68,16 +79,18 @@
     text-overflow: ellipsis
     text-overflow: ellipsis
     word-wrap: break-word
     word-wrap: break-word
 
 
+
   .list-header-watch-icon
   .list-header-watch-icon
     padding-left: 10px
     padding-left: 10px
     color: #a6a6a6
     color: #a6a6a6
 
 
+
   .list-header-menu
   .list-header-menu
     position: absolute
     position: absolute
-    padding: 7px
+    padding: 27px 19px
     margin-top: 1px
     margin-top: 1px
-    top: -@padding
-    right: -@padding
+    top: -7px
+    right: -7px
 
 
   .list-header-plus-icon
   .list-header-plus-icon
     color: #a6a6a6
     color: #a6a6a6
@@ -143,9 +156,12 @@
     float: left
     float: left
 
 
 @media screen and (max-width: 800px)
 @media screen and (max-width: 800px)
+  .list-header-menu
+    margin-right: 30px
+
   .mini-list
   .mini-list
     flex: 0 0 60px
     flex: 0 0 60px
-    height: 60px
+    height: auto
     width: 100%
     width: 100%
     border-left: 0px
     border-left: 0px
     border-bottom: 1px solid darken(white, 20%)
     border-bottom: 1px solid darken(white, 20%)
@@ -154,6 +170,8 @@
     display: block
     display: block
     width: 100%
     width: 100%
     border-left: 0px
     border-left: 0px
+    &:first-child
+      margin-left: 0px
 
 
     &.ui-sortable-helper
     &.ui-sortable-helper
       flex: 0 0 60px
       flex: 0 0 60px
@@ -172,8 +190,16 @@
       border-left: 0px
       border-left: 0px
       border-bottom: 1px solid darken(white, 20%)
       border-bottom: 1px solid darken(white, 20%)
 
 
-  .list-header
+  .list-body
+    padding: 15px 19px;
 
 
+  .list-header
+    padding: 0 12px 0px
+    border-bottom: 0px solid #e4e4e4
+    height: 60px
+    margin-top: 10px
+    display: flex
+    align-items: center
     .list-header-left-icon
     .list-header-left-icon
       display: inline
       display: inline
       padding: 7px
       padding: 7px
@@ -185,5 +211,100 @@
     .list-header-menu-icon
     .list-header-menu-icon
       position: absolute
       position: absolute
       padding: 7px
       padding: 7px
-      top: -@padding
+      top: 50%
+      transform: translateY(-50%)
       right: 17px
       right: 17px
+      font-size: 20px
+
+.link-board-wrapper
+  display: flex
+  align-items: baseline
+
+  .js-link-board
+    margin-left: 15px
+
+.search-card-results
+  max-height: 250px
+  overflow: hidden
+
+.sk-spinner-list
+  margin-top: unset !important
+
+list-header-color(background, color...)
+  border-bottom: 6px solid background
+
+.list-header-white
+  list-header-color(#ffffff, #4d4d4d) //Black text for better visibility
+  border: 1px solid #eee
+
+.list-header-green
+  list-header-color(#3cb500, #ffffff) //White text for better visibility
+
+.list-header-yellow
+  list-header-color(#fad900, #4d4d4d) //Black text for better visibility
+
+.list-header-orange
+  list-header-color(#ff9f19, #4d4d4d) //Black text for better visibility
+
+.list-header-red
+  list-header-color(#eb4646, #ffffff) //White text for better visibility
+
+.list-header-purple
+  list-header-color(#a632db, #ffffff) //White text for better visibility
+
+.list-header-blue
+  list-header-color(#0079bf, #ffffff) //White text for better visibility
+
+.list-header-pink
+  list-header-color(#ff78cb, #4d4d4d) //Black text for better visibility
+
+.list-header-sky
+  list-header-color(#00c2e0, #ffffff) //White text for better visibility
+
+.list-header-black
+  list-header-color(#4d4d4d, #ffffff) //White text for better visibility
+
+.list-header-lime
+  list-header-color(#51e898, #4d4d4d) //Black text for better visibility
+
+.list-header-silver
+  list-header-color(unset, #4d4d4d) //Black text for better visibility
+
+.list-header-peachpuff
+  list-header-color(#ffdab9, #4d4d4d) //Black text for better visibility
+
+.list-header-crimson
+  list-header-color(#dc143c, #ffffff) //White text for better visibility
+
+.list-header-plum
+  list-header-color(#dda0dd, #4d4d4d) //Black text for better visibility
+
+.list-header-darkgreen
+  list-header-color(#006400, #ffffff) //White text for better visibility
+
+.list-header-slateblue
+  list-header-color(#6a5acd, #ffffff) //White text for better visibility
+
+.list-header-magenta
+  list-header-color(#ff00ff, #ffffff) //White text for better visibility
+
+.list-header-gold
+  list-header-color(#ffd700, #4d4d4d) //Black text for better visibility
+
+.list-header-navy
+  list-header-color(#000080, #ffffff) //White text for better visibility
+
+.list-header-gray
+  list-header-color(#808080, #ffffff) //White text for better visibility
+
+.list-header-saddlebrown
+  list-header-color(#8b4513, #ffffff) //White text for better visibility
+
+.list-header-paleturquoise
+  list-header-color(#afeeee, #4d4d4d) //Black text for better visibility
+
+.list-header-mistyrose
+  list-header-color(#ffe4e1, #4d4d4d) //Black text for better visibility
+
+.list-header-indigo
+  list-header-color(#4b0082, #ffffff) //White text for better visibility

+ 91 - 2
client/components/lists/listBody.jade

@@ -4,7 +4,7 @@ template(name="listBody")
       if cards.count
       if cards.count
         +inlinedForm(autoclose=false position="top")
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
           +addCardForm(listId=_id position="top")
-      each (cards (idOrNull ../../_id))
+      each (cardsWithLimit (idOrNull ../../_id))
         a.minicard-wrapper.js-minicard(href=absoluteUrl
         a.minicard-wrapper.js-minicard(href=absoluteUrl
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
@@ -12,6 +12,9 @@ template(name="listBody")
             .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
             .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
               class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
               class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           +minicard(this)
           +minicard(this)
+      if (showSpinner (idOrNull ../../_id))
+        +spinnerList
+
       if canSeeAddCard
       if canSeeAddCard
         +inlinedForm(autoclose=false position="bottom")
         +inlinedForm(autoclose=false position="bottom")
           +addCardForm(listId=_id position="bottom")
           +addCardForm(listId=_id position="bottom")
@@ -20,6 +23,16 @@ template(name="listBody")
             i.fa.fa-plus
             i.fa.fa-plus
             | {{_ 'add-card'}}
             | {{_ 'add-card'}}
 
 
+template(name="spinnerList")
+  .sk-spinner.sk-spinner-wave.sk-spinner-list(
+    class=currentBoard.colorClass
+    id="showMoreResults")
+    .sk-rect1
+    .sk-rect2
+    .sk-rect3
+    .sk-rect4
+    .sk-rect5
+
 template(name="addCardForm")
 template(name="addCardForm")
   .minicard.minicard-composer.js-composer
   .minicard.minicard-composer.js-composer
     if getLabels
     if getLabels
@@ -34,8 +47,84 @@ template(name="addCardForm")
 
 
   .add-controls.clearfix
   .add-controls.clearfix
     button.primary.confirm(type="submit") {{_ 'add'}}
     button.primary.confirm(type="submit") {{_ 'add'}}
-    a.fa.fa-times-thin.js-close-inlined-form
+    unless currentBoard.isTemplatesBoard
+      unless currentBoard.isTemplateBoard
+        span.quiet
+          | {{_ 'or'}}
+          a.js-link {{_ 'link'}}
+        span.quiet
+          | &nbsp;
+          | /
+          a.js-search {{_ 'search'}}
+        span.quiet
+          | &nbsp;
+          | /
+          a.js-card-template {{_ 'template'}}
 
 
 template(name="autocompleteLabelLine")
 template(name="autocompleteLabelLine")
   .minicard-label(class="card-label-{{colorName}}" title=labelName)
   .minicard-label(class="card-label-{{colorName}}" title=labelName)
   span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
   span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
+
+template(name="linkCardPopup")
+  label {{_ 'boards'}}:
+  .link-board-wrapper
+    select.js-select-boards
+      option(value="")
+      each boards
+        option(value="{{_id}}") {{title}}
+    input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}")
+
+  label {{_ 'swimlanes'}}:
+  select.js-select-swimlanes
+    each swimlanes
+      option(value="{{_id}}") {{title}}
+
+  label {{_ 'lists'}}:
+  select.js-select-lists
+    each lists
+      option(value="{{_id}}") {{title}}
+
+  label {{_ 'cards'}}:
+  select.js-select-cards
+    each cards
+      option(value="{{getId}}") {{getTitle}}
+
+  .edit-controls.clearfix
+    input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
+
+template(name="searchElementPopup")
+  form
+    label
+      | {{_ 'title'}}
+      input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required)
+  unless isTemplateSearch
+    label {{_ 'boards'}}:
+    .link-board-wrapper
+      select.js-select-boards
+        option(value="")
+        each boards
+          option(value="{{_id}}") {{title}}
+  form.js-search-term-form
+    input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
+  .list-body.js-perfect-scrollbar.search-card-results
+    .minicards.clearfix.js-minicards
+      if isBoardTemplateSearch
+        each results
+          a.minicard-wrapper.js-minicard
+            +miniboard(this)
+      if isListTemplateSearch
+        each results
+          a.minicard-wrapper.js-minicard
+            +minilist(this)
+      if isSwimlaneTemplateSearch
+        each results
+          a.minicard-wrapper.js-minicard
+            +miniswimlane(this)
+      if isCardTemplateSearch
+        each results
+          a.minicard-wrapper.js-minicard
+            +minicard(this)
+      unless isTemplateSearch
+        each results
+          a.minicard-wrapper.js-minicard
+            +minicard(this)

+ 414 - 16
client/components/lists/listBody.js

@@ -1,4 +1,12 @@
+const subManager = new SubsManager();
+const InfiniteScrollIter = 10;
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
+  onCreated() {
+    // for infinite scrolling
+    this.cardlimit = new ReactiveVar(InfiniteScrollIter);
+  },
+
   mixins() {
   mixins() {
     return [Mixins.PerfectScrollbar];
     return [Mixins.PerfectScrollbar];
   },
   },
@@ -35,25 +43,59 @@ BlazeComponent.extendComponent({
 
 
     const members = formComponent.members.get();
     const members = formComponent.members.get();
     const labelIds = formComponent.labels.get();
     const labelIds = formComponent.labels.get();
+    const customFields = formComponent.customFields.get();
 
 
-    const boardId = this.data().board()._id;
+    const board = this.data().board();
+    let linkedId = '';
     let swimlaneId = '';
     let swimlaneId = '';
-    const boardView = Meteor.user().profile.boardView;
-    if (boardView === 'board-view-swimlanes')
-      swimlaneId = this.parentComponent().parentComponent().data()._id;
-    else if (boardView === 'board-view-lists')
-      swimlaneId = Swimlanes.findOne({boardId})._id;
-
+    const boardView = (Meteor.user().profile || {}).boardView;
+    let cardType = 'cardType-card';
     if (title) {
     if (title) {
+      if (board.isTemplatesBoard()) {
+        swimlaneId = this.parentComponent().parentComponent().data()._id; // Always swimlanes view
+        const swimlane = Swimlanes.findOne(swimlaneId);
+        // If this is the card templates swimlane, insert a card template
+        if (swimlane.isCardTemplatesSwimlane())
+          cardType = 'template-card';
+        // If this is the board templates swimlane, insert a board template and a linked card
+        else if (swimlane.isBoardTemplatesSwimlane()) {
+          linkedId = Boards.insert({
+            title,
+            permission: 'private',
+            type: 'template-board',
+          });
+          Swimlanes.insert({
+            title: TAPi18n.__('default'),
+            boardId: linkedId,
+          });
+          cardType = 'cardType-linkedBoard';
+        }
+      } else if (boardView === 'board-view-swimlanes')
+        swimlaneId = this.parentComponent().parentComponent().data()._id;
+      else if ((boardView === 'board-view-lists') || (boardView === 'board-view-cal') || !boardView)
+        swimlaneId = board.getDefaultSwimline()._id;
+
       const _id = Cards.insert({
       const _id = Cards.insert({
         title,
         title,
         members,
         members,
         labelIds,
         labelIds,
+        customFields,
         listId: this.data()._id,
         listId: this.data()._id,
-        boardId: this.data().board()._id,
+        boardId: board._id,
         sort: sortIndex,
         sort: sortIndex,
         swimlaneId,
         swimlaneId,
+        type: cardType,
+        linkedId,
       });
       });
+
+      // if the displayed card count is less than the total cards in the list,
+      // we need to increment the displayed card count to prevent the spinner
+      // to appear
+      const cardCount = this.data().cards(this.idOrNull(swimlaneId)).count();
+      if (this.cardlimit.get() < cardCount) {
+        this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
+      }
+
       // In case the filter is active we need to add the newly inserted card in
       // 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
       // the list of exceptions -- cards that are not filtered. Otherwise the
       // card will disappear instantly.
       // card will disappear instantly.
@@ -85,9 +127,9 @@ BlazeComponent.extendComponent({
       const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
       const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
       MultiSelection[methodName](this.currentData()._id);
       MultiSelection[methodName](this.currentData()._id);
 
 
-    // If the card is already selected, we want to de-select it.
-    // XXX We should probably modify the minicard href attribute instead of
-    // overwriting the event in case the card is already selected.
+      // If the card is already selected, we want to de-select it.
+      // XXX We should probably modify the minicard href attribute instead of
+      // overwriting the event in case the card is already selected.
     } else if (Session.equals('currentCard', this.currentData()._id)) {
     } else if (Session.equals('currentCard', this.currentData()._id)) {
       evt.stopImmediatePropagation();
       evt.stopImmediatePropagation();
       evt.preventDefault();
       evt.preventDefault();
@@ -107,11 +149,31 @@ BlazeComponent.extendComponent({
 
 
   idOrNull(swimlaneId) {
   idOrNull(swimlaneId) {
     const currentUser = Meteor.user();
     const currentUser = Meteor.user();
-    if (currentUser.profile.boardView === 'board-view-swimlanes')
+    if ((currentUser.profile || {}).boardView === 'board-view-swimlanes' ||
+        this.data().board().isTemplatesBoard())
       return swimlaneId;
       return swimlaneId;
     return undefined;
     return undefined;
   },
   },
 
 
+  cardsWithLimit(swimlaneId) {
+    const limit = this.cardlimit.get();
+    const selector = {
+      listId: this.currentData()._id,
+      archived: false,
+    };
+    if (swimlaneId)
+      selector.swimlaneId = swimlaneId;
+    return Cards.find(Filter.mongoSelector(selector), {
+      sort: ['sort'],
+      limit,
+    });
+  },
+
+  showSpinner(swimlaneId) {
+    const list = Template.currentData();
+    return list.cards(swimlaneId).count() > this.cardlimit.get();
+  },
+
   canSeeAddCard() {
   canSeeAddCard() {
     return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
     return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
   },
   },
@@ -146,11 +208,21 @@ BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     this.labels = new ReactiveVar([]);
     this.labels = new ReactiveVar([]);
     this.members = new ReactiveVar([]);
     this.members = new ReactiveVar([]);
+    this.customFields = new ReactiveVar([]);
+
+    const currentBoardId = Session.get('currentBoard');
+    arr = [];
+    _.forEach(Boards.findOne(currentBoardId).customFields().fetch(), function(field){
+      if(field.automaticallyOnCard)
+        arr.push({_id: field._id, value: null});
+    });
+    this.customFields.set(arr);
   },
   },
 
 
   reset() {
   reset() {
     this.labels.set([]);
     this.labels.set([]);
     this.members.set([]);
     this.members.set([]);
+    this.customFields.set([]);
   },
   },
 
 
   getLabels() {
   getLabels() {
@@ -162,7 +234,7 @@ BlazeComponent.extendComponent({
 
 
   pressKey(evt) {
   pressKey(evt) {
     // Pressing Enter should submit the card
     // Pressing Enter should submit the card
-    if (evt.keyCode === 13) {
+    if (evt.keyCode === 13 && !evt.shiftKey) {
       evt.preventDefault();
       evt.preventDefault();
       const $form = $(evt.currentTarget).closest('form');
       const $form = $(evt.currentTarget).closest('form');
       // XXX For some reason $form.submit() does not work (it's probably a bug
       // XXX For some reason $form.submit() does not work (it's probably a bug
@@ -171,8 +243,8 @@ BlazeComponent.extendComponent({
       // work.
       // work.
       $form.find('button[type=submit]').click();
       $form.find('button[type=submit]').click();
 
 
-    // Pressing Tab should open the form of the next column, and Maj+Tab go
-    // in the reverse order
+      // Pressing Tab should open the form of the next column, and Maj+Tab go
+      // in the reverse order
     } else if (evt.keyCode === 9) {
     } else if (evt.keyCode === 9) {
       evt.preventDefault();
       evt.preventDefault();
       const isReverse = evt.shiftKey;
       const isReverse = evt.shiftKey;
@@ -193,6 +265,9 @@ BlazeComponent.extendComponent({
   events() {
   events() {
     return [{
     return [{
       keydown: this.pressKey,
       keydown: this.pressKey,
+      'click .js-link': Popup.open('linkCard'),
+      'click .js-search': Popup.open('searchElement'),
+      'click .js-card-template': Popup.open('searchElement'),
     }];
     }];
   },
   },
 
 
@@ -230,7 +305,7 @@ BlazeComponent.extendComponent({
           const currentBoard = Boards.findOne(Session.get('currentBoard'));
           const currentBoard = Boards.findOne(Session.get('currentBoard'));
           callback($.map(currentBoard.labels, (label) => {
           callback($.map(currentBoard.labels, (label) => {
             if (label.name.indexOf(term) > -1 ||
             if (label.name.indexOf(term) > -1 ||
-                label.color.indexOf(term) > -1) {
+              label.color.indexOf(term) > -1) {
               return label;
               return label;
             }
             }
             return null;
             return null;
@@ -264,3 +339,326 @@ BlazeComponent.extendComponent({
     });
     });
   },
   },
 }).register('addCardForm');
 }).register('addCardForm');
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.selectedBoardId = new ReactiveVar('');
+    this.selectedSwimlaneId = new ReactiveVar('');
+    this.selectedListId = new ReactiveVar('');
+
+    this.boardId = Session.get('currentBoard');
+    // In order to get current board info
+    subManager.subscribe('board', this.boardId);
+    this.board = Boards.findOne(this.boardId);
+    // List where to insert card
+    const list = $(Popup._getTopStack().openerElement).closest('.js-list');
+    this.listId = Blaze.getData(list[0])._id;
+    // Swimlane where to insert card
+    const swimlane = $(Popup._getTopStack().openerElement).closest('.js-swimlane');
+    this.swimlaneId = '';
+    const boardView = (Meteor.user().profile || {}).boardView;
+    if (boardView === 'board-view-swimlanes')
+      this.swimlaneId = Blaze.getData(swimlane[0])._id;
+    else if (boardView === 'board-view-lists' || !boardView)
+      this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id;
+  },
+
+  boards() {
+    const boards = Boards.find({
+      archived: false,
+      'members.userId': Meteor.userId(),
+      _id: {$ne: Session.get('currentBoard')},
+      type: 'board',
+    }, {
+      sort: ['title'],
+    });
+    return boards;
+  },
+
+  swimlanes() {
+    if (!this.selectedBoardId.get()) {
+      return [];
+    }
+    const swimlanes = Swimlanes.find({boardId: this.selectedBoardId.get()});
+    if (swimlanes.count())
+      this.selectedSwimlaneId.set(swimlanes.fetch()[0]._id);
+    return swimlanes;
+  },
+
+  lists() {
+    if (!this.selectedBoardId.get()) {
+      return [];
+    }
+    const lists = Lists.find({boardId: this.selectedBoardId.get()});
+    if (lists.count())
+      this.selectedListId.set(lists.fetch()[0]._id);
+    return lists;
+  },
+
+  cards() {
+    if (!this.board) {
+      return [];
+    }
+    const ownCardsIds = this.board.cards().map((card) => { return card.linkedId || card._id; });
+    return Cards.find({
+      boardId: this.selectedBoardId.get(),
+      swimlaneId: this.selectedSwimlaneId.get(),
+      listId: this.selectedListId.get(),
+      archived: false,
+      linkedId: {$nin: ownCardsIds},
+      _id: {$nin: ownCardsIds},
+      type: {$nin: ['template-card']},
+    });
+  },
+
+  events() {
+    return [{
+      'change .js-select-boards'(evt) {
+        subManager.subscribe('board', $(evt.currentTarget).val());
+        this.selectedBoardId.set($(evt.currentTarget).val());
+      },
+      'change .js-select-swimlanes'(evt) {
+        this.selectedSwimlaneId.set($(evt.currentTarget).val());
+      },
+      'change .js-select-lists'(evt) {
+        this.selectedListId.set($(evt.currentTarget).val());
+      },
+      'click .js-done' (evt) {
+        // LINK CARD
+        evt.stopPropagation();
+        evt.preventDefault();
+        const linkedId = $('.js-select-cards option:selected').val();
+        if (!linkedId) {
+          Popup.close();
+          return;
+        }
+        const _id = Cards.insert({
+          title: $('.js-select-cards option:selected').text(), //dummy
+          listId: this.listId,
+          swimlaneId: this.swimlaneId,
+          boardId: this.boardId,
+          sort: Lists.findOne(this.listId).cards().count(),
+          type: 'cardType-linkedCard',
+          linkedId,
+        });
+        Filter.addException(_id);
+        Popup.close();
+      },
+      'click .js-link-board' (evt) {
+        //LINK BOARD
+        evt.stopPropagation();
+        evt.preventDefault();
+        const impBoardId = $('.js-select-boards option:selected').val();
+        if (!impBoardId || Cards.findOne({linkedId: impBoardId, archived: false})) {
+          Popup.close();
+          return;
+        }
+        const _id = Cards.insert({
+          title: $('.js-select-boards option:selected').text(), //dummy
+          listId: this.listId,
+          swimlaneId: this.swimlaneId,
+          boardId: this.boardId,
+          sort: Lists.findOne(this.listId).cards().count(),
+          type: 'cardType-linkedBoard',
+          linkedId: impBoardId,
+        });
+        Filter.addException(_id);
+        Popup.close();
+      },
+    }];
+  },
+}).register('linkCardPopup');
+
+BlazeComponent.extendComponent({
+  mixins() {
+    return [Mixins.PerfectScrollbar];
+  },
+
+  onCreated() {
+    this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-card-template');
+    this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-list-template');
+    this.isSwimlaneTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-open-add-swimlane-menu');
+    this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass('js-add-board');
+    this.isTemplateSearch = this.isCardTemplateSearch ||
+      this.isListTemplateSearch ||
+      this.isSwimlaneTemplateSearch ||
+      this.isBoardTemplateSearch;
+    let board = {};
+    if (this.isTemplateSearch) {
+      board = Boards.findOne((Meteor.user().profile || {}).templatesBoardId);
+    } else {
+      // Prefetch first non-current board id
+      board = Boards.findOne({
+        archived: false,
+        'members.userId': Meteor.userId(),
+        _id: {$nin: [Session.get('currentBoard'), (Meteor.user().profile || {}).templatesBoardId]},
+      });
+    }
+    if (!board) {
+      Popup.close();
+      return;
+    }
+    const boardId = board._id;
+    // Subscribe to this board
+    subManager.subscribe('board', boardId);
+    this.selectedBoardId = new ReactiveVar(boardId);
+
+    if (!this.isBoardTemplateSearch) {
+      this.boardId = Session.get('currentBoard');
+      // In order to get current board info
+      subManager.subscribe('board', this.boardId);
+      this.swimlaneId = '';
+      // Swimlane where to insert card
+      const swimlane = $(Popup._getTopStack().openerElement).parents('.js-swimlane');
+      if ((Meteor.user().profile || {}).boardView === 'board-view-swimlanes')
+        this.swimlaneId = Blaze.getData(swimlane[0])._id;
+      else
+        this.swimlaneId = Swimlanes.findOne({boardId: this.boardId})._id;
+      // List where to insert card
+      const list = $(Popup._getTopStack().openerElement).closest('.js-list');
+      this.listId = Blaze.getData(list[0])._id;
+    }
+    this.term = new ReactiveVar('');
+  },
+
+  boards() {
+    const boards = Boards.find({
+      archived: false,
+      'members.userId': Meteor.userId(),
+      _id: {$ne: Session.get('currentBoard')},
+      type: 'board',
+    }, {
+      sort: ['title'],
+    });
+    return boards;
+  },
+
+  results() {
+    if (!this.selectedBoardId) {
+      return [];
+    }
+    const board = Boards.findOne(this.selectedBoardId.get());
+    if (!this.isTemplateSearch || this.isCardTemplateSearch) {
+      return board.searchCards(this.term.get(), false);
+    } else if (this.isListTemplateSearch) {
+      return board.searchLists(this.term.get());
+    } else if (this.isSwimlaneTemplateSearch) {
+      return board.searchSwimlanes(this.term.get());
+    } else if (this.isBoardTemplateSearch) {
+      const boards = board.searchBoards(this.term.get());
+      boards.forEach((board) => {
+        subManager.subscribe('board', board.linkedId);
+      });
+      return boards;
+    } else {
+      return [];
+    }
+  },
+
+  events() {
+    return [{
+      'change .js-select-boards'(evt) {
+        subManager.subscribe('board', $(evt.currentTarget).val());
+        this.selectedBoardId.set($(evt.currentTarget).val());
+      },
+      'submit .js-search-term-form'(evt) {
+        evt.preventDefault();
+        this.term.set(evt.target.searchTerm.value);
+      },
+      'click .js-minicard'(evt) {
+        // 0. Common
+        const title = $('.js-element-title').val().trim();
+        if (!title)
+          return;
+        const element = Blaze.getData(evt.currentTarget);
+        element.title = title;
+        let _id = '';
+        if (!this.isTemplateSearch || this.isCardTemplateSearch) {
+          // Card insertion
+          // 1. Common
+          element.sort = Lists.findOne(this.listId).cards().count();
+          // 1.A From template
+          if (this.isTemplateSearch) {
+            element.type = 'cardType-card';
+            element.linkedId = '';
+            _id = element.copy(this.boardId, this.swimlaneId, this.listId);
+            // 1.B Linked card
+          } else {
+            delete element._id;
+            element.type = 'cardType-linkedCard';
+            element.linkedId = element.linkedId || element._id;
+            _id = Cards.insert(element);
+          }
+          Filter.addException(_id);
+          // List insertion
+        } else if (this.isListTemplateSearch) {
+          element.sort = Swimlanes.findOne(this.swimlaneId).lists().count();
+          element.type = 'list';
+          _id = element.copy(this.boardId, this.swimlaneId);
+        } else if (this.isSwimlaneTemplateSearch) {
+          element.sort = Boards.findOne(this.boardId).swimlanes().count();
+          element.type = 'swimlalne';
+          _id = element.copy(this.boardId);
+        } else if (this.isBoardTemplateSearch) {
+          board = Boards.findOne(element.linkedId);
+          board.sort = Boards.find({archived: false}).count();
+          board.type = 'board';
+          board.title = element.title;
+          delete board.slug;
+          _id = board.copy();
+        }
+        Popup.close();
+      },
+    }];
+  },
+}).register('searchElementPopup');
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.cardlimit = this.parentComponent().cardlimit;
+
+    this.listId = this.parentComponent().data()._id;
+    this.swimlaneId = '';
+
+    const boardView = (Meteor.user().profile || {}).boardView;
+    if (boardView === 'board-view-swimlanes')
+      this.swimlaneId = this.parentComponent().parentComponent().parentComponent().data()._id;
+  },
+
+  onRendered() {
+    this.spinner = this.find('.sk-spinner-list');
+    this.container = this.$(this.spinner).parents('.js-perfect-scrollbar')[0];
+
+    $(this.container).on(`scroll.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList());
+    $(window).on(`resize.spinner_${this.swimlaneId}_${this.listId}`, () => this.updateList());
+
+    this.updateList();
+  },
+
+  onDestroyed() {
+    $(this.container).off(`scroll.spinner_${this.swimlaneId}_${this.listId}`);
+    $(window).off(`resize.spinner_${this.swimlaneId}_${this.listId}`);
+  },
+
+  updateList() {
+    if (this.spinnerInView()) {
+      this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
+      window.requestIdleCallback(() => this.updateList());
+    }
+  },
+
+  spinnerInView() {
+    const parentViewHeight = this.container.clientHeight;
+    const bottomViewPosition = this.container.scrollTop + parentViewHeight;
+
+    const threshold = this.spinner.offsetTop;
+
+    // spinner deleted
+    if (!this.spinner.offsetTop) {
+      return false;
+    }
+
+    return bottomViewPosition > threshold;
+  },
+
+}).register('spinnerList');

+ 18 - 4
client/components/lists/listHeader.jade

@@ -1,5 +1,7 @@
 template(name="listHeader")
 template(name="listHeader")
-  .list-header.js-list-header
+  .list-header.js-list-header(
+    class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}"
+    class="{{#if colorClass}}list-header-{{colorClass}}{{/if}}")
     +inlinedForm
     +inlinedForm
       +editListTitleForm
       +editListTitleForm
     else
     else
@@ -15,9 +17,8 @@ template(name="listHeader")
          |/#{wipLimit.value})
          |/#{wipLimit.value})
 
 
         if showCardsCountForList cards.count
         if showCardsCountForList cards.count
-          = cards.count
-          span
-            |  {{_ 'cards-count'}}
+          |&nbsp;
+          p.quiet.small {{cardsCount}} {{_ 'cards-count'}}
       if isMiniScreen
       if isMiniScreen
         if currentList
         if currentList
           if isWatching
           if isWatching
@@ -50,6 +51,9 @@ template(name="listActionPopup")
     li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
     li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
   unless currentUser.isCommentOnly
   unless currentUser.isCommentOnly
     hr
     hr
+    ul.pop-over-list
+      li: a.js-set-color-list {{_ 'set-color-list'}}
+    hr
     ul.pop-over-list
     ul.pop-over-list
       if cards.count
       if cards.count
         li: a.js-select-cards {{_ 'list-select-cards'}}
         li: a.js-select-cards {{_ 'list-select-cards'}}
@@ -112,3 +116,13 @@ template(name="wipLimitErrorPopup")
     p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
     p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
     p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
     p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
     button.full.js-back-view(type="submit") {{_ 'cancel'}}
     button.full.js-back-view(type="submit") {{_ 'cancel'}}
+
+template(name="setListColorPopup")
+  form.edit-label
+    .palette-colors: each colors
+      // note: we use the swimlane palette to have more than just the border
+      span.card-label.palette-color.js-palette-color(class="swimlane-{{color}}")
+        if(isSelected color)
+          i.fa.fa-check
+    button.primary.confirm.js-submit {{_ 'save'}}
+    button.js-remove-color.negate.wide.right {{_ 'unset-color'}}

+ 47 - 0
client/components/lists/listHeader.js

@@ -1,3 +1,8 @@
+let listsColors;
+Meteor.startup(() => {
+  listsColors = Lists.simpleSchema()._schema.color.allowedValues;
+});
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   canSeeAddCard() {
   canSeeAddCard() {
     const list = Template.currentData();
     const list = Template.currentData();
@@ -22,6 +27,16 @@ BlazeComponent.extendComponent({
     return Meteor.user().getLimitToShowCardsCount();
     return Meteor.user().getLimitToShowCardsCount();
   },
   },
 
 
+  cardsCount() {
+    const list = Template.currentData();
+    let swimlaneId = '';
+    const boardView = (Meteor.user().profile || {}).boardView;
+    if (boardView === 'board-view-swimlanes')
+      swimlaneId = this.parentComponent().parentComponent().data()._id;
+
+    return list.cards(swimlaneId).count();
+  },
+
   reachedWipLimit() {
   reachedWipLimit() {
     const list = Template.currentData();
     const list = Template.currentData();
     return list.getWipLimit('enabled') && list.getWipLimit('value') <= list.cards().count();
     return list.getWipLimit('enabled') && list.getWipLimit('value') <= list.cards().count();
@@ -62,6 +77,7 @@ Template.listActionPopup.helpers({
 
 
 Template.listActionPopup.events({
 Template.listActionPopup.events({
   'click .js-list-subscribe' () {},
   'click .js-list-subscribe' () {},
+  'click .js-set-color-list': Popup.open('setListColor'),
   'click .js-select-cards' () {
   'click .js-select-cards' () {
     const cardIds = this.allCards().map((card) => card._id);
     const cardIds = this.allCards().map((card) => card._id);
     MultiSelection.add(cardIds);
     MultiSelection.add(cardIds);
@@ -144,3 +160,34 @@ Template.listMorePopup.events({
     Utils.goBoardId(this.boardId);
     Utils.goBoardId(this.boardId);
   }),
   }),
 });
 });
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentList = this.currentData();
+    this.currentColor = new ReactiveVar(this.currentList.color);
+  },
+
+  colors() {
+    return listsColors.map((color) => ({ color, name: '' }));
+  },
+
+  isSelected(color) {
+    return this.currentColor.get() === color;
+  },
+
+  events() {
+    return [{
+      'click .js-palette-color'() {
+        this.currentColor.set(this.currentData().color);
+      },
+      'click .js-submit' () {
+        this.currentList.setColor(this.currentColor.get());
+        Popup.close();
+      },
+      'click .js-remove-color'() {
+        this.currentList.setColor(null);
+        Popup.close();
+      },
+    }];
+  },
+}).register('setListColorPopup');

+ 8 - 0
client/components/lists/minilist.jade

@@ -0,0 +1,8 @@
+template(name="minilist")
+  .minicard(
+    class="minicard-{{colorClass}}")
+    .minicard-title
+      .handle
+        .fa.fa-arrows
+      +viewer
+        = title

+ 2 - 1
client/components/main/editor.jade

@@ -1,5 +1,6 @@
 template(name="editor")
 template(name="editor")
   textarea.editor(
   textarea.editor(
+    dir="auto"
     class="{{class}}"
     class="{{class}}"
     id=id
     id=id
     autofocus=autofocus
     autofocus=autofocus
@@ -7,7 +8,7 @@ template(name="editor")
     +Template.contentBlock
     +Template.contentBlock
 
 
 template(name="viewer")
 template(name="viewer")
-  .viewer
+  .viewer(dir="auto")
     +mentions
     +mentions
       +markdown
       +markdown
         {{> UI.contentBlock }}
         {{> UI.contentBlock }}

+ 7 - 2
client/components/main/editor.js

@@ -36,13 +36,18 @@ import sanitizeXss from 'xss';
 const at = HTML.CharRef({html: '&commat;', str: '@'});
 const at = HTML.CharRef({html: '&commat;', str: '@'});
 Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
 Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
   const view = this;
   const view = this;
+  let content = Blaze.toHTML(view.templateContentBlock);
   const currentBoard = Boards.findOne(Session.get('currentBoard'));
   const currentBoard = Boards.findOne(Session.get('currentBoard'));
+  if (!currentBoard)
+    return HTML.Raw(sanitizeXss(content));
   const knowedUsers = currentBoard.members.map((member) => {
   const knowedUsers = currentBoard.members.map((member) => {
-    member.username = Users.findOne(member.userId).username;
+    const u = Users.findOne(member.userId);
+    if(u){
+      member.username = u.username;
+    }
     return member;
     return member;
   });
   });
   const mentionRegex = /\B@([\w.]*)/gi;
   const mentionRegex = /\B@([\w.]*)/gi;
-  let content = Blaze.toHTML(view.templateContentBlock);
 
 
   let currentMention;
   let currentMention;
   while ((currentMention = mentionRegex.exec(content)) !== null) {
   while ((currentMention = mentionRegex.exec(content)) !== null) {

+ 36 - 37
client/components/main/header.jade

@@ -4,39 +4,38 @@ template(name="header")
     list all starred boards with a link to go there. This is inspired by the
     list all starred boards with a link to go there. This is inspired by the
     Reddit "subreddit" bar.
     Reddit "subreddit" bar.
     The first link goes to the boards page.
     The first link goes to the boards page.
-  unless isSandstorm
-    if currentUser
-      #header-quick-access(class=currentBoard.colorClass)
-        if isMiniScreen
-          ul
-            li
-              a(href="{{pathFor 'home'}}")
-                span.fa.fa-home
+  if currentUser
+    #header-quick-access(class=currentBoard.colorClass)
+      if isMiniScreen
+        ul
+          li
+            a(href="{{pathFor 'home'}}")
+              span.fa.fa-home
 
 
-            if currentList
-              each currentBoard.lists
-                li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
-                  a.js-select-list
-                    = title
-          #header-new-board-icon
-        else
-          ul
-            li
-              a(href="{{pathFor 'home'}}")
-                span.fa.fa-home
-                | {{_ 'all-boards'}}
-            each currentUser.starredBoards
-              li.separator -
-              li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
-                a(href="{{pathFor 'board' id=_id slug=slug}}")
+          if currentList
+            each currentBoard.lists
+              li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
+                a.js-select-list
                   = title
                   = title
-            else
-              li.current {{_ 'quick-access-description'}}
+        #header-new-board-icon
+      else
+        ul
+          li
+            a(href="{{pathFor 'home'}}")
+              span.fa.fa-home
+              | {{_ 'all-boards'}}
+          each currentUser.starredBoards
+            li.separator -
+            li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
+              a(href="{{pathFor 'board' id=_id slug=slug}}")
+                = title
+          else
+            li.current {{_ 'quick-access-description'}}
 
 
-        a#header-new-board-icon.js-create-board
-          i.fa.fa-plus(title="Create a new board")
+      a#header-new-board-icon.js-create-board
+        i.fa.fa-plus(title="Create a new board")
 
 
-        +headerUserBar
+      +headerUserBar
 
 
   #header(class=currentBoard.colorClass)
   #header(class=currentBoard.colorClass)
     //-
     //-
@@ -46,17 +45,16 @@ template(name="header")
     #header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
     #header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
       +Template.dynamic(template=headerBar)
       +Template.dynamic(template=headerBar)
 
 
-      unless hideLogo
+      //unless hideLogo
+
         //-
         //-
           On sandstorm, the logo shouldn't be clickable, because we only have one
           On sandstorm, the logo shouldn't be clickable, because we only have one
           page/document on it, and we don't want to see the home page containing
           page/document on it, and we don't want to see the home page containing
           the list of all boards.
           the list of all boards.
-        if isSandstorm
-          .wekan-logo
-            img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
-        else
-          a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
-            img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
+
+      //  unless currentSetting.hideLogo
+      //    a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
+      //    img(src="{{pathFor '/logo-header.png'}}" alt="")
 
 
   if appIsOffline
   if appIsOffline
     +offlineWarning
     +offlineWarning
@@ -66,7 +64,8 @@ template(name="header")
       .announcement
       .announcement
         p
         p
           i.fa.fa-bullhorn
           i.fa.fa-bullhorn
-          | #{announcement}
+          +viewer
+            | #{announcement}
           i.fa.fa-times-circle.js-close-announcement
           i.fa.fa-times-circle.js-close-announcement
 
 
 template(name="offlineWarning")
 template(name="offlineWarning")

+ 5 - 0
client/components/main/header.js

@@ -1,11 +1,16 @@
 Meteor.subscribe('user-admin');
 Meteor.subscribe('user-admin');
 Meteor.subscribe('boards');
 Meteor.subscribe('boards');
+Meteor.subscribe('setting');
 
 
 Template.header.helpers({
 Template.header.helpers({
   wrappedHeader() {
   wrappedHeader() {
     return !Session.get('currentBoard');
     return !Session.get('currentBoard');
   },
   },
 
 
+  currentSetting() {
+    return Settings.findOne();
+  },
+
   hideLogo() {
   hideLogo() {
     return Utils.isMiniScreen() && Session.get('currentBoard');
     return Utils.isMiniScreen() && Session.get('currentBoard');
   },
   },

+ 1 - 3
client/components/main/header.styl

@@ -188,8 +188,6 @@
     width: 100%
     width: 100%
     padding: 10px 0px
     padding: 10px 0px
     z-index: 30
     z-index: 30
-    position: absolute
-    bottom: 0px
 
 
     ul
     ul
       width: calc(100% - 60px)
       width: calc(100% - 60px)
@@ -218,7 +216,7 @@
       position: absolute
       position: absolute
       right: 0px
       right: 0px
       padding: 10px
       padding: 10px
-      margin: -10px
+      margin: -10px 0 -10px -10px
 
 
 .announcement,
 .announcement,
 .offline-warning
 .offline-warning

+ 41 - 18
client/components/main/layouts.jade

@@ -1,7 +1,6 @@
 head
 head
-  title Wekan
-  meta(name="viewport"
-   content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
+  title
+  meta(name="viewport" content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
   meta(http-equiv="X-UA-Compatible" content="IE=edge")
   meta(http-equiv="X-UA-Compatible" content="IE=edge")
   //- XXX We should use pathFor in the following `href` to support the case
   //- 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
     where the application is deployed with a path prefix, but it seems to be
@@ -9,34 +8,47 @@ head
     packages.
     packages.
   link(rel="shortcut icon" href="/wekan-favicon.png")
   link(rel="shortcut icon" href="/wekan-favicon.png")
   link(rel="apple-touch-icon" href="/wekan-favicon.png")
   link(rel="apple-touch-icon" href="/wekan-favicon.png")
+  link(rel="mask-icon" href="/wekan-logo-150.svg")
   link(rel="manifest" href="/wekan-manifest.json")
   link(rel="manifest" href="/wekan-manifest.json")
 
 
 template(name="userFormsLayout")
 template(name="userFormsLayout")
   section.auth-layout
   section.auth-layout
-    h1.at-form-landing-logo
-      img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan")
     section.auth-dialog
     section.auth-dialog
-      +Template.dynamic(template=content)
-      div.at-form-lang
-        select.select-lang.js-userform-set-language
-          each languages
-            if isCurrentLanguage
-              option(value="{{tag}}" selected="selected") {{name}}
-            else
-              option(value="{{tag}}") {{name}}
+      if isLoading
+        +loader
+      else
+        +Template.dynamic(template=content)
+        if currentSetting.displayAuthenticationMethod
+          +connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
+        div.at-form-lang
+          select.select-lang.js-userform-set-language
+            each languages
+              if isCurrentLanguage
+                option(value="{{tag}}" selected="selected") {{name}}
+              else
+                option(value="{{tag}}") {{name}}
 
 
 template(name="defaultLayout")
 template(name="defaultLayout")
   +header
   +header
   #content
   #content
+    | {{{afterBodyStart}}}
     +Template.dynamic(template=content)
     +Template.dynamic(template=content)
+    | {{{beforeBodyEnd}}}
   if (Modal.isOpen)
   if (Modal.isOpen)
     #modal
     #modal
       .overlay
       .overlay
-      .modal-content
-        a.modal-close-btn.js-close-modal
-          i.fa.fa-times-thin
-        +Template.dynamic(template=Modal.getHeaderName)
-        +Template.dynamic(template=Modal.getTemplateName)
+      if (Modal.isWide)
+        .modal-content-wide.modal-container
+          a.modal-close-btn.js-close-modal
+            i.fa.fa-times-thin
+          +Template.dynamic(template=Modal.getHeaderName)
+          +Template.dynamic(template=Modal.getTemplateName)
+      else
+        .modal-content.modal-container
+          a.modal-close-btn.js-close-modal
+            i.fa.fa-times-thin
+          +Template.dynamic(template=Modal.getHeaderName)
+          +Template.dynamic(template=Modal.getTemplateName)
 
 
 template(name="notFound")
 template(name="notFound")
   +message(label='page-not-found')
   +message(label='page-not-found')
@@ -47,3 +59,14 @@ template(name="message")
     unless currentUser
     unless currentUser
       with(pathFor route='atSignIn')
       with(pathFor route='atSignIn')
         p {{{_ 'page-maybe-private' this}}}
         p {{{_ 'page-maybe-private' this}}}
+
+template(name="loader")
+  h1.loadingText {{_ 'loading'}}
+  .lds-roller
+    div
+    div
+    div
+    div
+    div
+    div
+    div

+ 115 - 0
client/components/main/layouts.js

@@ -6,7 +6,36 @@ const i18nTagToT9n = (i18nTag) => {
   return i18nTag;
   return i18nTag;
 };
 };
 
 
+const validator = {
+  set(obj, prop, value) {
+    if (prop === 'state' && value !== 'signIn') {
+      $('.at-form-authentication').hide();
+    } else if (prop === 'state' && value === 'signIn') {
+      $('.at-form-authentication').show();
+    }
+    // The default behavior to store the value
+    obj[prop] = value;
+    // Indicate success
+    return true;
+  },
+};
+
+Template.userFormsLayout.onCreated(function() {
+  const instance = this;
+  instance.currentSetting = new ReactiveVar();
+  instance.isLoading = new ReactiveVar(false);
+
+  Meteor.subscribe('setting', {
+    onReady() {
+      instance.currentSetting.set(Settings.findOne());
+      return this.stop();
+    },
+  });
+});
+
 Template.userFormsLayout.onRendered(() => {
 Template.userFormsLayout.onRendered(() => {
+  AccountsTemplates.state.form.keys = new Proxy(AccountsTemplates.state.form.keys, validator);
+
   const i18nTag = navigator.language;
   const i18nTag = navigator.language;
   if (i18nTag) {
   if (i18nTag) {
     T9n.setLanguage(i18nTagToT9n(i18nTag));
     T9n.setLanguage(i18nTagToT9n(i18nTag));
@@ -15,6 +44,22 @@ Template.userFormsLayout.onRendered(() => {
 });
 });
 
 
 Template.userFormsLayout.helpers({
 Template.userFormsLayout.helpers({
+  currentSetting() {
+    return Template.instance().currentSetting.get();
+  },
+
+  isLoading() {
+    return Template.instance().isLoading.get();
+  },
+
+  afterBodyStart() {
+    return currentSetting.customHTMLafterBodyStart;
+  },
+
+  beforeBodyEnd() {
+    return currentSetting.customHTMLbeforeBodyEnd;
+  },
+
   languages() {
   languages() {
     return _.map(TAPi18n.getLanguages(), (lang, code) => {
     return _.map(TAPi18n.getLanguages(), (lang, code) => {
       const tag = code;
       const tag = code;
@@ -47,6 +92,15 @@ Template.userFormsLayout.events({
     T9n.setLanguage(i18nTagToT9n(i18nTag));
     T9n.setLanguage(i18nTagToT9n(i18nTag));
     evt.preventDefault();
     evt.preventDefault();
   },
   },
+  'click #at-btn'(event, instance) {
+    if (FlowRouter.getRouteName() === 'atSignIn') {
+      instance.isLoading.set(true);
+      authentication(event, instance)
+        .then(() => {
+          instance.isLoading.set(false);
+        });
+    }
+  },
 });
 });
 
 
 Template.defaultLayout.events({
 Template.defaultLayout.events({
@@ -54,3 +108,64 @@ Template.defaultLayout.events({
     Modal.close();
     Modal.close();
   },
   },
 });
 });
+
+async function authentication(event, instance) {
+  const match = $('#at-field-username_and_email').val();
+  const password = $('#at-field-password').val();
+
+  if (!match || !password) return undefined;
+
+  const result = await getAuthenticationMethod(instance.currentSetting.get(), match);
+
+  if (result === 'password') return undefined;
+
+  // Stop submit #at-pwd-form
+  event.preventDefault();
+  event.stopImmediatePropagation();
+
+  switch (result) {
+  case 'ldap':
+    return new Promise((resolve) => {
+      Meteor.loginWithLDAP(match, password, function() {
+        resolve(FlowRouter.go('/'));
+      });
+    });
+
+  case 'cas':
+    return new Promise((resolve) => {
+      Meteor.loginWithCas(match, password, function() {
+        resolve(FlowRouter.go('/'));
+      });
+    });
+
+  default:
+    return undefined;
+  }
+}
+
+function getAuthenticationMethod({displayAuthenticationMethod, defaultAuthenticationMethod}, match) {
+  if (displayAuthenticationMethod) {
+    return $('.select-authentication').val();
+  }
+  return getUserAuthenticationMethod(defaultAuthenticationMethod, match);
+}
+
+function getUserAuthenticationMethod(defaultAuthenticationMethod, match) {
+  return new Promise((resolve) => {
+    try {
+      Meteor.subscribe('user-authenticationMethod', match, {
+        onReady() {
+          const user = Users.findOne();
+
+          const authenticationMethod = user
+            ? user.authenticationMethod
+            : defaultAuthenticationMethod;
+
+          resolve(authenticationMethod);
+        },
+      });
+    } catch(error) {
+      resolve(defaultAuthenticationMethod);
+    }
+  });
+}

+ 120 - 3
client/components/main/layouts.styl

@@ -62,6 +62,23 @@ body
       float: right
       float: right
       font-size: 24px
       font-size: 24px
 
 
+  .modal-content-wide
+    width: 800px
+    min-height: 0px
+    margin: 42px auto
+    padding: 12px
+    border-radius: 4px
+    background: darken(white, 13%)
+    z-index: 110
+
+    h2
+      margin-bottom: 25px
+
+    .modal-close-btn
+      display: block
+      float: right
+      font-size: 24px
+
 h1
 h1
   font-size: 22px
   font-size: 22px
   line-height: 1.2em
   line-height: 1.2em
@@ -273,7 +290,7 @@ kbd
 // Implement a thiner close icon as suggested in
 // Implement a thiner close icon as suggested in
 // https://github.com/FortAwesome/Font-Awesome/issues/1540#issuecomment-68689950
 // https://github.com/FortAwesome/Font-Awesome/issues/1540#issuecomment-68689950
 .fa.fa-times-thin:before
 .fa.fa-times-thin:before
-  content: '\00d7';
+  content: '\00d7'
 
 
 .fa.fa-globe.colorful, .fa.fa-bell.colorful
 .fa.fa-globe.colorful, .fa.fa-bell.colorful
   color: #4caf50
   color: #4caf50
@@ -368,8 +385,8 @@ a
 
 
 @media screen and (max-width: 800px)
 @media screen and (max-width: 800px)
   #content
   #content
-    margin: 1px 0px 49px 0px
-    height: calc(100% - 96px)
+    margin: 1px 0px 0px 0px
+    height: calc(100% - 0px)
 
 
     > .wrapper
     > .wrapper
       margin-top: 0px
       margin-top: 0px
@@ -382,3 +399,103 @@ a
   height: 37px
   height: 37px
   margin: 8px 10px 0 0
   margin: 8px 10px 0 0
   width: 50px
   width: 50px
+
+.select-authentication
+  width: 100%
+
+.auth-layout
+  display: flex
+  flex-direction: column
+  align-items: center
+  justify-content: center
+  height: 100%
+
+  .auth-dialog
+    margin: 0 !important
+
+.loadingText
+  text-align: center
+
+.lds-roller
+  display: block
+  margin: auto
+  position: relative
+  width: 64px
+  height: 64px
+
+  div
+    animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite
+    transform-origin: 32px 32px
+
+  div:after
+    content: " "
+    display: block
+    position: absolute
+    width: 6px
+    height: 6px
+    border-radius: 50%
+    background: #dedede
+    margin: -3px 0 0 -3px
+
+  div:nth-child(1)
+    animation-delay: -0.036s
+
+  div:nth-child(1):after
+    top: 50px
+    left: 50px
+
+  div:nth-child(2)
+    animation-delay: -0.072s
+
+  div:nth-child(2):after
+    top: 54px
+    left: 45px
+
+  div:nth-child(3)
+    animation-delay: -0.108s
+
+  div:nth-child(3):after
+    top: 57px
+    left: 39px
+
+  div:nth-child(4)
+    animation-delay: -0.144s
+
+  div:nth-child(4):after
+    top: 58px
+    left: 32px
+
+  div:nth-child(5)
+    animation-delay: -0.18s
+
+  div:nth-child(5):after
+    top: 57px
+    left: 25px
+
+  div:nth-child(6)
+    animation-delay: -0.216s
+
+  div:nth-child(6):after
+    top: 54px
+    left: 19px
+
+  div:nth-child(7)
+    animation-delay: -0.252s
+
+  div:nth-child(7):after
+    top: 50px
+    left: 14px
+
+  div:nth-child(8)
+    animation-delay: -0.288s
+
+  div:nth-child(8):after
+    top: 45px
+    left: 10px
+
+@keyframes lds-roller
+  0%
+    transform: rotate(0deg)
+
+  100%
+    transform: rotate(360deg)

+ 3 - 0
client/components/main/popup.styl

@@ -33,6 +33,9 @@ $popupWidth = 300px
   textarea
   textarea
     height: 72px
     height: 72px
 
 
+  form a span
+    padding: 0 0.5rem
+
   .header
   .header
     height: 36px
     height: 36px
     position: relative
     position: relative

+ 11 - 7
client/components/mixins/perfectScrollbar.js

@@ -1,12 +1,16 @@
+const { isTouchDevice } = Utils;
+
 Mixins.PerfectScrollbar = BlazeComponent.extendComponent({
 Mixins.PerfectScrollbar = BlazeComponent.extendComponent({
   onRendered() {
   onRendered() {
-    const component = this.mixinParent();
-    const domElement = component.find('.js-perfect-scrollbar');
-    Ps.initialize(domElement);
+    if (!isTouchDevice()) {
+      const component = this.mixinParent();
+      const domElement = component.find('.js-perfect-scrollbar');
+      Ps.initialize(domElement);
 
 
-    // XXX We should create an event map to be consistent with other components
-    // but since BlazeComponent doesn't merge Mixins events transparently I
-    // prefered to use a jQuery event (which is what an event map ends up doing)
-    component.$(domElement).on('mouseenter', () => Ps.update(domElement));
+      // XXX We should create an event map to be consistent with other components
+      // but since BlazeComponent doesn't merge Mixins events transparently I
+      // prefered to use a jQuery event (which is what an event map ends up doing)
+      component.$(domElement).on('mouseenter', () => Ps.update(domElement));
+    }
   },
   },
 });
 });

BIN
client/components/rules/.DS_Store


+ 72 - 0
client/components/rules/actions/boardActions.jade

@@ -0,0 +1,72 @@
+template(name="boardActions")
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-move-card-to'}}
+      div.trigger-dropdown
+        select(id="move-gen-action")
+          option(value="top") {{_'r-top-of'}}
+          option(value="bottom") {{_'r-bottom-of'}}
+      div.trigger-text 
+        | {{_'r-its-list'}}
+    div.trigger-button.js-add-gen-move-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-move-card-to'}}
+      div.trigger-dropdown
+        select(id="move-spec-action")
+          option(value="top") {{_'r-top-of'}}
+          option(value="bottom") {{_'r-bottom-of'}}
+      div.trigger-text 
+        | {{_'r-list'}}
+      div.trigger-dropdown
+        input(id="listName",type=text,placeholder="{{_'r-name'}}")
+    div.trigger-button.js-add-spec-move-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-dropdown
+        select(id="arch-action")
+          option(value="archive") {{_'r-archive'}}
+          option(value="unarchive") {{_'r-unarchive'}}
+      div.trigger-text 
+        | {{_'r-card'}}
+    div.trigger-button.js-add-arch-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-add-swimlane'}}
+      div.trigger-dropdown
+        input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
+    div.trigger-button.js-add-swimlane-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-create-card'}}
+      div.trigger-dropdown
+        input(id="card-name",type=text,placeholder="{{_'r-name'}}")
+      div.trigger-text 
+        | {{_'r-in-list'}}
+      div.trigger-dropdown
+        input(id="list-name",type=text,placeholder="{{_'r-name'}}")
+      div.trigger-text 
+        | {{_'r-in-swimlane'}}
+      div.trigger-dropdown
+        input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
+    div.trigger-button.js-create-card-action.js-goto-rules
+      i.fa.fa-plus
+
+
+   
+  
+
+
+

+ 168 - 0
client/components/rules/actions/boardActions.js

@@ -0,0 +1,168 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+
+  },
+
+  events() {
+    return [{
+      'click .js-create-card-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const cardName = this.find('#card-name').value;
+        const listName = this.find('#list-name').value;
+        const swimlaneName = this.find('#swimlane-name2').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const triggerId = Triggers.insert(trigger);
+        const actionId = Actions.insert({
+          actionType: 'createCard',
+          swimlaneName,
+          cardName,
+          listName,
+          boardId,
+          desc,
+        });
+        Rules.insert({
+          title: ruleName,
+          triggerId,
+          actionId,
+          boardId,
+        });
+
+      },
+      'click .js-add-swimlane-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const swimlaneName = this.find('#swimlane-name').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const triggerId = Triggers.insert(trigger);
+        const actionId = Actions.insert({
+          actionType: 'addSwimlane',
+          swimlaneName,
+          boardId,
+          desc,
+        });
+        Rules.insert({
+          title: ruleName,
+          triggerId,
+          actionId,
+          boardId,
+        });
+
+      },
+      'click .js-add-spec-move-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const actionSelected = this.find('#move-spec-action').value;
+        const listTitle = this.find('#listName').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        if (actionSelected === 'top') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'moveCardToTop',
+            listTitle,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+        if (actionSelected === 'bottom') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'moveCardToBottom',
+            listTitle,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+      },
+      'click .js-add-gen-move-action' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const boardId = Session.get('currentBoard');
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const actionSelected = this.find('#move-gen-action').value;
+        if (actionSelected === 'top') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'moveCardToTop',
+            'listTitle': '*',
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+        if (actionSelected === 'bottom') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'moveCardToBottom',
+            'listTitle': '*',
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+      },
+      'click .js-add-arch-action' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const boardId = Session.get('currentBoard');
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const actionSelected = this.find('#arch-action').value;
+        if (actionSelected === 'archive') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'archive',
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+        if (actionSelected === 'unarchive') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'unarchive',
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+      },
+    }];
+  },
+
+}).register('boardActions');
+/* eslint-no-undef */

+ 55 - 0
client/components/rules/actions/cardActions.jade

@@ -0,0 +1,55 @@
+template(name="cardActions")
+  div.trigger-item
+    div.trigger-content
+      div.trigger-dropdown
+        select(id="label-action")
+          option(value="add") {{{_'r-add'}}}
+          option(value="remove") {{{_'r-remove'}}}
+      div.trigger-text 
+        | {{{_'r-label'}}}
+      div.trigger-dropdown
+        select(id="label-id")
+          each labels
+            option(value="#{_id}")
+              = name
+    div.trigger-button.js-add-label-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-dropdown
+        select(id="member-action")
+          option(value="add") {{{_'r-add'}}}
+          option(value="remove") {{{_'r-remove'}}}
+      div.trigger-text 
+        | {{{_'r-member'}}}
+      div.trigger-dropdown
+        input(id="member-name",type=text,placeholder="{{{_'r-name'}}}")  
+    div.trigger-button.js-add-member-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{{_'r-remove-all'}}}
+    div.trigger-button.js-add-removeall-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text
+        | {{{_'r-set-color'}}}
+        button.trigger-button.trigger-button-color.js-show-color-palette(
+          id="color-action"
+          class="card-details-{{cardColorButton}}")
+          | {{{_ cardColorButtonText }}}
+    div.trigger-button.js-set-color-action.js-goto-rules
+      i.fa.fa-plus
+
+template(name="setCardActionsColorPopup")
+  form.edit-label
+    .palette-colors: each colors
+      span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
+        if(isSelected color)
+          i.fa.fa-check
+    button.primary.confirm.js-submit {{_ 'save'}}

+ 188 - 0
client/components/rules/actions/cardActions.js

@@ -0,0 +1,188 @@
+let cardColors;
+Meteor.startup(() => {
+  cardColors = Cards.simpleSchema()._schema.color.allowedValues;
+});
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.subscribe('allRules');
+    this.cardColorButtonValue = new ReactiveVar('green');
+  },
+
+  cardColorButton() {
+    return this.cardColorButtonValue.get();
+  },
+
+  cardColorButtonText() {
+    return `color-${ this.cardColorButtonValue.get() }`;
+  },
+
+  labels() {
+    const labels = Boards.findOne(Session.get('currentBoard')).labels;
+    for (let i = 0; i < labels.length; i++) {
+      if (labels[i].name === '' || labels[i].name === undefined) {
+        labels[i].name = labels[i].color.toUpperCase();
+      }
+    }
+    return labels;
+  },
+
+  events() {
+    return [{
+      'click .js-add-label-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const actionSelected = this.find('#label-action').value;
+        const labelId = this.find('#label-id').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        if (actionSelected === 'add') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'addLabel',
+            labelId,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+        if (actionSelected === 'remove') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'removeLabel',
+            labelId,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+
+      },
+      'click .js-add-member-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const actionSelected = this.find('#member-action').value;
+        const username = this.find('#member-name').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        if (actionSelected === 'add') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'addMember',
+            username,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+            desc,
+          });
+        }
+        if (actionSelected === 'remove') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'removeMember',
+            username,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+      },
+      'click .js-add-removeall-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const triggerId = Triggers.insert(trigger);
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const boardId = Session.get('currentBoard');
+        const actionId = Actions.insert({
+          actionType: 'removeMember',
+          'username': '*',
+          boardId,
+          desc,
+        });
+        Rules.insert({
+          title: ruleName,
+          triggerId,
+          actionId,
+          boardId,
+        });
+      },
+      'click .js-show-color-palette'(event){
+        const funct = Popup.open('setCardActionsColor');
+        const colorButton = this.find('#color-action');
+        if (colorButton.value === '') {
+          colorButton.value = 'green';
+        }
+        funct.call(this, event);
+      },
+      'click .js-set-color-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const selectedColor = this.cardColorButtonValue.get();
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const triggerId = Triggers.insert(trigger);
+        const actionId = Actions.insert({
+          actionType: 'setColor',
+          selectedColor,
+          boardId,
+          desc,
+        });
+        Rules.insert({
+          title: ruleName,
+          triggerId,
+          actionId,
+          boardId,
+        });
+      },
+    }];
+  },
+
+}).register('cardActions');
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentAction = this.currentData();
+    this.colorButtonValue = Popup.getOpenerComponent().cardColorButtonValue;
+    this.currentColor = new ReactiveVar(this.colorButtonValue.get());
+  },
+
+  colors() {
+    return cardColors.map((color) => ({ color, name: '' }));
+  },
+
+  isSelected(color) {
+    return this.currentColor.get() === color;
+  },
+
+  events() {
+    return [{
+      'click .js-palette-color'() {
+        this.currentColor.set(this.currentData().color);
+      },
+      'click .js-submit' () {
+        this.colorButtonValue.set(this.currentColor.get());
+        Popup.close();
+      },
+    }];
+  },
+}).register('setCardActionsColorPopup');

+ 70 - 0
client/components/rules/actions/checklistActions.jade

@@ -0,0 +1,70 @@
+template(name="checklistActions")
+  div.trigger-item
+    div.trigger-content
+      div.trigger-dropdown
+        select(id="check-action")
+          option(value="add") {{{_'r-add'}}}
+          option(value="remove") {{{_'r-remove'}}}
+      div.trigger-text 
+        | {{{_'r-checklist'}}}
+      div.trigger-dropdown
+        input(id="checklist-name",type=text,placeholder="{{{_'r-name'}}}")  
+    div.trigger-button.js-add-checklist-action.js-goto-rules
+      i.fa.fa-plus  
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-dropdown
+        select(id="checkall-action")
+          option(value="check") {{{_'r-check-all'}}}
+          option(value="uncheck") {{{_'r-uncheck-all'}}}
+      div.trigger-text 
+        | {{{_'r-items-check'}}}
+      div.trigger-dropdown
+        input(id="checklist-name2",type=text,placeholder="{{{_'r-name'}}}")  
+    div.trigger-button.js-add-checkall-action.js-goto-rules
+      i.fa.fa-plus
+
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-dropdown
+        select(id="check-item-action")
+          option(value="check") {{{_'r-check'}}}
+          option(value="uncheck") {{{_'r-uncheck'}}}
+      div.trigger-text 
+        | {{{_'r-item'}}}
+      div.trigger-dropdown
+        input(id="checkitem-name",type=text,placeholder="{{{_'r-name'}}}")
+      div.trigger-text 
+        | {{{_'r-of-checklist'}}}
+      div.trigger-dropdown
+        input(id="checklist-name3",type=text,placeholder="{{{_'r-name'}}}")  
+    div.trigger-button.js-add-check-item-action.js-goto-rules
+      i.fa.fa-plus
+
+  div.trigger-item
+      div.trigger-content
+        div.trigger-text 
+          | {{{_'r-add-checklist'}}}
+        div.trigger-dropdown
+          input(id="checklist-name-3",type=text,placeholder="{{{_'r-name'}}}")
+        div.trigger-text 
+          | {{{_'r-with-items'}}}
+        div.trigger-dropdown
+          input(id="checklist-items",type=text,placeholder="{{{_'r-items-list'}}}")    
+      div.trigger-button.js-add-checklist-items-action.js-goto-rules
+        i.fa.fa-plus  
+
+  div.trigger-item
+      div.trigger-content
+        div.trigger-text 
+          | {{{_'r-checklist-note'}}}
+
+
+
+   
+  
+
+
+

+ 151 - 0
client/components/rules/actions/checklistActions.js

@@ -0,0 +1,151 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.subscribe('allRules');
+  },
+  events() {
+    return [{
+      'click .js-add-checklist-items-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const checklistName = this.find('#checklist-name-3').value;
+        const checklistItems = this.find('#checklist-items').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const triggerId = Triggers.insert(trigger);
+        const actionId = Actions.insert({
+          actionType: 'addChecklistWithItems',
+          checklistName,
+          checklistItems,
+          boardId,
+          desc,
+        });
+        Rules.insert({
+          title: ruleName,
+          triggerId,
+          actionId,
+          boardId,
+        });
+
+      },
+      'click .js-add-checklist-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const actionSelected = this.find('#check-action').value;
+        const checklistName = this.find('#checklist-name').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        if (actionSelected === 'add') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'addChecklist',
+            checklistName,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+        if (actionSelected === 'remove') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'removeChecklist',
+            checklistName,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+
+      },
+      'click .js-add-checkall-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const actionSelected = this.find('#checkall-action').value;
+        const checklistName = this.find('#checklist-name2').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        if (actionSelected === 'check') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'checkAll',
+            checklistName,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+        if (actionSelected === 'uncheck') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'uncheckAll',
+            checklistName,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+      },
+      'click .js-add-check-item-action' (event) {
+        const ruleName = this.data().ruleName.get();
+        const trigger = this.data().triggerVar.get();
+        const checkItemName = this.find('#checkitem-name');
+        const checklistName = this.find('#checklist-name3');
+        const actionSelected = this.find('#check-item-action').value;
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        if (actionSelected === 'check') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'checkItem',
+            checklistName,
+            checkItemName,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+        if (actionSelected === 'uncheck') {
+          const triggerId = Triggers.insert(trigger);
+          const actionId = Actions.insert({
+            actionType: 'uncheckItem',
+            checklistName,
+            checkItemName,
+            boardId,
+            desc,
+          });
+          Rules.insert({
+            title: ruleName,
+            triggerId,
+            actionId,
+            boardId,
+          });
+        }
+      },
+    }];
+  },
+
+}).register('checklistActions');

+ 11 - 0
client/components/rules/actions/mailActions.jade

@@ -0,0 +1,11 @@
+template(name="mailActions")
+  div.trigger-item.trigger-item-mail
+    div.trigger-content.trigger-content-mail
+      div.trigger-text.trigger-text-email
+        | {{_'r-send-email'}}
+      div.trigger-dropdown-mail
+        input(id="email-to",type=text,placeholder="{{_'r-to'}}")
+      input(id="email-subject",type=text,placeholder="{{_'r-subject'}}")
+      textarea(id="email-msg")  
+    div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules
+      i.fa.fa-plus

+ 35 - 0
client/components/rules/actions/mailActions.js

@@ -0,0 +1,35 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+
+  },
+
+  events() {
+    return [{
+      'click .js-mail-action' (event) {
+        const emailTo = this.find('#email-to').value;
+        const emailSubject = this.find('#email-subject').value;
+        const emailMsg = this.find('#email-msg').value;
+        const trigger = this.data().triggerVar.get();
+        const ruleName = this.data().ruleName.get();
+        const triggerId = Triggers.insert(trigger);
+        const boardId = Session.get('currentBoard');
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const actionId = Actions.insert({
+          actionType: 'sendEmail',
+          emailTo,
+          emailSubject,
+          emailMsg,
+          boardId,
+          desc,
+        });
+        Rules.insert({
+          title: ruleName,
+          triggerId,
+          actionId,
+          boardId,
+        });
+      },
+    }];
+  },
+
+}).register('mailActions');

+ 20 - 0
client/components/rules/ruleDetails.jade

@@ -0,0 +1,20 @@
+template(name="ruleDetails")
+  .rules
+    h2
+      i.fa.fa-magic
+      | {{{_ 'r-rule-details' }}}
+    .triggers-content
+        .triggers-body
+            .triggers-main-body
+                div.trigger-item
+                    div.trigger-content
+                        div.trigger-text 
+                        = trigger 
+                div.trigger-item
+                    div.trigger-content
+                        div.trigger-text 
+                        = action 
+    div.rules-back
+        button.js-goback
+          i.fa.fa-chevron-left
+          | {{{_ 'back'}}}

+ 38 - 0
client/components/rules/ruleDetails.js

@@ -0,0 +1,38 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.subscribe('allRules');
+    this.subscribe('allTriggers');
+    this.subscribe('allActions');
+
+  },
+
+  trigger() {
+    const ruleId = this.data().ruleId;
+    const rule = Rules.findOne({
+      _id: ruleId.get(),
+    });
+    const trigger = Triggers.findOne({
+      _id: rule.triggerId,
+    });
+    const desc = trigger.description();
+    const upperdesc = desc.charAt(0).toUpperCase() + desc.substr(1);
+    return upperdesc;
+  },
+  action() {
+    const ruleId = this.data().ruleId;
+    const rule = Rules.findOne({
+      _id: ruleId.get(),
+    });
+    const action = Actions.findOne({
+      _id: rule.actionId,
+    });
+    const desc = action.description();
+    const upperdesc = desc.charAt(0).toUpperCase() + desc.substr(1);
+    return upperdesc;
+  },
+
+  events() {
+    return [{}];
+  },
+
+}).register('ruleDetails');

+ 190 - 0
client/components/rules/rules.styl

@@ -0,0 +1,190 @@
+.rules-list
+  overflow:hidden
+  overflow-y:scroll
+  max-height: 400px
+.rules-lists-item
+  display: block
+  position: relative
+  overflow: auto
+  p
+  	display: inline-block
+  	float: left
+  	margin: revert
+.hide-element
+  display:none !important
+.user-details
+  display:inline-block 
+.rules-btns-group
+  position: absolute
+  right: 0
+  top: 50%
+  transform: translateY(-50%)
+  button
+  	margin: auto
+.rules-add
+  display: block
+  overflow: auto
+  margin-top: 15px
+  margin-bottom: 5px
+  input
+  	display: inline-block
+  	float: right
+  	margin: auto
+  	margin-right: 10px
+  button
+  	display: inline-block
+  	float: right
+  	margin: auto
+.rules-back
+  display: block
+  overflow: auto
+  margin-top: 15px
+  margin-bottom: 5px
+  button
+    display: inline-block
+    float: right
+    margin: auto
+    margin-right:14px
+   
+.flex
+  display: -webkit-box
+  display: -moz-box
+  display: -webkit-flex
+  display: -moz-flex
+  display: -ms-flexbox
+  display: flex
+
+
+
+.triggers-content
+  color: #727479
+  background: #dedede
+  .triggers-body
+    display flex
+    padding-top 15px
+    height 100%
+
+    .triggers-side-menu
+      background-color: #f7f7f7
+      border: 1px solid #f0f0f0
+      border-radius: 4px
+      height: intrinsic
+      box-shadow: inset -1px -1px 3px rgba(0,0,0,.05)
+
+      ul
+
+        li
+          margin: 0.1rem 0.2rem;
+          width:50px
+          height:50px
+          text-align:center
+          font-size: 25px
+          position: relative
+          
+          i
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            box-shadow: none
+            transform: translate(-50%,-50%);
+
+
+          &.active
+            background #fff
+            box-shadow 0 1px 2px rgba(0,0,0,0.15);
+
+          &:hover
+            background #fff
+            box-shadow 0 1px 2px rgba(0,0,0,0.15);
+          a
+            @extends .flex
+            padding: 1rem 0 1rem 1rem
+            width: 100% - 5rem
+
+
+            span
+              font-size: 13px
+    .triggers-main-body
+      padding: 0.1em 1em
+      width:100%
+      .trigger-item
+        overflow:auto
+        padding:10px
+        height:40px
+        margin-bottom:5px
+        border-radius: 3px
+        position: relative
+        background-color: white
+        .trigger-content
+            position:absolute
+            top:50%
+            transform: translateY(-50%)
+            left:10px
+            .trigger-text
+              font-size: 16px
+              display:inline-block
+            .trigger-inline-button
+              font-size: 16px
+              display: inline;
+              padding: 6px;
+              border: 1px solid #eee
+              border-radius: 4px
+              box-shadow: inset -1px -1px 3px rgba(0,0,0,.05) 
+              &:hover, &.is-active
+                box-shadow: 0 0 0 2px darken(white, 60%) inset
+            .trigger-text.trigger-text-email
+              margin-left: 5px;
+              margin-top: 10px;
+              margin-bottom: 10px;
+            .trigger-dropdown
+              display:inline-block
+              select
+                width:auto
+                height:30px
+                margin:0px
+                margin-left:5px
+              input
+                display: inline-block
+                width: 80px;
+                margin: 0;
+        .trigger-content-mail
+          left:20px
+          right:100px
+        .trigger-button
+          position:absolute
+          top:50%
+          transform: translateY(-50%)
+          width:30px
+          height:30px
+          border: 1px solid #eee
+          border-radius: 4px
+          box-shadow: inset -1px -1px 3px rgba(0,0,0,.05)
+          text-align:center
+          font-size: 20px
+          right:10px
+          i
+            position: absolute
+            top: 50%
+            left: 50%
+            box-shadow: none
+            transform: translate(-50%,-50%)
+          &:hover, &.is-active
+            box-shadow: 0 0 0 2px darken(white, 60%) inset
+        .trigger-button.trigger-button-email
+          top:30px
+        .trigger-button.trigger-button-person
+          right:-40px
+        .trigger-button.trigger-button-color
+          top: unset
+          position: unset
+          transform: unset
+          font-size: 16px
+          width:auto
+          padding-left: 10px
+          padding-right: 10px
+          height:40px
+      .trigger-item.trigger-item-mail
+        height:300px
+  
+
+

+ 29 - 0
client/components/rules/rulesActions.jade

@@ -0,0 +1,29 @@
+template(name="rulesActions")
+  h2
+    i.fa.fa-magic
+    | {{{_ 'r-rule' }}} "#{data.ruleName.get}" - {{{_ 'r-add-action'}}}
+  .triggers-content
+    .triggers-body
+      .triggers-side-menu
+        ul
+          li.active.js-set-board-actions
+            i.fa.fa-columns
+          li.js-set-card-actions
+            i.fa.fa-sticky-note
+          li.js-set-checklist-actions
+            i.fa.fa-check
+          li.js-set-mail-actions
+            i.fa.fa-at
+      .triggers-main-body
+        if ($eq currentActions.get 'board')
+          +boardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+        else if ($eq currentActions.get 'card')
+          +cardActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+        else if ($eq currentActions.get 'checklist')
+          +checklistActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+        else if ($eq currentActions.get 'mail')
+          +mailActions(ruleName=data.ruleName triggerVar=data.triggerVar)
+  div.rules-back
+        button.js-goback
+          i.fa.fa-chevron-left
+          | {{{_ 'back'}}}

+ 58 - 0
client/components/rules/rulesActions.js

@@ -0,0 +1,58 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentActions = new ReactiveVar('board');
+  },
+
+  setBoardActions() {
+    this.currentActions.set('board');
+    $('.js-set-card-actions').removeClass('active');
+    $('.js-set-board-actions').addClass('active');
+    $('.js-set-checklist-actions').removeClass('active');
+    $('.js-set-mail-actions').removeClass('active');
+  },
+  setCardActions() {
+    this.currentActions.set('card');
+    $('.js-set-card-actions').addClass('active');
+    $('.js-set-board-actions').removeClass('active');
+    $('.js-set-checklist-actions').removeClass('active');
+    $('.js-set-mail-actions').removeClass('active');
+  },
+  setChecklistActions() {
+    this.currentActions.set('checklist');
+    $('.js-set-card-actions').removeClass('active');
+    $('.js-set-board-actions').removeClass('active');
+    $('.js-set-checklist-actions').addClass('active');
+    $('.js-set-mail-actions').removeClass('active');
+  },
+  setMailActions() {
+    this.currentActions.set('mail');
+    $('.js-set-card-actions').removeClass('active');
+    $('.js-set-board-actions').removeClass('active');
+    $('.js-set-checklist-actions').removeClass('active');
+    $('.js-set-mail-actions').addClass('active');
+  },
+
+  rules() {
+    return Rules.find({});
+  },
+
+  name() {
+    // console.log(this.data());
+  },
+  events() {
+    return [{
+      'click .js-set-board-actions'(){
+        this.setBoardActions();
+      },
+      'click .js-set-card-actions'() {
+        this.setCardActions();
+      },
+      'click .js-set-mail-actions'() {
+        this.setMailActions();
+      },
+      'click .js-set-checklist-actions'() {
+        this.setChecklistActions();
+      },
+    }];
+  },
+}).register('rulesActions');

+ 27 - 0
client/components/rules/rulesList.jade

@@ -0,0 +1,27 @@
+template(name="rulesList")
+  .rules
+    h2
+      i.fa.fa-magic
+      | {{{_ 'r-board-rules' }}}
+
+    ul.rules-list
+      each rules
+        li.rules-lists-item
+          p 
+            = title
+          div.rules-btns-group
+            button.js-goto-details
+              i.fa.fa-eye
+              | {{{_ 'r-view-rule'}}}
+            if currentUser.isAdmin
+              button.js-delete-rule
+                i.fa.fa-trash-o
+                | {{{_ 'r-delete-rule'}}}
+      else
+        li.no-items-message {{{_ 'r-no-rules' }}}
+    if currentUser.isAdmin
+      div.rules-add
+        button.js-goto-trigger
+          i.fa.fa-plus
+          | {{{_ 'r-add-rule'}}}
+        input(type=text,placeholder="{{{_ 'r-new-rule-name' }}}",id="ruleTitle")

+ 15 - 0
client/components/rules/rulesList.js

@@ -0,0 +1,15 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.subscribe('allRules');
+  },
+
+  rules() {
+    const boardId = Session.get('currentBoard');
+    return Rules.find({
+      boardId,
+    });
+  },
+  events() {
+    return [{}];
+  },
+}).register('rulesList');

+ 9 - 0
client/components/rules/rulesMain.jade

@@ -0,0 +1,9 @@
+template(name="rulesMain")
+  if($eq rulesCurrentTab.get 'rulesList')
+    +rulesList
+  if($eq rulesCurrentTab.get 'trigger')
+    +rulesTriggers(ruleName=ruleName triggerVar=triggerVar)
+  if($eq rulesCurrentTab.get 'action')
+    +rulesActions(ruleName=ruleName triggerVar=triggerVar)
+  if($eq rulesCurrentTab.get 'ruleDetails')
+    +ruleDetails(ruleId=ruleId)

+ 97 - 0
client/components/rules/rulesMain.js

@@ -0,0 +1,97 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.rulesCurrentTab = new ReactiveVar('rulesList');
+    this.ruleName = new ReactiveVar('');
+    this.triggerVar = new ReactiveVar();
+    this.ruleId = new ReactiveVar();
+  },
+
+  setTrigger() {
+    this.rulesCurrentTab.set('trigger');
+  },
+  sanitizeObject(obj){
+    Object.keys(obj).forEach((key) => {
+      if(obj[key] === '' || obj[key] === undefined){
+        obj[key] = '*';
+      }}
+    );
+  },
+  setRulesList() {
+    this.rulesCurrentTab.set('rulesList');
+  },
+
+  setAction() {
+    this.rulesCurrentTab.set('action');
+  },
+
+  setRuleDetails() {
+    this.rulesCurrentTab.set('ruleDetails');
+  },
+
+  events() {
+    return [{
+      'click .js-delete-rule' () {
+        const rule = this.currentData();
+        Rules.remove(rule._id);
+        Actions.remove(rule.actionId);
+        Triggers.remove(rule.triggerId);
+
+      },
+      'click .js-goto-trigger' (event) {
+        event.preventDefault();
+        const ruleTitle = this.find('#ruleTitle').value;
+        if(ruleTitle !== undefined && ruleTitle !== ''){
+          this.find('#ruleTitle').value = '';
+          this.ruleName.set(ruleTitle);
+          this.setTrigger();
+        }
+      },
+      'click .js-goto-action' (event) {
+        event.preventDefault();
+        // Add user to the trigger
+        const username = $(event.currentTarget.offsetParent).find('.user-name').val();
+        let trigger = this.triggerVar.get();
+        trigger.userId = '*';
+        if(username !== undefined ){
+          const userFound = Users.findOne({username});
+          if(userFound !== undefined){
+            trigger.userId = userFound._id;
+            this.triggerVar.set(trigger);
+          }
+        }
+        // Sanitize trigger
+        trigger = this.triggerVar.get();
+        this.sanitizeObject(trigger);
+        this.triggerVar.set(trigger);
+        this.setAction();
+      },
+      'click .js-show-user-field' (event) {
+        event.preventDefault();
+        $(event.currentTarget.offsetParent).find('.user-details').removeClass('hide-element');
+      },
+      'click .js-goto-rules' (event) {
+        event.preventDefault();
+        this.setRulesList();
+      },
+      'click .js-goback' (event) {
+        event.preventDefault();
+        if(this.rulesCurrentTab.get() === 'trigger' || this.rulesCurrentTab.get() === 'ruleDetails' ){
+          this.setRulesList();
+        }
+        if(this.rulesCurrentTab.get() === 'action'){
+          this.setTrigger();
+        }
+      },
+      'click .js-goto-details' (event) {
+        event.preventDefault();
+        const rule = this.currentData();
+        this.ruleId.set(rule._id);
+        this.setRuleDetails();
+      },
+
+    }];
+  },
+
+}).register('rulesMain');
+
+

+ 25 - 0
client/components/rules/rulesTriggers.jade

@@ -0,0 +1,25 @@
+template(name="rulesTriggers")
+  h2
+    i.fa.fa-magic
+    | {{{_ 'r-rule' }}} "#{data.ruleName.get}" - {{{_ 'r-add-trigger'}}}
+  .triggers-content
+    .triggers-body
+      .triggers-side-menu
+        ul
+          li.active.js-set-board-triggers
+            i.fa.fa-columns
+          li.js-set-card-triggers
+            i.fa.fa-sticky-note
+          li.js-set-checklist-triggers
+            i.fa.fa-check
+      .triggers-main-body
+        if showBoardTrigger.get
+          +boardTriggers
+        else if showCardTrigger.get
+          +cardTriggers
+        else if showChecklistTrigger.get
+          +checklistTriggers
+  div.rules-back
+        button.js-goback
+          i.fa.fa-chevron-left
+          | {{{_ 'back'}}}

+ 53 - 0
client/components/rules/rulesTriggers.js

@@ -0,0 +1,53 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.showBoardTrigger = new ReactiveVar(true);
+    this.showCardTrigger = new ReactiveVar(false);
+    this.showChecklistTrigger = new ReactiveVar(false);
+  },
+
+  setBoardTriggers() {
+    this.showBoardTrigger.set(true);
+    this.showCardTrigger.set(false);
+    this.showChecklistTrigger.set(false);
+    $('.js-set-card-triggers').removeClass('active');
+    $('.js-set-board-triggers').addClass('active');
+    $('.js-set-checklist-triggers').removeClass('active');
+  },
+  setCardTriggers() {
+    this.showBoardTrigger.set(false);
+    this.showCardTrigger.set(true);
+    this.showChecklistTrigger.set(false);
+    $('.js-set-card-triggers').addClass('active');
+    $('.js-set-board-triggers').removeClass('active');
+    $('.js-set-checklist-triggers').removeClass('active');
+  },
+  setChecklistTriggers() {
+    this.showBoardTrigger.set(false);
+    this.showCardTrigger.set(false);
+    this.showChecklistTrigger.set(true);
+    $('.js-set-card-triggers').removeClass('active');
+    $('.js-set-board-triggers').removeClass('active');
+    $('.js-set-checklist-triggers').addClass('active');
+  },
+
+  rules() {
+    return Rules.find({});
+  },
+
+  name() {
+    // console.log(this.data());
+  },
+  events() {
+    return [{
+      'click .js-set-board-triggers' () {
+        this.setBoardTriggers();
+      },
+      'click .js-set-card-triggers' () {
+        this.setCardTriggers();
+      },
+      'click .js-set-checklist-triggers' () {
+        this.setChecklistTriggers();
+      },
+    }];
+  },
+}).register('rulesTriggers');

+ 116 - 0
client/components/rules/triggers/boardTriggers.jade

@@ -0,0 +1,116 @@
+template(name="boardTriggers")
+  div.trigger-item#trigger-two
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-a-card'}}
+      div.trigger-inline-button.js-open-card-title-popup 
+        i.fa.fa-filter
+      div.trigger-text 
+        | {{_'r-is'}}
+      div.trigger-text 
+        | {{_'r-added-to'}}
+      div.trigger-text 
+        | {{_'r-list'}}
+      div.trigger-dropdown
+        input(id="create-list-name",type=text,placeholder="{{_'r-list-name'}}")
+      div.trigger-text 
+        | {{_'r-in-swimlane'}}
+      div.trigger-dropdown
+        input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}") 
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-create-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item#trigger-three
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-a-card'}}
+      div.trigger-inline-button.js-open-card-title-popup 
+        i.fa.fa-filter
+      div.trigger-text 
+        | {{_'r-is-moved'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-gen-moved-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item#trigger-four
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-a-card'}}
+      div.trigger-inline-button.js-open-card-title-popup 
+        i.fa.fa-filter
+      div.trigger-text 
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="move-action")
+          option(value="moved-to") {{_'r-moved-to'}}
+          option(value="moved-from") {{_'r-moved-from'}}
+      div.trigger-text 
+        | {{_'r-list'}}
+      div.trigger-dropdown
+        input(id="move-list-name",type=text,placeholder="{{_'r-list-name'}}")
+      div.trigger-text 
+        | {{_'r-in-swimlane'}}
+      div.trigger-dropdown
+        input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}") 
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-moved-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item#trigger-five
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-a-card'}}
+      div.trigger-inline-button.js-open-card-title-popup 
+        i.fa.fa-filter
+      div.trigger-text 
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="arch-action")
+          option(value="archived") {{_'r-archived'}}
+          option(value="unarchived") {{_'r-unarchived'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-arch-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+      div.trigger-content
+        div.trigger-text 
+          | {{{_'r-board-note'}}}
+
+template(name="boardCardTitlePopup")
+    form
+      label
+        | Card Title Filter
+        input.js-card-filter-name(type="text" value=title autofocus)
+      input.js-card-filter-button.primary.wide(type="submit" value="{{_ 'set-filter'}}")
+
+   
+  
+
+
+

+ 119 - 0
client/components/rules/triggers/boardTriggers.js

@@ -0,0 +1,119 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.provaVar = new ReactiveVar('');
+    this.currentPopupTriggerId = 'def';
+    this.cardTitleFilters = {};
+  },
+  setNameFilter(name){
+    this.cardTitleFilters[this.currentPopupTriggerId] =  name;
+  },
+
+  events() {
+    return [{
+      'click .js-open-card-title-popup'(event){
+        const funct = Popup.open('boardCardTitle');
+        const divId = $(event.currentTarget.parentNode.parentNode).attr('id');
+        //console.log('current popup');
+        //console.log(this.currentPopupTriggerId);
+        this.currentPopupTriggerId = divId;
+        funct.call(this, event);
+      },
+      'click .js-add-create-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const listName = this.find('#create-list-name').value;
+        const swimlaneName = this.find('#create-swimlane-name').value;
+        const boardId = Session.get('currentBoard');
+        const divId = $(event.currentTarget.parentNode).attr('id');
+        const cardTitle = this.cardTitleFilters[divId];
+        // move to generic funciont
+        datas.triggerVar.set({
+          activityType: 'createCard',
+          boardId,
+          cardTitle,
+          swimlaneName,
+          listName,
+          desc,
+        });
+      },
+      'click .js-add-moved-trigger' (event) {
+        const datas = this.data();
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const swimlaneName = this.find('#create-swimlane-name-2').value;
+        const actionSelected = this.find('#move-action').value;
+        const listName = this.find('#move-list-name').value;
+        const boardId = Session.get('currentBoard');
+        const divId = $(event.currentTarget.parentNode).attr('id');
+        const cardTitle = this.cardTitleFilters[divId];
+        if (actionSelected === 'moved-to') {
+          datas.triggerVar.set({
+            activityType: 'moveCard',
+            boardId,
+            listName,
+            cardTitle,
+            swimlaneName,
+            'oldListName': '*',
+            desc,
+          });
+        }
+        if (actionSelected === 'moved-from') {
+          datas.triggerVar.set({
+            activityType: 'moveCard',
+            boardId,
+            cardTitle,
+            swimlaneName,
+            'listName': '*',
+            'oldListName': listName,
+            desc,
+          });
+        }
+      },
+      'click .js-add-gen-moved-trigger' (event){
+        const datas = this.data();
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const boardId = Session.get('currentBoard');
+
+        datas.triggerVar.set({
+          'activityType': 'moveCard',
+          boardId,
+          'swimlaneName': '*',
+          'listName':'*',
+          'oldListName': '*',
+          desc,
+        });
+      },
+      'click .js-add-arc-trigger' (event) {
+        const datas = this.data();
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const actionSelected = this.find('#arch-action').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'archived') {
+          datas.triggerVar.set({
+            activityType: 'archivedCard',
+            boardId,
+            desc,
+          });
+        }
+        if (actionSelected === 'unarchived') {
+          datas.triggerVar.set({
+            activityType: 'restoredCard',
+            boardId,
+            desc,
+          });
+        }
+      },
+
+    }];
+  },
+
+}).register('boardTriggers');
+
+
+Template.boardCardTitlePopup.events({
+  submit(evt, tpl) {
+    const title = tpl.$('.js-card-filter-name').val().trim();
+    Popup.getOpenerComponent().setNameFilter(title);
+    evt.preventDefault();
+    Popup.close();
+  },
+});

+ 114 - 0
client/components/rules/triggers/cardTriggers.jade

@@ -0,0 +1,114 @@
+template(name="cardTriggers")
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text
+        | {{_'r-when-a-label-is'}}
+      div.trigger-dropdown
+        select(id="label-action")
+          option(value="added") {{_'r-added-to'}}
+          option(value="removed") {{_'r-removed-from'}}
+      div.trigger-text
+        | {{_'r-a-card'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-gen-label-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text
+        | {{_'r-when-the-label-is'}}
+      div.trigger-dropdown
+        select(id="spec-label")
+          each labels
+            option(value="#{_id}" style="background-color: #{name}")
+              = translatedname
+      div.trigger-text
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="spec-label-action")
+          option(value="added") {{_'r-added-to'}}
+          option(value="removed") {{_'r-removed-from'}}
+      div.trigger-text
+        | {{_'r-a-card'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-spec-label-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text
+        | {{_'r-when-a-member'}}
+      div.trigger-dropdown
+        select(id="gen-member-action")
+          option(value="added") {{_'r-added-to'}}
+          option(value="removed") {{_'r-removed-from'}}
+      div.trigger-text
+        | {{_'r-a-card'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-gen-member-trigger.js-goto-action
+      i.fa.fa-plus
+
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text
+        | {{_'r-when-the-member'}}
+      div.trigger-dropdown
+        input(id="spec-member",type=text,placeholder="{{_'r-name'}}")
+      div.trigger-text
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="spec-member-action")
+          option(value="added") {{_'r-added-to'}}
+          option(value="removed") {{_'r-removed-from'}}
+      div.trigger-text
+        | {{_'r-a-card'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-spec-member-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text
+        | {{_'r-when-a-attach'}}
+      div.trigger-text
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="attach-action")
+          option(value="added") {{_'r-added-to'}}
+          option(value="removed") {{_'r-removed-from'}}
+      div.trigger-text
+        | {{_'r-a-card'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-attachment-trigger.js-goto-action
+      i.fa.fa-plus

+ 131 - 0
client/components/rules/triggers/cardTriggers.js

@@ -0,0 +1,131 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.subscribe('allRules');
+  },
+  labels() {
+    const labels = Boards.findOne(Session.get('currentBoard')).labels;
+    for (let i = 0; i < labels.length; i++) {
+      if (labels[i].name === '' || labels[i].name === undefined) {
+        labels[i].name = labels[i].color;
+        labels[i].translatedname = `${TAPi18n.__(`color-${  labels[i].color}`)}`;
+      } else {
+        labels[i].translatedname = labels[i].name;
+      }
+    }
+    return labels;
+  },
+  events() {
+    return [{
+      'click .js-add-gen-label-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#label-action').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'added') {
+          datas.triggerVar.set({
+            activityType: 'addedLabel',
+            boardId,
+            'labelId': '*',
+            desc,
+          });
+        }
+        if (actionSelected === 'removed') {
+          datas.triggerVar.set({
+            activityType: 'removedLabel',
+            boardId,
+            'labelId': '*',
+            desc,
+          });
+        }
+      },
+      'click .js-add-spec-label-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#spec-label-action').value;
+        const labelId = this.find('#spec-label').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'added') {
+          datas.triggerVar.set({
+            activityType: 'addedLabel',
+            boardId,
+            labelId,
+            desc,
+          });
+        }
+        if (actionSelected === 'removed') {
+          datas.triggerVar.set({
+            activityType: 'removedLabel',
+            boardId,
+            labelId,
+            desc,
+          });
+        }
+      },
+      'click .js-add-gen-member-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#gen-member-action').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'added') {
+          datas.triggerVar.set({
+            activityType: 'joinMember',
+            boardId,
+            'username': '*',
+            desc,
+          });
+        }
+        if (actionSelected === 'removed') {
+          datas.triggerVar.set({
+            activityType: 'unjoinMember',
+            boardId,
+            'username': '*',
+            desc,
+          });
+        }
+      },
+      'click .js-add-spec-member-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#spec-member-action').value;
+        const username = this.find('#spec-member').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'added') {
+          datas.triggerVar.set({
+            activityType: 'joinMember',
+            boardId,
+            username,
+            desc,
+          });
+        }
+        if (actionSelected === 'removed') {
+          datas.triggerVar.set({
+            activityType: 'unjoinMember',
+            boardId,
+            username,
+            desc,
+          });
+        }
+      },
+      'click .js-add-attachment-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#attach-action').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'added') {
+          datas.triggerVar.set({
+            activityType: 'addAttachment',
+            boardId,
+            desc,
+          });
+        }
+        if (actionSelected === 'removed') {
+          datas.triggerVar.set({
+            activityType: 'deleteAttachment',
+            boardId,
+            desc,
+          });
+        }
+      },
+    }];
+  },
+}).register('cardTriggers');

+ 125 - 0
client/components/rules/triggers/checklistTriggers.jade

@@ -0,0 +1,125 @@
+template(name="checklistTriggers")
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-a-checklist'}}
+      div.trigger-dropdown
+        select(id="gen-check-action")
+          option(value="created") {{_'r-added-to'}}
+          option(value="removed") {{_'r-removed-from'}}
+      div.trigger-text 
+        | {{_'r-a-card'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-gen-check-trigger.js-goto-action
+      i.fa.fa-plus
+
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-the-checklist'}}
+      div.trigger-dropdown
+        input(id="check-name",type=text,placeholder="{{_'r-name'}}") 
+      div.trigger-text 
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="spec-check-action")
+          option(value="created") {{_'r-added-to'}}
+          option(value="removed") {{_'r-removed-from'}}
+      div.trigger-text 
+        | {{_'r-a-card'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-spec-check-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-a-checklist'}}
+      div.trigger-dropdown
+        select(id="gen-comp-check-action")
+          option(value="completed") {{_'r-completed'}}
+          option(value="uncompleted") {{_'r-made-incomplete'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-gen-comp-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-the-checklist'}}
+      div.trigger-dropdown
+        input(id="spec-comp-check-name",type=text,placeholder="{{_'r-name'}}") 
+      div.trigger-text 
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="spec-comp-check-action")
+          option(value="completed") {{_'r-completed'}}
+          option(value="uncompleted") {{_'r-made-incomplete'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-spec-comp-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-a-item'}}
+      div.trigger-dropdown
+        select(id="check-item-gen-action")
+          option(value="checked") {{_'r-checked'}}
+          option(value="unchecked") {{_'r-unchecked'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-gen-check-item-trigger.js-goto-action
+      i.fa.fa-plus
+
+  div.trigger-item
+    div.trigger-content
+      div.trigger-text 
+        | {{_'r-when-the-item'}}
+      div.trigger-dropdown
+        input(id="check-item-name",type=text,placeholder="{{_'r-name'}}") 
+      div.trigger-text 
+        | {{_'r-is'}}
+      div.trigger-dropdown
+        select(id="check-item-spec-action")
+          option(value="checked") {{_'r-checked'}}
+          option(value="unchecked") {{_'r-unchecked'}}
+      div.trigger-button.trigger-button-person.js-show-user-field
+        i.fa.fa-user
+      div.user-details.hide-element
+        div.trigger-text
+          | {{_'r-by'}}
+        div.trigger-dropdown
+          input(class="user-name",type=text,placeholder="{{_'username'}}")
+    div.trigger-button.js-add-spec-check-item-trigger.js-goto-action
+      i.fa.fa-plus

+ 146 - 0
client/components/rules/triggers/checklistTriggers.js

@@ -0,0 +1,146 @@
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.subscribe('allRules');
+  },
+  events() {
+    return [{
+      'click .js-add-gen-check-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#gen-check-action').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'created') {
+          datas.triggerVar.set({
+            activityType: 'addChecklist',
+            boardId,
+            'checklistName': '*',
+            desc,
+          });
+        }
+        if (actionSelected === 'removed') {
+          datas.triggerVar.set({
+            activityType: 'removeChecklist',
+            boardId,
+            'checklistName': '*',
+            desc,
+          });
+        }
+      },
+      'click .js-add-spec-check-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#spec-check-action').value;
+        const checklistId = this.find('#check-name').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'created') {
+          datas.triggerVar.set({
+            activityType: 'addChecklist',
+            boardId,
+            'checklistName': checklistId,
+            desc,
+          });
+        }
+        if (actionSelected === 'removed') {
+          datas.triggerVar.set({
+            activityType: 'removeChecklist',
+            boardId,
+            'checklistName': checklistId,
+            desc,
+          });
+        }
+      },
+      'click .js-add-gen-comp-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+
+        const datas = this.data();
+        const actionSelected = this.find('#gen-comp-check-action').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'completed') {
+          datas.triggerVar.set({
+            activityType: 'completeChecklist',
+            boardId,
+            'checklistName': '*',
+            desc,
+          });
+        }
+        if (actionSelected === 'uncompleted') {
+          datas.triggerVar.set({
+            activityType: 'uncompleteChecklist',
+            boardId,
+            'checklistName': '*',
+            desc,
+          });
+        }
+      },
+      'click .js-add-spec-comp-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#spec-comp-check-action').value;
+        const checklistId = this.find('#spec-comp-check-name').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'completed') {
+          datas.triggerVar.set({
+            activityType: 'completeChecklist',
+            boardId,
+            'checklistName': checklistId,
+            desc,
+          });
+        }
+        if (actionSelected === 'uncompleted') {
+          datas.triggerVar.set({
+            activityType: 'uncompleteChecklist',
+            boardId,
+            'checklistName': checklistId,
+            desc,
+          });
+        }
+      },
+      'click .js-add-gen-check-item-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#check-item-gen-action').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'checked') {
+          datas.triggerVar.set({
+            activityType: 'checkedItem',
+            boardId,
+            'checklistItemName': '*',
+            desc,
+          });
+        }
+        if (actionSelected === 'unchecked') {
+          datas.triggerVar.set({
+            activityType: 'uncheckedItem',
+            boardId,
+            'checklistItemName': '*',
+            desc,
+          });
+        }
+      },
+      'click .js-add-spec-check-item-trigger' (event) {
+        const desc = Utils.getTriggerActionDesc(event, this);
+        const datas = this.data();
+        const actionSelected = this.find('#check-item-spec-action').value;
+        const checklistItemId = this.find('#check-item-name').value;
+        const boardId = Session.get('currentBoard');
+        if (actionSelected === 'checked') {
+          datas.triggerVar.set({
+            activityType: 'checkedItem',
+            boardId,
+            'checklistItemName': checklistItemId,
+            desc,
+          });
+        }
+        if (actionSelected === 'unchecked') {
+          datas.triggerVar.set({
+            activityType: 'uncheckedItem',
+            boardId,
+            'checklistItemName': checklistItemId,
+            desc,
+          });
+        }
+      },
+    }];
+  },
+
+}).register('checklistTriggers');

+ 9 - 0
client/components/settings/connectionMethod.jade

@@ -0,0 +1,9 @@
+template(name='connectionMethod')
+  div.at-form-authentication
+    label {{_ 'authentication-method'}}
+    select.select-authentication
+      each authentications
+        if isSelected value
+          option(value="{{value}}" selected) {{_ value}}
+        else
+          option(value="{{value}}") {{_ value}}

+ 37 - 0
client/components/settings/connectionMethod.js

@@ -0,0 +1,37 @@
+Template.connectionMethod.onCreated(function() {
+  this.authenticationMethods = new ReactiveVar([]);
+
+  Meteor.call('getAuthenticationsEnabled', (_, result) => {
+    if (result) {
+      // TODO : add a management of different languages
+      // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
+      this.authenticationMethods.set([
+        {value: 'password'},
+        // Gets only the authentication methods availables
+        ...Object.entries(result).filter((e) => e[1]).map((e) => ({value: e[0]})),
+      ]);
+    }
+
+    // If only the default authentication available, hides the select boxe
+    const content = $('.at-form-authentication');
+    if (!(this.authenticationMethods.get().length > 1)) {
+      content.hide();
+    } else {
+      content.show();
+    }
+  });
+});
+
+Template.connectionMethod.onRendered(() => {
+  // Moves the select boxe in the first place of the at-pwd-form div
+  $('.at-form-authentication').detach().prependTo('.at-pwd-form');
+});
+
+Template.connectionMethod.helpers({
+  authentications() {
+    return Template.instance().authenticationMethods.get();
+  },
+  isSelected(match) {
+    return Template.instance().data.authenticationMethod === match;
+  },
+});

+ 1 - 1
client/components/settings/informationBody.jade

@@ -17,7 +17,7 @@ template(name='statistics')
     table
     table
       tbody
       tbody
         tr
         tr
-          th {{_ 'Wekan_version'}}
+          th Wekan {{_ 'info'}}
           td {{statistics.version}}
           td {{statistics.version}}
         tr
         tr
           th {{_ 'Node_version'}}
           th {{_ 'Node_version'}}

Some files were not shown because too many files changed in this diff