浏览代码

Merge pull request #1 from wekan/devel

ldap changes
Thiago Fernando 6 年之前
父节点
当前提交
ce0473480b
共有 100 个文件被更改,包括 7950 次插入723 次删除
  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. 二进制
      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
   },
   "parserOptions": {
-    "ecmaVersion": 6,
+    "ecmaVersion": 2017,
     "sourceType": "module",
     "ecmaFeatures": {
       "experimentalObjectRestSpread": true
@@ -14,7 +14,7 @@
   },
   "rules": {
     "strict": 0,
-    "no-undef": 2,
+    "no-undef": 0,
     "accessor-pairs": 2,
     "comma-dangle": [2, "always-multiline"],
     "consistent-return": 2,
@@ -100,7 +100,9 @@
     "Attachments": true,
     "Boards": true,
     "CardComments": true,
+    "DatePicker" : true,
     "Cards": true,
+    "CustomFields": true,
     "Lists": true,
     "UnsavedEditCollection": true,
     "Users": true,
@@ -119,7 +121,8 @@
     "allowIsBoardAdmin": true,
     "allowIsBoardMember": true,
     "allowIsBoardMemberByCard": true,
-    "allowIsBoardMemberNonComment": true,
+    "allowIsBoardMemberCommentOnly": true,
+    "allowIsBoardMemberNoComments": true,
     "Emoji": true,
     "Checklists": true,
     "Settings": true,
@@ -132,6 +135,7 @@
     "Announcements": true,
     "Swimlanes": true,
     "ChecklistItems": true,
+    "Subtasks": true,
     "Npm": true
   }
 }

+ 17 - 6
.github/ISSUE_TEMPLATE.md

@@ -1,16 +1,27 @@
 ## Issue
 
+Add these issues to elsewhere:
+- Snap: https://github.com/wekan/wekan-snap/issues
+
+Other Wekan issues can be added here.
+
 **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:
-* 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:
 * MongoDB Version:
 * ROOT_URL environment variable http(s)://(subdomain).example.com(/suburl):
 
 **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
 tmp/
 node_modules/
+npm-debug.log
 .vscode/
 .idea/
 .build/*
-packages/kadira-flow-router/
-packages/meteor-useraccounts-core/
 package-lock.json
 **/parts/
 **/stage
 **/prime
 **/*.snap
 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
 
 # Build system
-ecmascript@0.9.0
+ecmascript
 stylus@2.513.13
 standard-minifier-css@1.3.5
 standard-minifier-js@2.2.0
@@ -31,6 +31,9 @@ kenton:accounts-sandstorm
 service-configuration@1.0.11
 useraccounts:unstyled
 useraccounts:flow-routing
+wekan-ldap
+wekan-accounts-cas
+wekan-accounts-oidc
 
 # Utilities
 check@1.2.5
@@ -49,7 +52,6 @@ kadira:dochead
 meteorhacks:picker
 meteorhacks:subs-manager
 mquandalle:autofocus
-mquandalle:moment
 ongoworks:speakingurl
 raix:handlebar-helpers
 tap:i18n
@@ -63,9 +65,7 @@ mousetrap:mousetrap
 mquandalle:jquery-textcomplete
 mquandalle:jquery-ui-drag-drop-sort
 mquandalle:mousetrap-bindglobal
-mquandalle:perfect-scrollbar
 peerlibrary:blaze-components@=0.15.1
-perak:markdown
 templates:tabs
 verron:autosize
 simple:json-routes
@@ -77,10 +77,18 @@ email@1.2.3
 horka:swipebox
 dynamic-import@0.2.0
 staringatlights:fast-render
-staringatlights:flow-router
 
 mixmax:smart-disconnect
 accounts-password@1.5.0
 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
 accounts-base@1.4.0
+accounts-oauth@1.1.15
 accounts-password@1.5.0
 aldeed:collection2@2.10.0
 aldeed:collection2-core@1.2.0
@@ -18,9 +19,7 @@ binary-heap@1.0.10
 blaze@2.3.2
 blaze-tools@1.0.10
 boilerplate-generator@1.3.1
-browser-policy@1.1.0
 browser-policy-common@1.0.11
-browser-policy-content@1.1.0
 browser-policy-framing@1.1.0
 caching-compiler@1.1.9
 caching-html-compiler@1.1.2
@@ -61,7 +60,6 @@ ecmascript-runtime@0.5.0
 ecmascript-runtime-client@0.5.0
 ecmascript-runtime-server@0.5.0
 ejson@1.1.0
-eluck:accounts-lockout@0.9.0
 email@1.2.3
 es5-shim@4.6.15
 fastclick@1.0.13
@@ -83,8 +81,10 @@ launch-screen@1.1.1
 livedata@1.0.18
 localstorage@1.2.0
 logging@1.1.19
+lucasantoniassi:accounts-lockout@1.0.0
 matb33:collection-hooks@0.8.4
 matteodem:easy-search@1.6.4
+mdg:meteor-apm-agent@3.1.2
 mdg:validation-error@0.5.1
 meteor@1.8.2
 meteor-base@1.2.0
@@ -94,6 +94,7 @@ meteorhacks:collection-utils@1.2.0
 meteorhacks:meteorx@1.4.1
 meteorhacks:picker@1.0.3
 meteorhacks:subs-manager@1.6.4
+meteorhacks:unblock@1.1.0
 meteorspark:util@0.2.0
 minifier-css@1.2.16
 minifier-js@2.2.2
@@ -103,6 +104,7 @@ mixmax:smart-disconnect@0.0.4
 mobile-status-bar@1.0.14
 modules@0.11.0
 modules-runtime@0.9.1
+momentjs:moment@2.22.2
 mongo@1.3.1
 mongo-dev-server@1.1.0
 mongo-id@1.0.6
@@ -117,8 +119,11 @@ mquandalle:jquery-ui-drag-drop-sort@0.2.0
 mquandalle:moment@1.0.1
 mquandalle:mousetrap-bindglobal@0.0.1
 mquandalle:perfect-scrollbar@0.6.5_2
+msavin:usercache@1.0.0
 npm-bcrypt@0.9.3
 npm-mongo@2.2.33
+oauth@1.2.1
+oauth2@1.2.0
 observe-sequence@1.0.16
 ongoworks:speakingurl@1.1.0
 ordered-dict@1.0.9
@@ -127,7 +132,6 @@ peerlibrary:base-component@0.16.0
 peerlibrary:blaze-components@0.15.1
 peerlibrary:computed-field@0.7.0
 peerlibrary:reactive-field@0.3.0
-perak:markdown@1.0.5
 promise@0.10.0
 raix:eventemitter@0.1.3
 raix:handlebar-helpers@0.2.5
@@ -139,6 +143,7 @@ reactive-var@1.0.11
 reload@1.1.11
 retry@1.0.9
 routepolicy@1.0.12
+rzymek:fullcalendar@3.8.0
 service-configuration@1.0.11
 session@1.1.7
 sha@1.0.9
@@ -155,7 +160,6 @@ srp@1.0.10
 standard-minifier-css@1.3.5
 standard-minifier-js@2.2.3
 staringatlights:fast-render@2.16.5
-staringatlights:flow-router@2.12.2
 staringatlights:inject-data@2.0.5
 stylus@2.513.13
 tap:i18n@1.8.2
@@ -174,4 +178,11 @@ useraccounts:unstyled@1.14.2
 verron:autosize@3.0.8
 webapp@1.4.0
 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

+ 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
 # Transifex uses a `_` separator, without an option to customize it on one side
 # 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]
 file_filter = i18n/<lang>.i18n.json

文件差异内容过多而无法显示
+ 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)
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # 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 ${SRC_PATH} /home/wekan/app
 
 RUN \
+    set -o xtrace && \
     # Add non-root user wekan
     useradd --user-group --system --home-dir /home/wekan wekan && \
     \
     # OS dependencies
     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
-    #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:
     # https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
@@ -45,11 +126,10 @@ RUN \
     # Also see beginning of wekan/server/authentication.js
     #   import Fiber from "fibers";
     #   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
-    # 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
     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
     cd /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" && \
     chown wekan:wekan /home/wekan/install_meteor.sh && \
     \
@@ -109,40 +192,72 @@ RUN \
     fi; \
     \
     # Get additional packages
-    mkdir -p /home/wekan/app/packages && \
+    #mkdir -p /home/wekan/app/packages && \
     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 && \
     cd /home/wekan/.meteor && \
     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
     cd /home/wekan/app && \
     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 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 && \
+    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 && \
-    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/ && \
     gosu wekan:wekan npm install && \
-    gosu wekan:wekan npm install bcrypt && \
+    #gosu wekan:wekan npm install bcrypt && \
     mv /home/wekan/app_build/bundle /build && \
     \
+    # Put back the original tar
+    mv $(which tar)~ $(which tar) && \
+    \
     # Cleanup
     apt-get remove --purge -y ${BUILD_DEPS} && \
     apt-get autoremove -y && \
+    npm uninstall -g api2html &&\
     rm -R /var/lib/apt/lists/* && \
     rm -R /home/wekan/.meteor && \
     rm -R /home/wekan/app && \
     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
 
-ENV PORT=80
+ENV PORT=8080
 EXPOSE $PORT
+USER wekan
 
 CMD ["node", "/build/main.js"]

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 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
 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)
 [![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)
 [![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)
+[![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
 
@@ -73,9 +99,16 @@ bottom of the page for more info.
 
 [![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
 
@@ -100,4 +133,7 @@ with [Meteor](https://www.meteor.com).
 [open_source]: https://en.wikipedia.org/wiki/Open-source_software
 [free_software]: https://en.wikipedia.org/wiki/Free_software
 [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",
-  "description": "The open-source Trello-like kanban",
+  "description": "The open-source kanban",
   "repository": "https://github.com/wekan/wekan",
   "logo": "https://raw.githubusercontent.com/wekan/wekan/master/meta/icons/wekan-150.png",
   "keywords": ["productivity", "tool", "team", "kanban"],

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

@@ -14,6 +14,9 @@ template(name="boardActivities")
       p.activity-desc
         +memberName(user=user)
 
+        if($eq activityType 'deleteAttachment')
+          | {{{_ 'activity-delete-attach' cardLink}}}.
+
         if($eq activityType 'addAttachment')
           | {{{_ 'activity-attached' attachmentLink cardLink}}}.
 
@@ -31,12 +34,28 @@ template(name="boardActivities")
           .activity-checklist(href="{{ card.absoluteUrl }}")
             +viewer
               = 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')
           | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
           .activity-checklist(href="{{ card.absoluteUrl }}")
             +viewer
               = checklistItem.title
+        if($eq activityType 'removedChecklistItem')
+          | {{{_ 'activity-checklist-item-removed' checklist.title cardLink}}}.
 
         if($eq activityType 'archivedCard')
           | {{{_ 'activity-archived' cardLink}}}.
@@ -53,6 +72,9 @@ template(name="boardActivities")
         if($eq activityType 'createCard')
           | {{{_ 'activity-added' cardLink boardLabel}}}.
 
+        if($eq activityType 'createCustomField')
+          | {{_ 'activity-customfield-created' customField}}.
+
         if($eq activityType 'createList')
           | {{_ 'activity-added' list.title boardLabel}}.
 
@@ -77,6 +99,9 @@ template(name="boardActivities")
           else
             | {{{_ 'activity-added' memberLink cardLink}}}.
 
+        if($eq activityType 'moveCardBoard')
+          | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
+
         if($eq activityType 'moveCard')
           | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
 
@@ -86,6 +111,18 @@ template(name="boardActivities")
         if($eq activityType 'restoredCard')
           | {{{_ '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 user._id member._id)
             | {{{_ 'activity-unjoined' cardLink}}}.
@@ -101,7 +138,7 @@ template(name="cardActivities")
       p.activity-desc
         +memberName(user=user)
         if($eq activityType 'createCard')
-          | {{_ 'activity-added' cardLabel list.title}}.
+          | {{_ 'activity-added' cardLabel listName}}.
         if($eq activityType 'importCard')
           | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
         if($eq activityType 'joinMember')
@@ -116,14 +153,44 @@ template(name="cardActivities")
             | {{{_ 'activity-removed' cardLabel memberLink}}}.
         if($eq activityType 'archivedCard')
           | {{_ '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')
           | {{_ 'activity-sent' cardLabel boardLabel}}.
         if($eq activityType 'moveCard')
           | {{_ 'activity-moved' cardLabel oldList.title list.title}}.
+
+        if($eq activityType 'moveCardBoard')
+          | {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
+
         if($eq activityType 'addAttachment')
           | {{{_ 'activity-attached' attachmentLink cardLabel}}}.
           if attachment.isImage
             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')
           | {{{_ 'activity-checklist-added' cardLabel}}}.
           .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
     sidebar.callFirstWith(null, 'resetNextPeak');
     this.autorun(() => {
-      const mode = this.data().mode;
+      let mode = this.data().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 user = Meteor.user();
       const hideSystem = user ? user.hasHiddenSystemMessages() : false;
-      if (id === null)
+      if (searchId === null)
         return;
 
-      this.subscribe('activities', mode, id, limit, hideSystem, () => {
+      this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
         this.loadNextPageLocked = false;
 
         // 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() {
     return TAPi18n.__('this-board');
   },
@@ -58,6 +72,40 @@ BlazeComponent.extendComponent({
     }, 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() {
     return this.currentData().list().title;
   },
@@ -91,6 +139,13 @@ BlazeComponent.extendComponent({
     }, attachment.name()));
   },
 
+  customField() {
+    const customField = this.currentData().customField();
+    if (!customField)
+      return null;
+    return customField.name;
+  },
+
   events() {
     return [{
       // 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) {
         const input = this.getInput();
         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) {
           CardComments.insert({
             text,
-            boardId: this.currentData().boardId,
-            cardId: this.currentData()._id,
+            boardId,
+            cardId,
           });
           resetCommentInput(input);
           Tracker.flush();

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

@@ -6,9 +6,17 @@ template(name="archivedBoards")
   ul.archived-lists
     each archivedBoards
       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
       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({
   onCreated() {
     this.subscribe('archivedBoards');
@@ -29,6 +23,17 @@ BlazeComponent.extendComponent({
         board.restore();
         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');

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

@@ -20,8 +20,22 @@ template(name="boardBody")
       class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
       if showOverlay.get
         .board-overlay
-      if isViewSwimlanes
+      if currentBoard.isTemplatesBoard
         each currentBoard.swimlanes
           +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 { calculateIndex } = Utils;
+const { calculateIndex, enableClickOnTouch } = Utils;
+const swimlaneWhileSortingHeight = 150;
 
 BlazeComponent.extendComponent({
   onCreated() {
@@ -35,6 +36,37 @@ BlazeComponent.extendComponent({
     this._isDragging = false;
     // Used to set the overlay
     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() {
     const boardComponent = this;
@@ -43,21 +75,64 @@ BlazeComponent.extendComponent({
     $swimlanesDom.sortable({
       tolerance: 'pointer',
       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',
-      items: '.js-swimlane:not(.placeholder)',
+      items: '.swimlane:not(.placeholder)',
       placeholder: 'swimlane placeholder',
       distance: 7,
       start(evt, ui) {
+        const listDom = ui.placeholder.next('.js-swimlane');
+        const parentOffset = ui.item.parent().offset();
+
         ui.placeholder.height(ui.helper.height());
         EscapeActions.executeUpTo('popup-close');
+        listDom.addClass('moving-swimlane');
         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) {
         // To attribute the new index number, we need to get the DOM element
         // 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);
 
         $swimlanesDom.sortable('cancel');
@@ -72,8 +147,35 @@ BlazeComponent.extendComponent({
 
         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() {
       return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
     }
@@ -88,12 +190,20 @@ BlazeComponent.extendComponent({
 
   isViewSwimlanes() {
     const currentUser = Meteor.user();
-    return (currentUser.profile.boardView === 'board-view-swimlanes');
+    if (!currentUser) return false;
+    return ((currentUser.profile || {}).boardView === 'board-view-swimlanes');
   },
 
   isViewLists() {
     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() {
@@ -105,7 +215,6 @@ BlazeComponent.extendComponent({
         .childComponents('addListForm')[0].open();
     }
   },
-
   events() {
     return [{
       // 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');
+
+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
   position: cover
-  overflow-y: hidden;
+  overflow-x: hidden
+  overflow-y: hidden
 
   .board-canvas
     position: cover
     transition: margin .1s
-    overflow-y: auto;
+    overflow-y: auto
 
     &.is-sibling-sidebar-open
       margin-right: 248px
@@ -49,6 +50,6 @@ position()
         display: flex
         flex-direction: column
         margin: 0
-        padding: 0 40px 0px 0
+        padding: 0 0px 0px 0
         overflow-x: hidden
         overflow-y: auto

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

@@ -7,71 +7,69 @@ template(name="boardHeaderBar")
 
   .board-header-btns.left
     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
     if currentBoard
       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 currentUser
@@ -87,15 +85,20 @@ template(name="boardHeaderBar")
         if Filter.isActive
           a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
             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'}}")
         i.fa.fa-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
         a.board-header-btn.js-multiselection-activate(
@@ -108,32 +111,8 @@ template(name="boardHeaderBar")
               i.fa.fa-times-thin
 
       .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")
   ul.pop-over-list
@@ -184,14 +163,6 @@ template(name="boardChangeWatchPopup")
             i.fa.fa-check
           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")
   form
     label
@@ -213,45 +184,21 @@ template(name="createBoard")
     input.primary.wide(type="submit" value="{{_ 'create'}}")
     span.quiet
       | {{_ '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")
   form
     label
       | {{_ 'title'}}
-      input.js-board-name(type="text" value=title autofocus)
+      input.js-board-name(type="text" value=title autofocus dir="auto")
     label
       | {{_ 'description'}}
-      textarea.js-board-desc= description
+      textarea.js-board-desc(dir="auto")= description
     input.primary.wide(type="submit" value="{{_ 'rename'}}")
 
-template(name="archiveBoardPopup")
+template(name="boardCreateRulePopup")
   p {{_ 'close-board-pop'}}
   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({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
+  'click .js-custom-fields'() {
+    Sidebar.setView('customFields');
+    Popup.close();
+  },
   'click .js-open-archives'() {
     Sidebar.setView('archives');
     Popup.close();
@@ -13,8 +17,15 @@ Template.boardMenuPopup.events({
     // confirm that the board was successfully archived.
     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-import-board': Popup.open('chooseBoardSource'),
+  'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
 });
 
 Template.boardMenuPopup.helpers({
@@ -78,12 +89,19 @@ BlazeComponent.extendComponent({
       },
       'click .js-toggle-board-view'() {
         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');
-        } else if (currentUser.profile.boardView === 'board-view-lists') {
+        } else {
           currentUser.setBoardView('board-view-swimlanes');
         }
       },
+      'click .js-toggle-sidebar'() {
+        Sidebar.toggle();
+      },
       'click .js-open-filter-view'() {
         Sidebar.setView('filter');
       },
@@ -95,6 +113,9 @@ BlazeComponent.extendComponent({
       'click .js-open-search-view'() {
         Sidebar.setView('search');
       },
+      'click .js-open-rules-view'() {
+        Modal.openWide('rulesMain');
+      },
       'click .js-multiselection-activate'() {
         const currentCard = Session.get('currentCard');
         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({
   template() {
     return 'createBoard';
@@ -192,16 +191,11 @@ const CreateBoard = BlazeComponent.extendComponent({
       'click .js-import': Popup.open('boardImportBoard'),
       submit: this.onSubmit,
       'click .js-import-board': Popup.open('chooseBoardSource'),
+      'click .js-board-template': Popup.open('searchElement'),
     }];
   },
 }).register('createBoardPopup');
 
-BlazeComponent.extendComponent({
-  template() {
-    return 'chooseBoardSource';
-  },
-}).register('chooseBoardSourcePopup');
-
 (class HeaderBarCreateBoard extends CreateBoard {
   onSubmit(evt) {
     super.onSubmit(evt);
@@ -251,50 +245,3 @@ BlazeComponent.extendComponent({
     }];
   },
 }).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
   padding: 5px
   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")
   .wrapper
     ul.board-list.clearfix
+      li.js-add-board
+        a.board-list-item.label {{_ 'add-board'}}
       each boards
         li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
           if isInvited
@@ -20,15 +22,15 @@ template(name="boardList")
                 i.fa.js-star-board(
                   class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
                   title="{{_ 'star-board-title'}}")
-
+                p.board-list-item-desc= description
                 if hasSpentTimeCards
                   i.fa.js-has-spenttime-cards(
                     class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
                     title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
+                i.fa.js-clone-board(
+                    class="fa-clone"
+                    title="{{_ 'duplicate-board'}}")
 
-                p.board-list-item-desc= description
-      li.js-add-board
-        a.board-list-item.label {{_ 'add-board'}}
 
 
 template(name="boardListHeaderBar")
@@ -37,3 +39,6 @@ template(name="boardListHeaderBar")
     a.board-header-btn.js-open-archived-board
       i.fa.fa-archive
       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();
 
+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({
   onCreated() {
     Meteor.subscribe('setting');
@@ -9,11 +24,9 @@ BlazeComponent.extendComponent({
     return Boards.find({
       archived: false,
       'members.userId': Meteor.userId(),
-    }, {
-      sort: ['title'],
-    });
+      type: 'board',
+    }, { sort: ['title'] });
   },
-
   isStarred() {
     const user = Meteor.user();
     return user && user.hasStarred(this.currentData()._id);
@@ -42,6 +55,21 @@ BlazeComponent.extendComponent({
         Meteor.user().toggleBoardStar(boardId);
         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'() {
         const boardId = this.currentData()._id;
         Meteor.user().removeInvite(boardId);

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

@@ -94,13 +94,27 @@ $spaceBetweenTiles = 16px
   .is-star-active
     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
     &:hover
       .fa-star,
+      .fa-clone,
       .fa-star-o
         color: white
 
     .fa-star,
+    .fa-clone,
     .fa-star-o
       color: white
       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;
     FS.Utility.eachFile(evt, (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();
 
       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")
   if canModifyCard
     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
-const EditCardDate = BlazeComponent.extendComponent({
+BlazeComponent.extendComponent({
   template() {
     return 'editCardDate';
   },
@@ -93,10 +93,10 @@ Template.dateBadge.helpers({
 });
 
 // editCardReceivedDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   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) {
@@ -104,22 +104,22 @@ Template.dateBadge.helpers({
   }
 
   _deleteDate() {
-    this.card.unsetReceived();
+    this.card.setReceived(null);
   }
 }).register('editCardReceivedDatePopup');
 
 
 // editCardStartDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   onCreated() {
     super.onCreated();
-    this.data().startAt && this.date.set(moment(this.data().startAt));
+    this.data().getStart() && this.date.set(moment(this.data().getStart()));
   }
 
   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() {
-    this.card.unsetStart();
+    this.card.setStart(null);
   }
 }).register('editCardStartDatePopup');
 
 // editCardDueDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   onCreated() {
     super.onCreated();
-    this.data().dueAt && this.date.set(moment(this.data().dueAt));
+    this.data().getDue() && this.date.set(moment(this.data().getDue()));
   }
 
   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() {
-    this.card.unsetDue();
+    this.card.setDue(null);
   }
 }).register('editCardDueDatePopup');
 
 // editCardEndDatePopup
-(class extends EditCardDate {
+(class extends DatePicker {
   onCreated() {
     super.onCreated();
-    this.data().endAt && this.date.set(moment(this.data().endAt));
+    this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
   }
 
   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() {
-    this.card.unsetEnd();
+    this.card.setEnd(null);
   }
 }).register('editCardEndDatePopup');
 
@@ -213,16 +213,23 @@ class CardReceivedDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().receivedAt));
+      self.date.set(moment(self.data().getReceived()));
     });
   }
 
   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';
-    }
     return classes;
   }
 
@@ -243,16 +250,24 @@ class CardStartDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().startAt));
+      self.date.set(moment(self.data().getStart()));
     });
   }
 
   classes() {
     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';
-    }
     return classes;
   }
 
@@ -273,17 +288,26 @@ class CardDueDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().dueAt));
+      self.date.set(moment(self.data().getDue()));
     });
   }
 
   classes() {
     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';
-    else if (this.now.get().diff(this.date.get(), 'minute') >= 0)
+    else if (now.diff(theDate, 'minute') >= 0)
       classes += 'due';
-    else if (this.now.get().diff(this.date.get(), 'days') >= -1)
+    else if (now.diff(theDate, 'days') >= -1)
       classes += 'almost-due';
     return classes;
   }
@@ -305,17 +329,19 @@ class CardEndDate extends CardDate {
     super.onCreated();
     const self = this;
     self.autorun(() => {
-      self.date.set(moment(self.data().endAt));
+      self.date.set(moment(self.data().getEnd()));
     });
   }
 
   classes() {
     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';
-    else if (this.data.dueAt.diff(this.date.get(), 'days') >= 0)
+    else if (theDate.diff(dueAt, 'days') >= 0)
       classes += 'due';
-    else if (this.data.dueAt.diff(this.date.get(), 'days') >= -2)
+    else if (theDate.diff(dueAt, 'days') >= -2)
       classes += 'almost-due';
     return classes;
   }
@@ -355,4 +381,3 @@ CardEndDate.register('cardEndDate');
     return this.date.get().format('l');
   }
 }).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
   display: block
   border-radius: 4px
@@ -62,12 +43,12 @@
   &.start-date
     time
       &::before
-        content: "\f08b"  // symbol: fa-sign-out
+        content: "\f251"  // symbol: fa-hourglass-start
 
   &.received-date
     time
       &::before
-        content: "\f251"  // symbol: fa-hourglass-start
+        content: "\f08b"  // symbol: fa-sign-out
 
   time
     &::before

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

@@ -1,6 +1,6 @@
 template(name="cardDetails")
   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")
         +editCardTitleForm
       else
@@ -10,46 +10,63 @@ template(name="cardDetails")
         h2.card-details-title.js-card-title(
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
             +viewer
-              = title
+              = getTitle
               if isWatching
                 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-item.card-details-item-received
         h3.card-details-item-title {{_ 'card-received'}}
-        if receivedAt
+        if getReceived
           +cardReceivedDate
         else
-          a.js-received-date {{_ 'add'}}
+          if canModifyCard
+            a.js-received-date {{_ 'add'}}
 
       .card-details-item.card-details-item-start
         h3.card-details-item-title {{_ 'card-start'}}
-        if startAt
+        if getStart
           +cardStartDate
         else
-          a.js-start-date {{_ 'add'}}
+          if canModifyCard
+            a.js-start-date {{_ 'add'}}
 
       .card-details-item.card-details-item-due
         h3.card-details-item-title {{_ 'card-due'}}
-        if dueAt
+        if getDue
           +cardDueDate
         else
-          a.js-due-date {{_ 'add'}}
+          if canModifyCard
+            a.js-due-date {{_ 'add'}}
 
       .card-details-item.card-details-item-end
         h3.card-details-item-title {{_ 'card-end'}}
-        if endAt
+        if getEnd
           +cardEndDate
         else
-          a.js-end-date {{_ 'add'}}
+          if canModifyCard
+            a.js-end-date {{_ 'add'}}
 
     .card-details-items
       .card-details-item.card-details-item-members
         h3.card-details-item-title {{_ 'members'}}
-        each members
+        each getMembers
           +userAvatar(userId=this cardId=../_id)
           | {{! XXX Hack to hide syntaxic coloration /// }}
         if canModifyCard
@@ -66,9 +83,16 @@ template(name="cardDetails")
             i.fa.fa-plus
 
     .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
-          if isOvertime
+          if getIsOvertime
             h3.card-details-item-title {{_ 'overtime-hours'}}
           else
             h3.card-details-item-title {{_ 'spent-time-hours'}}
@@ -79,15 +103,15 @@ template(name="cardDetails")
       h3.card-details-item-title {{_ 'description'}}
       +inlinedCardDescription(classNames="card-description js-card-description")
         +editor(autofocus=true)
-          | {{getUnsavedValue 'cardDescription' _id description}}
+          | {{getUnsavedValue 'cardDescription' _id getDescription}}
         .edit-controls.clearfix
           button.primary(type="submit") {{_ 'save'}}
           a.fa.fa-times-thin.js-close-inlined-form
       else
         a.js-open-inlined-form
-          if description
+          if getDescription
             +viewer
-              = description
+              = getDescription
           else
             | {{_ 'edit'}}
         if (hasUnsavedValue 'cardDescription' _id)
@@ -96,14 +120,51 @@ template(name="cardDetails")
             a.js-open-inlined-form {{_ 'view-it'}}
             = ' - '
             a.js-close-inlined-form {{_ 'discard'}}
-    else if description
+    else if getDescription
       h3.card-details-item-title {{_ 'description'}}
       +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
     +checklists(cardId = _id)
 
+    if currentBoard.allowsSubtasks
+      hr
+      +subtasks(cardId = _id)
+
     hr
     h3
       i.fa.fa-paperclip
@@ -112,42 +173,64 @@ template(name="cardDetails")
     +attachmentsGalery
 
     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
-      +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")
-  textarea.js-edit-card-title(rows='1' autofocus)
-    = title
+  textarea.js-edit-card-title(rows='1' autofocus dir="auto")
+    = getTitle
   .edit-controls.clearfix
     button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
     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")
   ul.pop-over-list
     li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
   if canModifyCard
     hr
     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-set-card-color {{_ 'setCardColorPopup-title'}}
     hr
     ul.pop-over-list
       li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
@@ -167,10 +250,9 @@ template(name="moveCardPopup")
 template(name="copyCardPopup")
   label(for='copy-card-title') {{_ 'title'}}:
   textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
-    = title
+    = getTitle
   +boardsAndLists
 
-
 template(name="copyChecklistToManyCardsPopup")
   label(for='copy-checklist-cards-title') {{_ 'copyChecklistToManyCardsPopup-instructions'}}:
   textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
@@ -179,7 +261,7 @@ template(name="copyChecklistToManyCardsPopup")
 
 template(name="boardsAndLists")
   label {{_ 'boards'}}:
-  select.js-select-boards
+  select.js-select-boards(autofocus)
     each boards
       if $eq _id currentBoard._id
         option(value="{{_id}}" selected) {{_ 'current'}}
@@ -217,14 +299,49 @@ template(name="cardMorePopup")
       span {{_ 'link-card'}}
       = ' '
       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'}}
     span.clearfix
     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'}}
     span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
     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")
   p {{_ "card-delete-pop"}}
   unless archived

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

@@ -1,5 +1,10 @@
 const subManager = new SubsManager();
-const { calculateIndexData } = Utils;
+const { calculateIndexData, enableClickOnTouch } = Utils;
+
+let cardColors;
+Meteor.startup(() => {
+  cardColors = Cards.simpleSchema()._schema.color.allowedValues;
+});
 
 BlazeComponent.extendComponent({
   mixins() {
@@ -20,9 +25,14 @@ BlazeComponent.extendComponent({
   },
 
   onCreated() {
+    this.currentBoard = Boards.findOne(Session.get('currentBoard'));
     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();
 
     Meteor.subscribe('unsaved-edits');
@@ -44,7 +54,8 @@ BlazeComponent.extendComponent({
   scrollParentContainer() {
     const cardPanelWidth = 510;
     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 $cardContainer = bodyBoardComponent.$('.js-swimlanes');
     const cardContainerScroll = $cardContainer.scrollLeft();
@@ -63,10 +74,52 @@ BlazeComponent.extendComponent({
     if (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() {
-    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');
 
     $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() {
       return Meteor.user() && Meteor.user().isBoardMember();
     }
@@ -111,11 +205,17 @@ BlazeComponent.extendComponent({
       if ($checklistsDom.data('sortable')) {
         $checklistsDom.sortable('option', 'disabled', !userIsMember());
       }
+      if ($subtasksDom.data('sortable')) {
+        $subtasksDom.sortable('option', 'disabled', !userIsMember());
+      }
     });
   },
 
   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() {
@@ -146,6 +246,20 @@ BlazeComponent.extendComponent({
           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-add-members': Popup.open('cardMembers'),
       'click .js-add-labels': Popup.open('cardLabels'),
@@ -154,8 +268,11 @@ BlazeComponent.extendComponent({
       'click .js-due-date': Popup.open('editCardDueDate'),
       'click .js-end-date': Popup.open('editCardEndDate'),
       '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'() {
         Meteor.call('toggleSystemMessages');
@@ -180,7 +297,7 @@ BlazeComponent.extendComponent({
   close(isReset = false) {
     if (this.isOpen.get() && !isReset) {
       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());
       }
     }
@@ -215,6 +332,7 @@ Template.cardDetailsActionsPopup.events({
   'click .js-members': Popup.open('cardMembers'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
+  'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
   'click .js-due-date': Popup.open('editCardDueDate'),
@@ -223,6 +341,7 @@ Template.cardDetailsActionsPopup.events({
   'click .js-move-card': Popup.open('moveCard'),
   'click .js-copy-card': Popup.open('copyCard'),
   'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
+  'click .js-set-card-color': Popup.open('setCardColor'),
   'click .js-move-card-to-top' (evt) {
     evt.preventDefault();
     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({
   'click .js-done' () {
     // XXX We should *not* get the currentCard from the global state, but
     // instead from a “component” state.
     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 newListId = lSelect.options[lSelect.selectedIndex].value;
+    const listId = lSelect.options[lSelect.selectedIndex].value;
     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();
   },
 });
-
 BlazeComponent.extendComponent({
   onCreated() {
     subManager.subscribe('board', Session.get('currentBoard'));
@@ -286,6 +432,7 @@ BlazeComponent.extendComponent({
     const boards = Boards.find({
       archived: false,
       'members.userId': Meteor.userId(),
+      _id: {$ne: Meteor.user().getTemplatesBoardId()},
     }, {
       sort: ['title'],
     });
@@ -315,14 +462,12 @@ BlazeComponent.extendComponent({
 Template.copyCardPopup.events({
   'click .js-done'() {
     const card = Cards.findOne(Session.get('currentCard'));
-    const oldId = card._id;
-    card._id = null;
     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];
-    card.swimlaneId = slSelect.options[slSelect.selectedIndex].value;
+    const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
     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 title = textarea.val().trim();
     // insert new card to the bottom of new list
@@ -331,39 +476,13 @@ Template.copyCardPopup.events({
     if (title) {
       card.title = title;
       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
       // 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);
 
-      // 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();
     }
   },
@@ -400,30 +519,23 @@ Template.copyChecklistToManyCardsPopup.events({
         Filter.addException(_id);
 
         // 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() {
           '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
-        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();
@@ -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 {
-      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
 EscapeActions.register('detailsPane',

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

@@ -1,11 +1,12 @@
 @import 'nib'
 
 .card-details
-  padding: 0 20px
+  padding: 0
   flex-shrink: 0
-  flex-basis: 470px
+  flex-basis: 510px
   will-change: flex-basis
-  overflow: hidden
+  overflow-y: scroll
+  overflow-x: hidden
   background: darken(white, 3%)
   border-radius: bottom 3px
   z-index: 20 !important
@@ -13,8 +14,16 @@
   box-shadow: 0 0 7px 0 darken(white, 30%)
   transition: flex-basis 0.1s
 
+  .mCustomScrollBox
+    padding-left: 0
+
+  .ps-scrollbar-y-rail
+    pointer-event: all
+    position: absolute;
+
   .card-details-canvas
     width: 470px
+    padding-left: 20px;
 
   .card-details-header
     margin: 0 -20px 5px
@@ -46,6 +55,12 @@
       margin: 7px 0 0
       padding: 0
 
+    .linked-card-location
+      font-style: italic
+      font-size: 1em
+      margin-bottom: 0
+      & p
+        margin-bottom: 0
 
     form.inlined-form
       margin-top: 5px
@@ -69,6 +84,7 @@
 
   .card-details-items
     display: flex
+    flex-wrap: wrap
     margin: 15px 0
 
     .card-details-item
@@ -80,9 +96,11 @@
       &.card-details-item-received,
       &.card-details-item-start,
       &.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
     font-size: 16px
@@ -115,10 +133,92 @@ input[type="submit"].attachment-add-link-submit
 
     .card-details-canvas
       width: 100%
-
+      padding-left: 0px;
+ 
     .card-details-header
       .close-card-details
         margin-right: 0px
 
       .card-details-menu
         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
       .fields
         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'}}
         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
         .warning {{_ error.get}}
@@ -15,8 +15,8 @@ template(name="editCardSpentTime")
 
 template(name="timeBadge")
   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}}
   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}}

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

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

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

@@ -27,7 +27,6 @@ template(name="checklistDetail")
         if canModifyCard
           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
           h2.title.js-open-inlined-form.is-editable
             +viewer
@@ -57,7 +56,7 @@ template(name="addChecklistItemForm")
     a.fa.fa-times-thin.js-close-inlined-form
 
 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'
       = item.title
     else
@@ -75,7 +74,7 @@ template(name="checklistItems")
       +inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
         +editChecklistItemForm(type = 'item' item = item checklist = checklist)
       else
-        +itemDetail(item = item checklist = checklist)
+        +checklistItemDetail(item = item checklist = checklist)
     if canModifyCard
       +inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
         +addChecklistItemForm
@@ -84,7 +83,7 @@ template(name="checklistItems")
           i.fa.fa-plus
           | {{_ 'add-checklist-item'}}...
 
-template(name='itemDetail')
+template(name='checklistItemDetail')
   .js-checklist-item.checklist-item
     if canModifyCard
       .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) {
   items.sortable({
@@ -36,6 +36,9 @@ function initSorting(items) {
       checklistItem.move(checklistId, sortIndex.base);
     },
   });
+
+  // ugly touch event hotfix
+  enableClickOnTouch('.js-checklist-item:not(.placeholder)');
 }
 
 BlazeComponent.extendComponent({
@@ -71,8 +74,10 @@ BlazeComponent.extendComponent({
     event.preventDefault();
     const textarea = this.find('textarea.js-add-checklist-item');
     const title = textarea.value.trim();
-    const cardId = this.currentData().cardId;
+    let cardId = this.currentData().cardId;
     const card = Cards.findOne(cardId);
+    if (card.isLinked())
+      cardId = card.linkedId;
 
     if (title) {
       Checklists.insert({
@@ -204,7 +209,7 @@ Template.checklistDeleteDialog.onDestroyed(() => {
   $cardDetails.animate( { scrollTop: this.scrollState.position });
 });
 
-Template.itemDetail.helpers({
+Template.checklistItemDetail.helpers({
   canModifyCard() {
     return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
   },
@@ -223,4 +228,4 @@ BlazeComponent.extendComponent({
       '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
 .card-label
   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
   font-weight: 700
   font-size: 13px
@@ -48,9 +48,11 @@
 
 .card-label-yellow
   background-color: #fad900
+  color: #000000 //Black text for better visibility
 
 .card-label-orange
   background-color: #ff9f19
+  color: #000000 //Black text for better visibility
 
 .card-label-red
   background-color: #eb4646
@@ -63,6 +65,7 @@
 
 .card-label-pink
   background-color: #ff78cb
+  color: #000000 //Black text for better visibility
 
 .card-label-sky
   background-color: #00c2e0
@@ -72,6 +75,55 @@
 
 .card-label-lime
   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,
 .create-label

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

@@ -1,5 +1,8 @@
 template(name="minicard")
-  .minicard
+  .minicard(
+    class="{{#if isLinkedCard}}linked-card{{/if}}"
+    class="{{#if isLinkedBoard}}linked-board{{/if}}"
+    class="minicard-{{colorClass}}")
     if cover
       .minicard-cover(style="background-image: url('{{cover.url}}');")
     if labels
@@ -7,30 +10,72 @@ template(name="minicard")
         each labels
           .minicard-label(class="card-label-{{color}}" title="{{name}}")
     .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
-        = title
+        = getTitle
+      if $eq 'subtext-with-full-path' currentBoard.presentParentTask
+        .parent-subtext
+          | {{ parentString ' > ' }}
+      if $eq 'subtext-with-parent' currentBoard.presentParentTask
+        .parent-subtext
+          | {{ parentCardName }}
+
     .dates
-      if startAt
+      if getReceived
+        unless getStart
+          unless getDue
+            unless getEnd
+              .date
+                +minicardReceivedDate
+      if getStart
         .date
           +minicardStartDate
-      if dueAt
+      if getDue
         .date
           +minicardDueDate
-      if spentTime
+      if getSpentTime
         .date
           +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
-        each members
+        each getMembers
           +userAvatar(userId=this)
+
     .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
       if attachments.count
         .badge

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

@@ -6,4 +6,15 @@ BlazeComponent.extendComponent({
   template() {
     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');

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

@@ -9,7 +9,7 @@
 
   &.placeholder
     background: darken(white, 20%)
-    border-radius: 2px
+    border-radius: 9px
 
   &.ui-sortable-helper
     cursor: grabbing
@@ -44,6 +44,16 @@
   transition: transform 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 &
     transform: translateX(11px)
     border-bottom-right-radius: 0
@@ -77,9 +87,31 @@
       height: @width
       border-radius: 2px
       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
     p:last-child
       margin-bottom: 0
+    .viewer
+      display: inline-block
   .dates
     display: flex;
     flex-direction: row;
@@ -155,6 +187,13 @@
       margin-bottom: 20px
       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)
   .minicard
     .is-selected &
@@ -163,3 +202,86 @@
       border-top-right-radius: 0
       z-index: 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'
 
+select,
 textarea,
 input:not([type=file]),
 button
   box-sizing: border-box
-  -webkit-appearance: none
   background-color: #ebebeb
   border: 1px solid #ccc
   border-radius: 3px
@@ -85,6 +85,9 @@ select
   width: 256px
   margin-bottom: 8px
 
+  &.inline
+  	width: 100%
+
 option[disabled]
   color: #8c8c8c
 
@@ -222,9 +225,12 @@ textarea
 
 .edit-controls,
 .add-controls
+  display: flex
+  align-items: baseline
   margin-top: 0
 
   button[type=submit]
+  input[type=button]
     float: left
     height: 32px
     margin-top: -2px

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

@@ -12,11 +12,11 @@ template(name="import")
 
 template(name="importTextarea")
   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)
       | {{jsonText}}
     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'}}
     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({
   // Proxy
@@ -26,6 +26,13 @@ BlazeComponent.extendComponent({
 
     const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
     const $cards = this.$('.js-minicards');
+
+    if(window.matchMedia('(max-width: 1199px)').matches) {
+      $( '.js-minicards' ).sortable({
+        handle: '.handle',
+      });
+    }
+
     $cards.sortable({
       connectWith: '.js-minicards:not(.js-list-full)',
       tolerance: 'pointer',
@@ -47,6 +54,7 @@ BlazeComponent.extendComponent({
       items: itemsSelector,
       placeholder: 'minicard-wrapper placeholder',
       start(evt, ui) {
+        ui.helper.css('z-index', 1000);
         ui.placeholder.height(ui.helper.height());
         EscapeActions.executeUpTo('popup-close');
         boardComponent.setIsDragging(true);
@@ -59,7 +67,13 @@ BlazeComponent.extendComponent({
         const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
         const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
         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
         // to its new position, which disrupts Blaze reactive updates mechanism
@@ -72,17 +86,20 @@ BlazeComponent.extendComponent({
 
         if (MultiSelection.isActive()) {
           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 {
           const cardDomElement = ui.item.get(0);
           const card = Blaze.getData(cardDomElement);
-          card.move(swimlaneId, listId, sortIndex.base);
+          card.move(currentBoard._id, swimlaneId, listId, sortIndex.base);
         }
         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
     this.autorun(() => {
       $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.
   background: darken(white, 13%)
   border-left: 1px solid darken(white, 20%)
-  border-bottom: 1px solid #CCC
   padding: 0
   float: left
 
@@ -44,11 +43,23 @@
       background: white
       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
   flex: 0 0 auto
-  margin: 20px 12px 4px
+  padding: 20px 12px 4px
   position: relative
   min-height: 20px
+  background-color: #e4e4e4;
+  border-bottom: 6px solid #e4e4e4;
+
 
   &.ui-sortable-handle
     cursor: grab
@@ -68,16 +79,18 @@
     text-overflow: ellipsis
     word-wrap: break-word
 
+
   .list-header-watch-icon
     padding-left: 10px
     color: #a6a6a6
 
+
   .list-header-menu
     position: absolute
-    padding: 7px
+    padding: 27px 19px
     margin-top: 1px
-    top: -@padding
-    right: -@padding
+    top: -7px
+    right: -7px
 
   .list-header-plus-icon
     color: #a6a6a6
@@ -143,9 +156,12 @@
     float: left
 
 @media screen and (max-width: 800px)
+  .list-header-menu
+    margin-right: 30px
+
   .mini-list
     flex: 0 0 60px
-    height: 60px
+    height: auto
     width: 100%
     border-left: 0px
     border-bottom: 1px solid darken(white, 20%)
@@ -154,6 +170,8 @@
     display: block
     width: 100%
     border-left: 0px
+    &:first-child
+      margin-left: 0px
 
     &.ui-sortable-helper
       flex: 0 0 60px
@@ -172,8 +190,16 @@
       border-left: 0px
       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
       display: inline
       padding: 7px
@@ -185,5 +211,100 @@
     .list-header-menu-icon
       position: absolute
       padding: 7px
-      top: -@padding
+      top: 50%
+      transform: translateY(-50%)
       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
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
-      each (cards (idOrNull ../../_id))
+      each (cardsWithLimit (idOrNull ../../_id))
         a.minicard-wrapper.js-minicard(href=absoluteUrl
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
@@ -12,6 +12,9 @@ template(name="listBody")
             .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
               class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           +minicard(this)
+      if (showSpinner (idOrNull ../../_id))
+        +spinnerList
+
       if canSeeAddCard
         +inlinedForm(autoclose=false position="bottom")
           +addCardForm(listId=_id position="bottom")
@@ -20,6 +23,16 @@ template(name="listBody")
             i.fa.fa-plus
             | {{_ '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")
   .minicard.minicard-composer.js-composer
     if getLabels
@@ -34,8 +47,84 @@ template(name="addCardForm")
 
   .add-controls.clearfix
     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")
   .minicard-label(class="card-label-{{colorName}}" title=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({
+  onCreated() {
+    // for infinite scrolling
+    this.cardlimit = new ReactiveVar(InfiniteScrollIter);
+  },
+
   mixins() {
     return [Mixins.PerfectScrollbar];
   },
@@ -35,25 +43,59 @@ BlazeComponent.extendComponent({
 
     const members = formComponent.members.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 = '';
-    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 (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({
         title,
         members,
         labelIds,
+        customFields,
         listId: this.data()._id,
-        boardId: this.data().board()._id,
+        boardId: board._id,
         sort: sortIndex,
         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
       // the list of exceptions -- cards that are not filtered. Otherwise the
       // card will disappear instantly.
@@ -85,9 +127,9 @@ BlazeComponent.extendComponent({
       const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
       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)) {
       evt.stopImmediatePropagation();
       evt.preventDefault();
@@ -107,11 +149,31 @@ BlazeComponent.extendComponent({
 
   idOrNull(swimlaneId) {
     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 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() {
     return !this.reachedWipLimit() && Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
   },
@@ -146,11 +208,21 @@ BlazeComponent.extendComponent({
   onCreated() {
     this.labels = 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() {
     this.labels.set([]);
     this.members.set([]);
+    this.customFields.set([]);
   },
 
   getLabels() {
@@ -162,7 +234,7 @@ BlazeComponent.extendComponent({
 
   pressKey(evt) {
     // Pressing Enter should submit the card
-    if (evt.keyCode === 13) {
+    if (evt.keyCode === 13 && !evt.shiftKey) {
       evt.preventDefault();
       const $form = $(evt.currentTarget).closest('form');
       // XXX For some reason $form.submit() does not work (it's probably a bug
@@ -171,8 +243,8 @@ BlazeComponent.extendComponent({
       // work.
       $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) {
       evt.preventDefault();
       const isReverse = evt.shiftKey;
@@ -193,6 +265,9 @@ BlazeComponent.extendComponent({
   events() {
     return [{
       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'));
           callback($.map(currentBoard.labels, (label) => {
             if (label.name.indexOf(term) > -1 ||
-                label.color.indexOf(term) > -1) {
+              label.color.indexOf(term) > -1) {
               return label;
             }
             return null;
@@ -264,3 +339,326 @@ BlazeComponent.extendComponent({
     });
   },
 }).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")
-  .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
       +editListTitleForm
     else
@@ -15,9 +17,8 @@ template(name="listHeader")
          |/#{wipLimit.value})
 
         if showCardsCountForList cards.count
-          = cards.count
-          span
-            |  {{_ 'cards-count'}}
+          |&nbsp;
+          p.quiet.small {{cardsCount}} {{_ 'cards-count'}}
       if isMiniScreen
         if currentList
           if isWatching
@@ -50,6 +51,9 @@ template(name="listActionPopup")
     li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
   unless currentUser.isCommentOnly
     hr
+    ul.pop-over-list
+      li: a.js-set-color-list {{_ 'set-color-list'}}
+    hr
     ul.pop-over-list
       if cards.count
         li: a.js-select-cards {{_ 'list-select-cards'}}
@@ -112,3 +116,13 @@ template(name="wipLimitErrorPopup")
     p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
     p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
     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({
   canSeeAddCard() {
     const list = Template.currentData();
@@ -22,6 +27,16 @@ BlazeComponent.extendComponent({
     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() {
     const list = Template.currentData();
     return list.getWipLimit('enabled') && list.getWipLimit('value') <= list.cards().count();
@@ -62,6 +77,7 @@ Template.listActionPopup.helpers({
 
 Template.listActionPopup.events({
   'click .js-list-subscribe' () {},
+  'click .js-set-color-list': Popup.open('setListColor'),
   'click .js-select-cards' () {
     const cardIds = this.allCards().map((card) => card._id);
     MultiSelection.add(cardIds);
@@ -144,3 +160,34 @@ Template.listMorePopup.events({
     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")
   textarea.editor(
+    dir="auto"
     class="{{class}}"
     id=id
     autofocus=autofocus
@@ -7,7 +8,7 @@ template(name="editor")
     +Template.contentBlock
 
 template(name="viewer")
-  .viewer
+  .viewer(dir="auto")
     +mentions
       +markdown
         {{> UI.contentBlock }}

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

@@ -36,13 +36,18 @@ import sanitizeXss from 'xss';
 const at = HTML.CharRef({html: '&commat;', str: '@'});
 Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
   const view = this;
+  let content = Blaze.toHTML(view.templateContentBlock);
   const currentBoard = Boards.findOne(Session.get('currentBoard'));
+  if (!currentBoard)
+    return HTML.Raw(sanitizeXss(content));
   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;
   });
   const mentionRegex = /\B@([\w.]*)/gi;
-  let content = Blaze.toHTML(view.templateContentBlock);
 
   let currentMention;
   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
     Reddit "subreddit" bar.
     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
-            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)
     //-
@@ -46,17 +45,16 @@ template(name="header")
     #header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
       +Template.dynamic(template=headerBar)
 
-      unless hideLogo
+      //unless hideLogo
+
         //-
           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
           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
     +offlineWarning
@@ -66,7 +64,8 @@ template(name="header")
       .announcement
         p
           i.fa.fa-bullhorn
-          | #{announcement}
+          +viewer
+            | #{announcement}
           i.fa.fa-times-circle.js-close-announcement
 
 template(name="offlineWarning")

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

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

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

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

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

@@ -1,7 +1,6 @@
 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")
   //- 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
@@ -9,34 +8,47 @@ head
     packages.
   link(rel="shortcut 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")
 
 template(name="userFormsLayout")
   section.auth-layout
-    h1.at-form-landing-logo
-      img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan")
     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")
   +header
   #content
+    | {{{afterBodyStart}}}
     +Template.dynamic(template=content)
+    | {{{beforeBodyEnd}}}
   if (Modal.isOpen)
     #modal
       .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")
   +message(label='page-not-found')
@@ -47,3 +59,14 @@ template(name="message")
     unless currentUser
       with(pathFor route='atSignIn')
         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;
 };
 
+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(() => {
+  AccountsTemplates.state.form.keys = new Proxy(AccountsTemplates.state.form.keys, validator);
+
   const i18nTag = navigator.language;
   if (i18nTag) {
     T9n.setLanguage(i18nTagToT9n(i18nTag));
@@ -15,6 +44,22 @@ Template.userFormsLayout.onRendered(() => {
 });
 
 Template.userFormsLayout.helpers({
+  currentSetting() {
+    return Template.instance().currentSetting.get();
+  },
+
+  isLoading() {
+    return Template.instance().isLoading.get();
+  },
+
+  afterBodyStart() {
+    return currentSetting.customHTMLafterBodyStart;
+  },
+
+  beforeBodyEnd() {
+    return currentSetting.customHTMLbeforeBodyEnd;
+  },
+
   languages() {
     return _.map(TAPi18n.getLanguages(), (lang, code) => {
       const tag = code;
@@ -47,6 +92,15 @@ Template.userFormsLayout.events({
     T9n.setLanguage(i18nTagToT9n(i18nTag));
     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({
@@ -54,3 +108,64 @@ Template.defaultLayout.events({
     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
       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
   font-size: 22px
   line-height: 1.2em
@@ -273,7 +290,7 @@ kbd
 // Implement a thiner close icon as suggested in
 // https://github.com/FortAwesome/Font-Awesome/issues/1540#issuecomment-68689950
 .fa.fa-times-thin:before
-  content: '\00d7';
+  content: '\00d7'
 
 .fa.fa-globe.colorful, .fa.fa-bell.colorful
   color: #4caf50
@@ -368,8 +385,8 @@ a
 
 @media screen and (max-width: 800px)
   #content
-    margin: 1px 0px 49px 0px
-    height: calc(100% - 96px)
+    margin: 1px 0px 0px 0px
+    height: calc(100% - 0px)
 
     > .wrapper
       margin-top: 0px
@@ -382,3 +399,103 @@ a
   height: 37px
   margin: 8px 10px 0 0
   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
     height: 72px
 
+  form a span
+    padding: 0 0.5rem
+
   .header
     height: 36px
     position: relative

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

@@ -1,12 +1,16 @@
+const { isTouchDevice } = Utils;
+
 Mixins.PerfectScrollbar = BlazeComponent.extendComponent({
   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));
+    }
   },
 });

二进制
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
       tbody
         tr
-          th {{_ 'Wekan_version'}}
+          th Wekan {{_ 'info'}}
           td {{statistics.version}}
         tr
           th {{_ 'Node_version'}}

部分文件因为文件数量过多而无法显示