Forráskód Böngészése

Merge branch 'master' of https://github.com/wekan/wekan

Romulus Tsai 蔡仲明 5 éve
szülő
commit
cfcc73724f
100 módosított fájl, 4190 hozzáadás és 1007 törlés
  1. 5 0
      .babelrc
  2. 3 3
      .devcontainer/Dockerfile
  3. 1 0
      .eslintignore
  4. 2 0
      .eslintrc.json
  5. 257 0
      .future-snap/broken-snapcraft.yaml
  6. 155 0
      .future-snap/snapcraft.yaml
  7. 2 0
      .github/ISSUE_TEMPLATE.md
  8. 10 0
      .gitpod.Dockerfile
  9. 4 0
      .gitpod.yml
  10. 11 10
      .meteor/packages
  11. 1 1
      .meteor/release
  12. 38 38
      .meteor/versions
  13. 1 0
      .prettierignore
  14. 2 2
      .travis.yml
  15. 785 0
      CHANGELOG.md
  16. 12 4
      Dockerfile
  17. 77 0
      Dockerfile.arm64v8
  18. 4 2
      README.md
  19. 1 1
      Stackerfile.yml
  20. 6 0
      client/00-startup.js
  21. 157 190
      client/components/activities/activities.jade
  22. 74 41
      client/components/activities/activities.js
  23. 1 1
      client/components/activities/activities.styl
  24. 20 0
      client/components/activities/comments.styl
  25. 1 1
      client/components/boards/boardArchive.js
  26. 4 10
      client/components/boards/boardBody.js
  27. 0 14
      client/components/boards/boardHeader.jade
  28. 1 20
      client/components/boards/boardHeader.js
  29. 4 4
      client/components/boards/boardsList.jade
  30. 72 8
      client/components/boards/boardsList.js
  31. 17 3
      client/components/boards/boardsList.styl
  32. 16 12
      client/components/cards/attachments.jade
  33. 2 1
      client/components/cards/cardDate.js
  34. 355 170
      client/components/cards/cardDetails.jade
  35. 179 14
      client/components/cards/cardDetails.js
  36. 32 3
      client/components/cards/cardDetails.styl
  37. 5 2
      client/components/cards/checklists.jade
  38. 26 11
      client/components/cards/checklists.js
  39. 4 1
      client/components/cards/checklists.styl
  40. 2 0
      client/components/cards/labels.styl
  41. 4 0
      client/components/cards/minicard.jade
  42. 6 10
      client/components/cards/minicard.js
  43. 1 1
      client/components/cards/minicard.styl
  44. 3 1
      client/components/cards/subtasks.jade
  45. 22 4
      client/components/cards/subtasks.js
  46. 0 3
      client/components/import/import.jade
  47. 16 13
      client/components/lists/list.js
  48. 3 3
      client/components/lists/list.styl
  49. 21 4
      client/components/lists/listBody.js
  50. 36 14
      client/components/lists/listHeader.jade
  51. 7 8
      client/components/lists/listHeader.js
  52. 64 128
      client/components/main/editor.js
  53. 7 0
      client/components/main/header.jade
  54. 6 3
      client/components/main/header.styl
  55. 10 4
      client/components/main/layouts.jade
  56. 7 0
      client/components/main/layouts.js
  57. 5 0
      client/components/main/popup.styl
  58. 10 0
      client/components/notifications/notification.jade
  59. 28 0
      client/components/notifications/notification.js
  60. 57 0
      client/components/notifications/notification.styl
  61. 53 0
      client/components/notifications/notificationIcon.jade
  62. 5 0
      client/components/notifications/notifications.jade
  63. 32 0
      client/components/notifications/notifications.js
  64. 17 0
      client/components/notifications/notifications.styl
  65. 20 0
      client/components/notifications/notificationsDrawer.jade
  66. 53 0
      client/components/notifications/notificationsDrawer.js
  67. 69 0
      client/components/notifications/notificationsDrawer.styl
  68. 25 12
      client/components/rules/actions/boardActions.jade
  69. 25 5
      client/components/rules/actions/boardActions.js
  70. 6 2
      client/components/settings/informationBody.jade
  71. 63 4
      client/components/settings/peopleBody.jade
  72. 95 0
      client/components/settings/peopleBody.js
  73. 1 1
      client/components/settings/peopleBody.styl
  74. 19 12
      client/components/settings/settingBody.jade
  75. 1 9
      client/components/settings/settingBody.js
  76. 4 1
      client/components/settings/settingBody.styl
  77. 200 29
      client/components/sidebar/sidebar.jade
  78. 402 13
      client/components/sidebar/sidebar.js
  79. 1 1
      client/components/sidebar/sidebar.styl
  80. 30 24
      client/components/sidebar/sidebarArchives.jade
  81. 9 0
      client/components/sidebar/sidebarArchives.js
  82. 26 7
      client/components/sidebar/sidebarFilters.jade
  83. 5 0
      client/components/sidebar/sidebarFilters.js
  84. 3 5
      client/components/swimlanes/swimlaneHeader.js
  85. 17 16
      client/components/swimlanes/swimlanes.jade
  86. 20 28
      client/components/swimlanes/swimlanes.js
  87. 1 0
      client/components/users/userAvatar.jade
  88. 61 23
      client/components/users/userHeader.jade
  89. 58 6
      client/components/users/userHeader.js
  90. 10 0
      client/lib/datepicker.js
  91. 9 1
      client/lib/filter.js
  92. 45 7
      client/lib/keyboard.js
  93. 8 3
      client/lib/textComplete.js
  94. 48 22
      client/lib/utils.js
  95. 7 3
      config/accounts.js
  96. 21 0
      config/router.js
  97. 47 5
      docker-compose.yml
  98. 4 4
      fix-download-unicode/cfs_access-point.txt
  99. 7 0
      helm/wekan/README.md
  100. 1 1
      helm/wekan/requirements.yaml

+ 5 - 0
.babelrc

@@ -0,0 +1,5 @@
+{ 
+  "presets": [ 
+    "@babel/preset-stage-3" 
+  ]
+}

+ 3 - 3
.devcontainer/Dockerfile

@@ -1,4 +1,4 @@
-FROM ubuntu:disco
+FROM ubuntu:rolling
 LABEL maintainer="sgr"
 LABEL maintainer="sgr"
 
 
 ENV BUILD_DEPS="gnupg gosu bsdtar wget curl bzip2 g++ build-essential python git ca-certificates iproute2"
 ENV BUILD_DEPS="gnupg gosu bsdtar wget curl bzip2 g++ build-essential python git ca-certificates iproute2"
@@ -6,8 +6,8 @@ ENV DEBIAN_FRONTEND=noninteractive
 
 
 ENV \
 ENV \
     DEBUG=false \
     DEBUG=false \
-    NODE_VERSION=8.17.0 \
-    METEOR_RELEASE=1.8.1 \
+    NODE_VERSION=12.16.3 \
+    METEOR_RELEASE=1.10.2 \
     USE_EDGE=false \
     USE_EDGE=false \
     METEOR_EDGE=1.5-beta.17 \
     METEOR_EDGE=1.5-beta.17 \
     NPM_VERSION=latest \
     NPM_VERSION=latest \

+ 1 - 0
.eslintignore

@@ -1 +1,2 @@
 packages/*
 packages/*
+.snap-meteor-1.8/*

+ 2 - 0
.eslintrc.json

@@ -11,6 +11,7 @@
     "browser": true,
     "browser": true,
     "meteor": true
     "meteor": true
   },
   },
+  "parser": "babel-eslint",
   "parserOptions": {
   "parserOptions": {
     "ecmaVersion": 2018,
     "ecmaVersion": 2018,
     "sourceType": "module"
     "sourceType": "module"
@@ -145,6 +146,7 @@
     "allowIsBoardMemberByCard": true,
     "allowIsBoardMemberByCard": true,
     "allowIsBoardMemberCommentOnly": true,
     "allowIsBoardMemberCommentOnly": true,
     "allowIsBoardMemberNoComments": true,
     "allowIsBoardMemberNoComments": true,
+    "allowIsBoardMemberWorker": true,
     "Emoji": true,
     "Emoji": true,
     "Checklists": true,
     "Checklists": true,
     "Settings": true,
     "Settings": true,

+ 257 - 0
.future-snap/broken-snapcraft.yaml

@@ -0,0 +1,257 @@
+name: wekan
+version: 0
+version-script: git describe --tags |  cut -c 2-
+summary: The open-source kanban
+description: |
+   Wekan is an open-source and collaborative kanban board application.
+
+   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.
+   Depending on target environment, some configuration settings might need to be adjusted.
+   For full list of configuration options call:
+   $ wekan.help
+
+confinement: strict
+grade: stable
+
+architectures:
+  - amd64
+
+plugs:
+  mongodb-plug:
+    interface: content
+    target: $SNAP_DATA/shared
+
+hooks:
+  configure:
+    plugs:
+      - network
+      - network-bind
+
+slots:
+  mongodb-slot:
+    interface: content
+    write:
+      - $SNAP_DATA/share
+
+apps:
+    wekan:
+        command: wekan-control
+        daemon: simple
+        plugs: [network, network-bind]
+
+    mongodb:
+        command: mongodb-control
+        daemon: simple
+        plugs: [network, network-bind]
+
+    caddy:
+        command: caddy-control
+        daemon: simple
+        plugs: [network, network-bind]
+
+    help:
+        command: wekan-help
+
+    database-backup:
+        command: mongodb-backup
+        plugs: [network, network-bind]
+
+    database-list-backups:
+        command: ls -al $SNAP_COMMON/db-backups/
+
+    database-restore:
+        command: mongodb-restore
+        plugs: [network, network-bind]
+
+parts:
+    mongodb:
+        source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.6.tgz
+        plugin: dump
+        stage-packages: [libssl1.0.0, libcurl3]
+        filesets:
+            mongo:
+                - usr
+                - bin
+                - lib
+        stage:
+            - $mongo
+        prime:
+            - $mongo
+
+    wekan:
+        source: .
+        plugin: nodejs
+        node-engine: 12.16.3
+        node-packages:
+            - node-gyp
+            - node-pre-gyp
+            - fibers
+        build-packages:
+            - ca-certificates
+            - apt-utils
+            - python
+            - python3
+            - g++
+            - capnproto
+            - curl
+            - libcurl3
+            - execstack
+            - nodejs
+            - npm
+        stage-packages:
+            - libfontconfig1
+        override-build: |
+            echo "Cleaning environment first"
+            rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
+            # Create the OpenAPI specification
+            rm -rf .build
+            ## Use Meteor 1.8.x on Snap
+            #rm -rf .meteor
+            #mv .snap-meteor-1.8/.meteor .
+            #mv .snap-meteor-1.8/package.json .
+            #mv .snap-meteor-1.8/package-lock.json .
+            ## Meteor 1.9.x has changes to Buffer() => Buffer.alloc(), so reverting those
+            #mv .snap-meteor-1.8/cfs_access-point.txt fix-download-unicode/
+            #mv .snap-meteor-1.8/export.js models/
+            #mv .snap-meteor-1.8/wekanCreator.js models/
+            #mv .snap-meteor-1.8/ldap.js packages/wekan-ldap/server/ldap.js
+            #mv .snap-meteor-1.8/oidc_server.js packages/wekan-oidc/oidc_server.js
+            rm -rf .snap-meteor-1.8
+            #mkdir -p .build/python
+            #cd .build/python
+            #git clone --depth 1 -b master https://github.com/Kronuz/esprima-python
+            #cd esprima-python
+            #python3 setup.py install
+            #cd ../../..
+            #mkdir -p ./public/api
+            #python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml
+            # we temporary need api2html and mkdirp
+            #npm install -g api2html@0.3.0
+            #npm install -g mkdirp
+            #api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml
+            #npm uninstall -g mkdirp
+            #npm uninstall -g api2html
+            # Node Fibers 100% CPU usage issue:
+            # https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
+            # https://github.com/meteor/meteor/issues/9796#issuecomment-381676326
+            # https://github.com/sandstorm-io/sandstorm/blob/0f1fec013fe7208ed0fd97eb88b31b77e3c61f42/shell/server/00-startup.js#L99-L129
+            # Also see beginning of wekan/server/authentication.js
+            #   import Fiber from "fibers";
+            #   Fiber.poolSize = 1e9;
+            # OLD: Download node version 8.12.0 prerelease build => Official node 8.12.0 has been released
+            # Description at https://releases.wekan.team/node.txt
+            ##echo "375bd8db50b9c692c0bbba6e96d4114cd29bee3770f901c1ff2249d1038f1348  node" >> node-SHASUMS256.txt.asc
+            ##curl https://releases.wekan.team/node -o node
+            # Verify Fibers patched node authenticity
+            ##echo "Fibers 100% CPU issue patched node authenticity:"
+            ##grep node node-SHASUMS256.txt.asc | shasum -a 256 -c -
+            ##rm -f node-SHASUMS256.txt.asc
+            ##chmod +x node
+            ##mv node `which node`
+            # DOES NOT WORK: paxctl fix.
+            # Removed from build-packages: - paxctl
+            #echo "Applying paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303"
+            #paxctl -mC `which node`
+            #echo "Installing npm"
+            #curl -L https://www.npmjs.com/install.sh | sh
+            echo "Installing meteor"
+            curl https://install.meteor.com/ -o install_meteor.sh
+            #sed -i "s|RELEASE=.*|RELEASE=\"1.8.1-beta.0\"|g" install_meteor.sh
+            chmod +x install_meteor.sh
+            sh install_meteor.sh
+            rm install_meteor.sh
+            # REPOS BELOW ARE INCLUDED TO WEKAN REPO
+            #if [ ! -d "packages" ]; then
+            #  mkdir packages
+            #fi
+            #if [ ! -d "packages/kadira-flow-router" ]; then
+            #  cd packages
+            #  git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router
+            #  cd ..
+            #fi
+            #if [ ! -d "packages/meteor-useraccounts-core" ]; then
+            #  cd packages
+            #  git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core
+            #  sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' meteor-useraccounts-core/package.js
+            #  cd ..
+            #fi
+            #if [ ! -d "packages/meteor-accounts-cas" ]; then
+            #  cd packages
+            #  git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git meteor-accounts-cas
+            #  cd ..
+            #fi
+            #if [ ! -d "packages/wekan-ldap" ]; then
+            #  cd packages
+            #  git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git
+            #  cd ..
+            #fi
+            #if [ ! -d "packages/wekan-scrollbar" ]; then
+            #  cd packages
+            #  git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git
+            #  cd ..
+            #fi
+            #if [ ! -d "packages/wekan_accounts-oidc" ]; then
+            #  cd packages
+            #  git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git
+            #  mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan-accounts-oidc
+            #  mv meteor-accounts-oidc/packages/switch_oidc wekan-oidc
+            #  rm -rf meteor-accounts-oidc
+            #  cd ..
+            #fi
+            #if [ ! -d "packages/markdown" ]; then
+            #  cd packages
+            #  git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git
+            #  cd ..
+            #fi
+            rm -rf .build
+            meteor add standard-minifier-js --allow-superuser
+            meteor npm install --allow-superuser
+            meteor npm install --allow-superuser --save babel-runtime
+            meteor build .build --directory --allow-superuser
+            cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
+            #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 .build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt
+            #rm -rf node_modules/bcrypt
+            #meteor npm install --save bcrypt
+            # Change from npm-bcrypt directory back to .build/bundle/programs/server directory.
+            #cd ../../../../
+            # Change to directory .build/bundle/programs/server
+            cd .build/bundle/programs/server
+            npm install
+            npm install --allow-superuser --save babel-runtime
+            #meteor npm install --save bcrypt
+            # Change back to Wekan source directory
+            cd ../../../..
+            cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
+            cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
+            rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
+            rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
+            rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
+            rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
+            rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
+            # Meteor 1.8.x additional .swp remove
+            rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
+
+        organize:
+            README: README.wekan
+        prime:
+            - -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
+
+    helpers:
+        source: snap-src
+        plugin: dump
+
+    caddy:
+        plugin: dump
+        source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
+        source-type: tar
+        organize:
+          caddy: bin/caddy
+          CHANGES.txt: CADDY_CHANGES.txt
+          EULA.txt: CADDY_EULA.txt
+          LICENSES.txt: CADDY_LICENSES.txt
+          README.txt: CADDY_README.txt
+        stage:
+          - -init

+ 155 - 0
.future-snap/snapcraft.yaml

@@ -0,0 +1,155 @@
+name: wekan
+version: git
+summary: The open-source kanban
+description: |
+   Wekan is an open-source and collaborative kanban board application.
+
+   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.
+   Depending on target environment, some configuration settings might need to be adjusted.
+   For full list of configuration options call:
+   $ wekan.help
+
+confinement: strict
+grade: stable
+base: core18
+
+architectures:
+  - amd64
+
+plugs:
+  mongodb-plug:
+    interface: content
+    target: $SNAP_DATA/shared
+
+hooks:
+  configure:
+    plugs:
+      - network
+      - network-bind
+
+slots:
+  mongodb-slot:
+    interface: content
+    write:
+      - $SNAP_DATA/share
+
+apps:
+    wekan:
+        command: wekan-control
+        daemon: simple
+        plugs: [network, network-bind]
+
+    mongodb:
+        command: mongodb-control
+        daemon: simple
+        plugs: [network, network-bind]
+
+    caddy:
+        command: caddy-control
+        daemon: simple
+        plugs: [network, network-bind]
+
+    help:
+        command: wekan-help
+
+    database-backup:
+        command: mongodb-backup
+        plugs: [network, network-bind]
+
+    database-list-backups:
+        command: ls -al $SNAP_COMMON/db-backups/
+
+    database-restore:
+        command: mongodb-restore
+        plugs: [network, network-bind]
+
+parts:
+    mongodb:
+        source: https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.2/multiverse/binary-amd64/mongodb-org-server_4.2.2_amd64.deb
+        #https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.14.tgz
+        #https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
+        plugin: dump
+        stage-packages: [libssl1.0.0, libcurl3]
+        filesets:
+            mongo:
+                - usr
+                - bin
+                - lib
+        stage:
+            - $mongo
+        prime:
+            - $mongo
+
+    wekan:
+        source: .
+        plugin: nodejs
+        node-engine: 12.14.3
+        node-packages:
+            - node-gyp
+            - node-pre-gyp
+            - fibers
+        build-packages:
+            - ca-certificates
+            - apt-utils
+            - build-essential
+            - python
+            - python3
+            - g++
+            - capnproto
+            - curl
+            - libcurl3
+            - execstack
+            - nodejs
+            - npm
+        stage-packages:
+            - libfontconfig1
+        override-build: |
+            echo "Cleaning environment first"
+            rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
+            rm -rf .build
+            echo "Installing meteor"
+            curl https://install.meteor.com/ -o install_meteor.sh
+            chmod +x install_meteor.sh
+            sh install_meteor.sh
+            rm install_meteor.sh
+            rm -rf .build
+            meteor add standard-minifier-js --allow-superuser
+            meteor npm install --allow-superuser
+            meteor npm install --allow-superuser --save babel-runtime
+            meteor build .build --directory --allow-superuser
+            cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
+            cd .build/bundle/programs/server
+            npm install
+            npm install --allow-superuser --save babel-runtime
+            # Change back to Wekan source directory
+            cd ../../../..
+            cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
+            cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
+            rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
+            rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
+            rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
+            rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
+            rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
+            rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
+
+        organize:
+            README: README.wekan
+        prime:
+            - -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
+
+    helpers:
+        source: snap-src
+        plugin: dump
+
+    caddy:
+        plugin: dump
+        source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
+        source-type: tar
+        organize:
+          caddy: bin/caddy
+          CHANGES.txt: CADDY_CHANGES.txt
+          EULA.txt: CADDY_EULA.txt
+          LICENSES.txt: CADDY_LICENSES.txt
+          README.txt: CADDY_README.txt
+        stage:
+          - -init

+ 2 - 0
.github/ISSUE_TEMPLATE.md

@@ -2,6 +2,8 @@
 
 
 Add these issues to elsewhere:
 Add these issues to elsewhere:
 - Snap: https://github.com/wekan/wekan-snap/issues
 - Snap: https://github.com/wekan/wekan-snap/issues
+- LDAP: https://github.com/wekan/wekan-ldap/issues
+- UCS: https://github.com/wekan/univention/issues
 
 
 Other Wekan issues can be added here.
 Other Wekan issues can be added here.
 
 

+ 10 - 0
.gitpod.Dockerfile

@@ -0,0 +1,10 @@
+FROM gitpod/workspace-mongodb
+                    
+USER gitpod
+
+# Install custom tools, runtime, etc. using apt-get
+# For example, the command below would install "bastet" - a command line tetris clone:
+#
+# RUN sudo apt-get -q update && #     sudo apt-get install -yq bastet && #     sudo rm -rf /var/lib/apt/lists/*
+#
+# More information: https://www.gitpod.io/docs/config-docker/

+ 4 - 0
.gitpod.yml

@@ -0,0 +1,4 @@
+tasks:
+  - init: npm install
+image:
+  file: .gitpod.Dockerfile

+ 11 - 10
.meteor/packages

@@ -6,10 +6,11 @@
 meteor-base@1.4.0
 meteor-base@1.4.0
 
 
 # Build system
 # Build system
-ecmascript@0.13.2
-standard-minifier-css@1.5.4
-standard-minifier-js@2.5.2
+ecmascript@0.14.3
+standard-minifier-css@1.6.0
+standard-minifier-js@2.6.0
 mquandalle:jade
 mquandalle:jade
+coffeescript@2.4.1!
 
 
 # Polyfills
 # Polyfills
 es5-shim@4.8.0
 es5-shim@4.8.0
@@ -22,7 +23,7 @@ dburles:collection-helpers
 idmontie:migrations
 idmontie:migrations
 matb33:collection-hooks
 matb33:collection-hooks
 matteodem:easy-search
 matteodem:easy-search
-mongo@1.7.0
+mongo@1.10.0
 mquandalle:collection-mutations
 mquandalle:collection-mutations
 
 
 # Account system
 # Account system
@@ -37,13 +38,12 @@ wekan-accounts-oidc
 # Utilities
 # Utilities
 check@1.3.1
 check@1.3.1
 jquery@1.11.10
 jquery@1.11.10
-random@1.1.0
+random@1.2.0
 reactive-dict@1.3.0
 reactive-dict@1.3.0
 session@1.2.0
 session@1.2.0
 tracker@1.2.0
 tracker@1.2.0
 underscore@1.0.10
 underscore@1.0.10
 3stack:presence
 3stack:presence
-alethes:pages
 arillo:flow-router-helpers
 arillo:flow-router-helpers
 audit-argument-checks@1.0.7
 audit-argument-checks@1.0.7
 kadira:blaze-layout
 kadira:blaze-layout
@@ -67,15 +67,15 @@ templates:tabs
 verron:autosize
 verron:autosize
 simple:json-routes
 simple:json-routes
 rajit:bootstrap3-datepicker
 rajit:bootstrap3-datepicker
-shell-server@0.4.0
+shell-server@0.5.0
 simple:rest-accounts-password
 simple:rest-accounts-password
 useraccounts:core
 useraccounts:core
 email@1.2.3
 email@1.2.3
 horka:swipebox
 horka:swipebox
-dynamic-import@0.5.1
+dynamic-import@0.5.2
 staringatlights:fast-render
 staringatlights:fast-render
 
 
-accounts-password@1.5.2
+accounts-password@1.6.0
 cfs:gridfs
 cfs:gridfs
 rzymek:fullcalendar
 rzymek:fullcalendar
 momentjs:moment@2.22.2
 momentjs:moment@2.22.2
@@ -85,7 +85,8 @@ msavin:usercache
 wekan-scrollbar
 wekan-scrollbar
 mquandalle:perfect-scrollbar
 mquandalle:perfect-scrollbar
 mdg:meteor-apm-agent@3.2.0-rc.0!
 mdg:meteor-apm-agent@3.2.0-rc.0!
-coagmano:stylus
+# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
+coagmano:stylus@1.1.0!
 lucasantoniassi:accounts-lockout
 lucasantoniassi:accounts-lockout
 meteorhacks:subs-manager
 meteorhacks:subs-manager
 meteorhacks:picker
 meteorhacks:picker

+ 1 - 1
.meteor/release

@@ -1 +1 @@
-METEOR@1.8.3
+METEOR@1.10.2

+ 38 - 38
.meteor/versions

@@ -1,29 +1,28 @@
 3stack:presence@1.1.2
 3stack:presence@1.1.2
-accounts-base@1.4.5
-accounts-oauth@1.1.16
-accounts-password@1.5.2
+accounts-base@1.6.0
+accounts-oauth@1.2.0
+accounts-password@1.6.0
 aldeed:collection2@2.10.0
 aldeed:collection2@2.10.0
 aldeed:collection2-core@1.2.0
 aldeed:collection2-core@1.2.0
 aldeed:schema-deny@1.1.0
 aldeed:schema-deny@1.1.0
 aldeed:schema-index@1.1.1
 aldeed:schema-index@1.1.1
 aldeed:simple-schema@1.5.4
 aldeed:simple-schema@1.5.4
-alethes:pages@1.8.6
 allow-deny@1.1.0
 allow-deny@1.1.0
 arillo:flow-router-helpers@0.5.2
 arillo:flow-router-helpers@0.5.2
 audit-argument-checks@1.0.7
 audit-argument-checks@1.0.7
 autoupdate@1.6.0
 autoupdate@1.6.0
-babel-compiler@7.4.2
-babel-runtime@1.4.0
+babel-compiler@7.5.3
+babel-runtime@1.5.0
 base64@1.0.12
 base64@1.0.12
 binary-heap@1.0.11
 binary-heap@1.0.11
 blaze@2.3.4
 blaze@2.3.4
 blaze-tools@1.0.10
 blaze-tools@1.0.10
-boilerplate-generator@1.6.0
+boilerplate-generator@1.7.0
 browser-policy-common@1.0.11
 browser-policy-common@1.0.11
 browser-policy-framing@1.1.0
 browser-policy-framing@1.1.0
-caching-compiler@1.2.1
+caching-compiler@1.2.2
 caching-html-compiler@1.1.3
 caching-html-compiler@1.1.3
-callback-hook@1.2.0
+callback-hook@1.3.0
 cfs:access-point@0.1.49
 cfs:access-point@0.1.49
 cfs:base-package@0.0.30
 cfs:base-package@0.0.30
 cfs:collection@0.5.5
 cfs:collection@0.5.5
@@ -44,23 +43,24 @@ cfs:upload-http@0.0.20
 cfs:worker@0.1.5
 cfs:worker@0.1.5
 check@1.3.1
 check@1.3.1
 chuangbo:cookie@1.1.0
 chuangbo:cookie@1.1.0
-coagmano:stylus@2.0.0
-coffeescript@1.0.17
+coagmano:stylus@1.1.0
+coffeescript@2.4.1
+coffeescript-compiler@2.4.1
 cottz:publish-relations@2.0.8
 cottz:publish-relations@2.0.8
 dburles:collection-helpers@1.1.0
 dburles:collection-helpers@1.1.0
 ddp@1.4.0
 ddp@1.4.0
 ddp-client@2.3.3
 ddp-client@2.3.3
 ddp-common@1.4.0
 ddp-common@1.4.0
 ddp-rate-limiter@1.0.7
 ddp-rate-limiter@1.0.7
-ddp-server@2.3.0
+ddp-server@2.3.1
 deps@1.0.12
 deps@1.0.12
 diff-sequence@1.1.1
 diff-sequence@1.1.1
-dynamic-import@0.5.1
+dynamic-import@0.5.2
 easylogic:summernote@0.8.8
 easylogic:summernote@0.8.8
-ecmascript@0.13.2
+ecmascript@0.14.3
 ecmascript-runtime@0.7.0
 ecmascript-runtime@0.7.0
-ecmascript-runtime-client@0.9.0
-ecmascript-runtime-server@0.8.0
+ecmascript-runtime-client@0.10.0
+ecmascript-runtime-server@0.9.0
 ejson@1.1.1
 ejson@1.1.1
 email@1.2.3
 email@1.2.3
 es5-shim@4.8.0
 es5-shim@4.8.0
@@ -75,7 +75,7 @@ htmljs@1.0.11
 http@1.4.2
 http@1.4.2
 id-map@1.1.0
 id-map@1.1.0
 idmontie:migrations@1.0.3
 idmontie:migrations@1.0.3
-inter-process-messaging@0.1.0
+inter-process-messaging@0.1.1
 jquery@1.11.11
 jquery@1.11.11
 kadira:blaze-layout@2.3.0
 kadira:blaze-layout@2.3.0
 kadira:dochead@1.5.0
 kadira:dochead@1.5.0
@@ -84,7 +84,7 @@ kenton:accounts-sandstorm@0.7.0
 konecty:mongo-counter@0.0.5_3
 konecty:mongo-counter@0.0.5_3
 lamhieu:meteorx@2.1.1
 lamhieu:meteorx@2.1.1
 lamhieu:unblock@1.0.0
 lamhieu:unblock@1.0.0
-launch-screen@1.1.1
+launch-screen@1.2.0
 livedata@1.0.18
 livedata@1.0.18
 localstorage@1.2.0
 localstorage@1.2.0
 logging@1.1.20
 logging@1.1.20
@@ -101,16 +101,16 @@ meteorhacks:collection-utils@1.2.0
 meteorhacks:picker@1.0.3
 meteorhacks:picker@1.0.3
 meteorhacks:subs-manager@1.6.4
 meteorhacks:subs-manager@1.6.4
 meteorspark:util@0.2.0
 meteorspark:util@0.2.0
-minifier-css@1.4.3
-minifier-js@2.5.1
+minifier-css@1.5.0
+minifier-js@2.6.0
 minifiers@1.1.8-faster-rebuild.0
 minifiers@1.1.8-faster-rebuild.0
-minimongo@1.4.5
-mobile-status-bar@1.0.14
-modern-browsers@0.1.4
-modules@0.14.0
-modules-runtime@0.11.0
+minimongo@1.6.0
+mobile-status-bar@1.1.0
+modern-browsers@0.1.5
+modules@0.15.0
+modules-runtime@0.12.0
 momentjs:moment@2.24.0
 momentjs:moment@2.24.0
-mongo@1.7.0
+mongo@1.10.0
 mongo-decimal@0.1.1
 mongo-decimal@0.1.1
 mongo-dev-server@1.1.0
 mongo-dev-server@1.1.0
 mongo-id@1.0.7
 mongo-id@1.0.7
@@ -127,13 +127,13 @@ mquandalle:mousetrap-bindglobal@0.0.1
 mquandalle:perfect-scrollbar@0.6.5_2
 mquandalle:perfect-scrollbar@0.6.5_2
 msavin:usercache@1.8.0
 msavin:usercache@1.8.0
 npm-bcrypt@0.9.3
 npm-bcrypt@0.9.3
-npm-mongo@3.2.0
-oauth@1.2.8
-oauth2@1.2.1
+npm-mongo@3.7.0
+oauth@1.3.0
+oauth2@1.3.0
 observe-sequence@1.0.16
 observe-sequence@1.0.16
 ongoworks:speakingurl@1.1.0
 ongoworks:speakingurl@1.1.0
 ordered-dict@1.1.0
 ordered-dict@1.1.0
-ostrio:cookies@2.5.0
+ostrio:cookies@2.6.0
 peerlibrary:assert@0.3.0
 peerlibrary:assert@0.3.0
 peerlibrary:base-component@0.16.0
 peerlibrary:base-component@0.16.0
 peerlibrary:blaze-components@0.15.1
 peerlibrary:blaze-components@0.15.1
@@ -144,7 +144,7 @@ promise@0.11.2
 raix:eventemitter@0.1.3
 raix:eventemitter@0.1.3
 raix:handlebar-helpers@0.2.5
 raix:handlebar-helpers@0.2.5
 rajit:bootstrap3-datepicker@1.7.1_1
 rajit:bootstrap3-datepicker@1.7.1_1
-random@1.1.0
+random@1.2.0
 rate-limit@1.0.9
 rate-limit@1.0.9
 reactive-dict@1.3.0
 reactive-dict@1.3.0
 reactive-var@1.0.11
 reactive-var@1.0.11
@@ -156,19 +156,19 @@ server-render@0.3.1
 service-configuration@1.0.11
 service-configuration@1.0.11
 session@1.2.0
 session@1.2.0
 sha@1.0.9
 sha@1.0.9
-shell-server@0.4.0
+shell-server@0.5.0
 simple:authenticate-user-by-token@1.0.1
 simple:authenticate-user-by-token@1.0.1
 simple:json-routes@2.1.0
 simple:json-routes@2.1.0
 simple:rest-accounts-password@1.1.2
 simple:rest-accounts-password@1.1.2
 simple:rest-bearer-token-parser@1.0.1
 simple:rest-bearer-token-parser@1.0.1
 simple:rest-json-error-handler@1.0.1
 simple:rest-json-error-handler@1.0.1
-socket-stream-client@0.2.2
+socket-stream-client@0.3.0
 softwarerero:accounts-t9n@1.3.11
 softwarerero:accounts-t9n@1.3.11
 spacebars@1.0.15
 spacebars@1.0.15
 spacebars-compiler@1.1.3
 spacebars-compiler@1.1.3
-srp@1.0.12
-standard-minifier-css@1.5.4
-standard-minifier-js@2.5.2
+srp@1.1.0
+standard-minifier-css@1.6.0
+standard-minifier-js@2.6.0
 staringatlights:fast-render@3.2.0
 staringatlights:fast-render@3.2.0
 staringatlights:inject-data@2.3.0
 staringatlights:inject-data@2.3.0
 tap:i18n@1.8.2
 tap:i18n@1.8.2
@@ -181,12 +181,12 @@ tracker@1.2.0
 twbs:bootstrap@3.3.6
 twbs:bootstrap@3.3.6
 ui@1.0.13
 ui@1.0.13
 underscore@1.0.10
 underscore@1.0.10
-url@1.2.0
+url@1.3.0
 useraccounts:core@1.14.2
 useraccounts:core@1.14.2
 useraccounts:flow-routing@1.14.2
 useraccounts:flow-routing@1.14.2
 useraccounts:unstyled@1.14.2
 useraccounts:unstyled@1.14.2
 verron:autosize@3.0.8
 verron:autosize@3.0.8
-webapp@1.7.5
+webapp@1.9.1
 webapp-hashing@1.0.9
 webapp-hashing@1.0.9
 wekan-accounts-cas@0.1.0
 wekan-accounts-cas@0.1.0
 wekan-accounts-oidc@1.0.10
 wekan-accounts-oidc@1.0.10

+ 1 - 0
.prettierignore

@@ -5,3 +5,4 @@ node_modules/
 .vscode/
 .vscode/
 .tx/
 .tx/
 .github/
 .github/
+.snap-meteor-1.8/

+ 2 - 2
.travis.yml

@@ -1,9 +1,9 @@
-dist: disco
+dist: focal
 sudo: required
 sudo: required
 
 
 env:
 env:
   TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
   TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
-  TRAVIS_NODE_VERSION: 8.17.0
+  TRAVIS_NODE_VERSION: 12.16.3
   TRAVIS_NPM_VERSION: latest
   TRAVIS_NPM_VERSION: latest
 
 
 before_install:
 before_install:

+ 785 - 0
CHANGELOG.md

@@ -1,3 +1,788 @@
+# Upcoming Wekan release
+
+This release adds the following server platforms:
+
+- [Android arm64/x64](https://github.com/wekan/wekan/wiki/Android).
+  Thanks to xet7.
+
+and adds the following features:
+
+- [Install Wekan to mobile homescreen icon and use fullscreen
+  PWA](https://github.com/commit/8d5adc04645e3e71423f16869f39b8d79969bccd).
+  [Docs for iOS and Android at wiki PWA page](https://github.com/wekan/wekan/wiki/PWA).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Fix getStartDayOfWeek once again](https://github.com/wekan/wekan/pull/3061).
+  Thanks to marc1006.
+- [Fix shortcuts list and support card shortcuts when hovering
+  a card](https://github.com/wekan/wekan/pull/3066).
+  Thanks to marc1006.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v4.01 2020-04-28 Wekan release
+
+This release adds the following updates:
+
+- [Upgrade to Node v12.16.3](https://github.com/wekan/wekan/commit/1d89e96dd101c11913f1acdd6d16b5650eaf18a7).
+  Thanks to Node developers and xet7.
+
+and fixes the following bugs:
+
+- [Fix Docker builds](https://github.com/wekan/wekan/commit/280e66947e3afa878c41e876cf827ebcec81a2c6).
+  Thanks to xet7.
+- [Fix Cards and Users API docs at https://wekan.github.io/api/ not generated because of
+  syntax error and new Javascript syntax](https://github.com/wekan/wekan/commit/9ae20a3f51e63c29f536e2f5b3e66a2c7d88c691).
+  Wekan uses wekan/releases/generate-docs*.sh Python code to generate OpenAPI docs,
+  it did not show any errors while generating docs, only left out parts of API docs.
+  This affected Wekan versions v3.94-v4.00.
+  Thanks to pvcon13 and xet7.
+- [Fix list header height when cards count is shown](https://github.com/wekan/wekan/pull/3056).
+  Thanks to marc1006.
+- [Smaller height for Add Board button](https://github.com/wekan/wekan/commit/6afc9259f084717a0cc3ce6d66979fd7c1471939).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v4.00 2020-04-27 Wekan release
+
+This release fixes the following bugs:
+
+- [Make sure that the board header buttons fit into one line even for devices with 360px width
+  resolution](https://github.com/wekan/wekan/pull/3052).
+  Thanks to marc1006.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.99 2020-04-27 Wekan release
+
+This release fixes the following bugs:
+
+- [Fix Boards are very hard to tap in mobile](https://github.com/wekan/wekan/pull/3051).
+  Thanks to marc1006.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.98 2020-04-25 Wekan release
+
+News:
+
+- There is now many mobile and desktop webbrowser fixes. Please test does your
+  favourite Javascript enabled webbrowser work, and add issues if something
+  does not work, and there is no existing issue about that yet.
+- Desktop browser mode has setting for Show/Hide drag handles:
+  top right click username / Change Settings / Show desktop drag handles.
+  You can request desktop website also at mobile webbrowsers on Android.
+  At iOS requesting desktop website did not seem to work yet.
+- At iOS Safari and Chrome, to see swimlane buttons you need to scroll to right.
+  Fixes to this and other issues are welcome as pull request.
+
+This release adds the following new features:
+
+- [Pre-fill the title of checklists (Trello-style)](https://github.com/wekan/wekan/pull/3030).
+  Thanks to boeserwolf.
+- [Implement option to change the first day of the week in user settings](https://github.com/wekan/wekan/pull/3032).
+  Thanks to marc1006.
+- [Add babel to build chain and linter. Enables fancy Javascript language
+  features like optional chaining, for developer happiness](https://github.com/wekan/wekan/pull/3034).
+  Thanks to boeserwolf.
+- [Use only one 'Apply' button for applying the user settings](https://github.com/wekan/wekan/pull/3039).
+  Thanks to marc1006.
+- [Allow variable height for board list items. Allow words in title/description to be able to break
+  and wrap onto the next line](https://github.com/wekan/wekan/pull/3046).
+  Thanks to marc1006.
+
+and adds the following updates:
+
+- [Upgrade to Meteor 1.10.2](https://github.com/wekan/wekan/commit/d1f98d0c472fb41e25fb29a9a6f6dae7db003f6f).
+  Thanks to Meteor developers and xet7.
+- [Set Snap MongoDB compatibility to 4.2 according to Meteor ChangeLog](https://github.com/wekan/wekan/commit/7de18eccea3854db3be6197bf21afbfd3ddb65a6).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Multiple lint issue fixes](https://github.com/wekan/wekan/pull/3031).
+  Thanks to marc1006.
+- [Fix lint errors in lint error fix](https://github.com/wekan/wekan/commit/9e95c06415e614e587d684ff9660cc53c5f8c8d3).
+  Thanks to xet7.
+- [Fix getStartDayOfWeek function](https://github.com/wekan/wekan/pull/3038).
+  Thanks to marc1006 and boeserwolf.
+- Improve mobile devices support [Part1](https://github.com/wekan/wekan/pull/3040) and [Part2](https://github.com/wekan/wekan/pull/3045).
+  Thanks to marc1006.
+- [Fix Wekan not load at all in Firefox v.68 for Android](https://github.com/wekan/wekan/commit/1235363465b824d26129d4aa74a4445f362c1a73).
+  Thanks to xet7.
+- [Fix comment typo in docker-compose.yml](https://github.com/wekan/wekan/pull/3044).
+  Thanks to VictorioBerra.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.97 2020-04-19 Wekan release
+
+This release adds the following new features:
+
+- [Sortable boards](https://github.com/wekan/wekan/pull/3027).
+  Thanks to boeserwolf.
+- [Added dockerfiles for multi-arch builds and manifest](https://github.com/wekan/wekan/pull/3023).
+  [In Progress](https://github.com/wekan/wekan/issues/2999).
+  Thanks to brokencode64.
+- [Make linked card clickable](https://github.com/wekan/wekan/pull/3025).
+  Thanks to boeserwolf.
+
+and fixes the following bugs:
+
+- [Fix using checklists on mobile and iPad](https://github.com/wekan/wekan/pull/3019).
+  Thanks to devinsm.
+- [Improve card layout on mobile devices](https://github.com/wekan/wekan/pull/3024).
+  Thanks to marc1006.
+- [Make OCP OAuth work with Openshift 4.x](https://github.com/wekan/wekan/pull/3020).
+  Thanks to ckavili.
+- [Remove old warning from Sandstorm import board data loss, because bug has been already
+  fixed](https://github.com/wekan/wekan/commit/960fe5163b6a2f7c3dca03b5e31d69611b49f079).
+  Thanks to aputsiaq and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.96 2020-04-15 Wekan release
+
+This release adds the following Sandstorm updates:
+
+- This is the first Sandstorm Wekan release that uses newest Meteor 1.10.1 and Node 12.x.
+  Now all Wekan platforms use newest Meteor and Node 12.x LTS.
+  Thanks to kentonv and xet7.
+- [Fix capnp workaround to work with newest Meteor and
+  Node 12.x](https://github.com/wekan/wekan/commit/b2d546579c4957352c29b36c0c8a4a08b944dbb4).
+  Thanks to kentonv.
+- [Update Sandstorm release script for newest Meteor and
+  Node 12.x](https://github.com/wekan/wekan/commit/c5f782976b971fa3f2323e80a013bbf6a49c0596).
+  Thanks to xet7.
+- [Remove Meteor 1.8.x files because Sandstorm Wekan now uses newest
+  Meteor](https://github.com/wekan/wekan/commit/1a836969e10215bad47ac56a9b0d9de801b66fd2).
+  Thanks to xet7.
+
+and adds the following new features:
+
+- [Hide password auth with environment variable PASSWORD_LOGIN_ENABLED=false](https://github.com/wekan/wekan/pull/3014).
+  Snap example: `sudo snap set wekan password-login-enabled='false'` .
+  Thanks to salleman33.
+
+and fixes the following bugs:
+
+- [Fix Board admins can not clone or archive their boards at All Boards
+  page](https://github.com/wekan/wekan/pull/3013).
+  Thanks to salleman33.
+- [Fix `<p>` margin in card labels](https://github.com/wekan/wekan/pull/3015).
+  Thanks to boeserwolf.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.95 2020-04-12 Wekan release
+
+This release adds the following new features:
+
+- [Add gitpod config](https://github.com/wekan/wekan/pull/3009).
+  This adds support for Gitpod.io, a free automated
+  dev environment that makes contributing and generally working on GitHub
+  projects much easier. It allows anyone to start a ready-to-code dev
+  environment for any branch, issue and pull request with a single click.
+  Thanks to juniormendonca.
+- [Public boards overview](https://github.com/wekan/wekan/pull/3008).
+  Thanks to NicoP-S.
+
+and fixes the following bugs:
+
+- [Fix styling issue in notifications drawer](https://github.com/wekan/wekan/pull/3012).
+  Thanks to boeserwolf.
+- [Fix error in notifications cleanup cron](https://github.com/wekan/wekan/pull/3010).
+  Thanks to jtbairdsr.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.94 2020-04-12 Wekan release
+
+This release adds the following new features:
+
+- [Public vote](https://github.com/wekan/wekan/pull/3006).
+  Thanks to NicoP-S.
+- [Add robots.txt disallow all](https://github.com/wekan/wekan/commit/3fae5355d40055757bf4a5f0c503581195609720).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.93 2020-04-10 Wekan release
+
+This release adds the following new features:
+
+- [Trello vote import & hide export button if with_api is
+  disabled](https://github.com/wekan/wekan/pull/3000).
+  Thanks to NicoP-S.
+- [When adding a user to a board that has subtasks, also add user to the subtask
+  board](https://github.com/wekan/wekan/pull/3004).
+  Thanks to slvrpdr.
+
+and adds the following updates:
+
+- Upgrade to Node v12.16.2 [Part1](https://github.com/wekan/wekan/commit/6db717b9b384fe1491063e507b80e67791a07e3a)
+  and [Part2](https://github.com/wekan/wekan/commit/268d7fcb32186a902a84e7f6d80c50b1f3790bad).
+  Thanks to Node developers and xet7.
+
+and fixes the following bugs:
+
+- [Fix bug that prevents editing or deleting
+  comments](https://github.com/wekan/wekan/pull/3005).
+  Thanks to jtbairdsr.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.92 2020-04-09 Wekan release
+
+This release adds the following new features:
+
+- [Scheduler to clean up read notifications. Also added a button to manually remove all
+  read notifications, and a fix to prevent users form getting notifications for their own
+  actions](https://github.com/wekan/wekan/pull/2998).
+  Thanks to jtbairdsr.
+- [Add setting](https://github.com/wekan/wekan/commit/5ebb47cb0ec7272894a37d99579ede872251f55c)
+  default [NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE=2](https://github.com/wekan/wekan/pull/2998)
+  to all Wekan platforms.
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.91 2020-04-08 Wekan release
+
+This release adds the following new features:
+
+- [OpenShift: Route template added to helm chart for Openshift v4x
+  cluster](https://github.com/wekan/wekan/pull/2996).
+  Thanks to ckavili.
+- [Filter by Assignee](https://github.com/wekan/wekan/pull/2997).
+  Thanks to daniel-eder.
+- [Vote on Card](https://github.com/wekan/wekan/pull/2994).
+  Thanks to NicoP-S and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.90 2020-04-06 Wekan release
+
+This release makes the following updates:
+
+- [Update dependencies](https://github.com/wekan/wekan/commit/d798f6e3ef09595ce4f1d1fbc053eec70fc91fb9).
+
+and updates the following translations:
+
+- [Update layouts.js for zh-TW language name](https://github.com/wekan/wekan/pull/2988).
+  Thanks to doggy8088.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.89 2020-04-05 Wekan release
+
+This release adds the following new features:
+
+- [Create subtasks in parenttask swimlane](https://github.com/wekan/wekan/issues/1953).
+  Thanks to TOSCom-DanielEder.
+- [When searching cards in a board, also search from Custom Fields](https://github.com/wekan/wekan/pull/2985).
+  Thanks to slvrpdr.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.88 2020-04-02 Wekan release
+
+This release adds the following new features:
+
+- [Notification drawer](https://github.com/wekan/wekan/pull/2975) [like Trello](https://github.com/wekan/wekan/issues/2471).
+  Thanks to jtbairdsr and xet7.
+
+and makes the following UI changes:
+
+- [Minicard labels on the top and title on bottom](https://github.com/wekan/wekan/issues/2980).
+  Thanks to helioguardabaxo and xet7.
+
+and fixes the following bugs:
+
+- [Fix start-wekan.sh MongoDB port to 27017](https://github.com/wekan/wekan/commit/c60a092fc0ed9fe15c417bcb443b1e3e3aaedf7e).
+  Thanks to Keelan and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.87 2020-04-01 Wekan release
+
+This release makes the following UI changes:
+
+- [Move "Rules" from "Board View" to "Board Settings"](https://github.com/wekan/wekan/issues/2973).
+  Thanks to helioguardabaxo and xet7.
+- [Improvements on card details visualization](https://github.com/wekan/wekan/issues/2974).
+  Thanks to helioguardabaxo and xet7.
+- [Hide duplicate "Hide system messages" at Change Settings/Member Settings, because it's also on card
+  slider](https://github.com/wekan/wekan/issues/2837).
+  Thanks to notohiro and xet7.
+
+and fixes the following bugs:
+
+- [Fix Browser always reload the whole page when I change one of the card
+  color](https://github.com/wekan/wekan/commit/3546d7aa02bc65cf1183cb493adeb543ba51945d).
+  Fixed by making label colors and text again editable.
+  Regression from [Wekan v3.86 2)](https://github.com/wekan/wekan/commit/b9099a8b7ea6f63c79bdcbb871cb993b2cb7e325).
+  Thanks to javen9881 and xet7.
+- [Fix richer editor submit did not clear edit area](https://github.com/wekan/wekan/commit/033d6710470b2ecd7a0ec0b2f0741ff459e68b32).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.86 2020-03-24 Wekan release
+
+This release fixes the following bugs:
+
+- [Fix Rich editor can not be disabled, regression from changes yesterday at Wekan v3.85](https://github.com/wekan/wekan/commit/12ab8fac5db9c5ac8069d0ca2bca340d6004a25b).
+  Thanks to uusijani, vjrj and xet7.
+- [1) Fix Pasting text into a card is adding a line before and after
+      (and multiplies by pasting more) by changing paste "p" to "br".
+   2) Fixes to summernote and markdown comment editors, related
+       to keeping them open when adding comments, having
+       @member mention not close card, and disabling clicking of
+       @member mention](https://github.com/wekan/wekan/commit/b9099a8b7ea6f63c79bdcbb871cb993b2cb7e325).
+  Thanks to xet7 !
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.85 2020-03-23 Wekan release
+
+This release fixes the following CRITICAL SECURITY VULNERABILITIES:
+
+- [Fix XSS bug reported today 4 hours ago by Cyb3rjunky](https://github.com/wekan/wekan/commit/482682e50079d70c5113169020d6834013b57c11).
+  Logged in users could run javascript in input fields.
+  This affects Wekan versions v3.12-v3.84.
+  In [Wekan v3.12](https://github.com/wekan/wekan/blob/master/CHANGELOG.md#v312-2019-08-09-wekan-release)
+  there was [changes for XSS filter to allow inserting images, videos etc
+  on comment WYSIWYG editor](https://github.com/wekan/wekan/pull/2593)
+  so features related to that are now removed.
+  After this fix, Javascript in input fields is not executed.
+  Thanks to Cyb3rjunky and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.84 2020-03-16 Wekan release
+
+This release adds the following features:
+
+- Add settings for mouse wheel scroll inertia and scroll
+  amount [Part1](https://github.com/wekan/wekan/commit/9d13001b903f9ec50f5fa3a4bdbacae32b27ac65)
+  and [Part2](https://github.com/wekan/wekan/commit/aaecac091209e90c0c2123830728f5e7a835ccb4).
+  For example: sudo snap set wekan scrollinertia='200' , sudo snap set wekan scrollamount='200' .
+  Thanks to danger89 and xet7.
+
+and adds the following updates:
+
+- [Upgrade to Meteor 1.10.1](https://github.com/wekan/wekan/commit/e16c65babc1f021c35a3d46bc61e649ec94d1e82).
+  Thanks to xet7.
+- [Update markdown](https://github.com/wekan/wekan/commit/6e0fa78022ea487176eb0a32ec5a4a441f8e0c3c).
+  Thanks to xet7.
+- [Update minimist](https://github.com/wekan/wekan/commit/ea6baa5c2b956ee28b0a7e63f988e2fc1998201a).
+  Thanks to xet7.
+- [Update acorn](https://github.com/wekan/wekan/commit/369a29707bbec3bf89717c16e8b698fb4666087a).
+  Thanks to xet7.
+- [Update prettier-eslint](https://github.com/wekan/wekan/commit/8183b7bdaa01d2ce53ac7215beafd5efe21373e8).
+  Thanks to xet7.
+- [Update ostrio:cookies](https://github.com/wekan/wekan/commit/14b8610837117616d436e2bac6a9dc653e315662).
+  Thanks to xet7.
+- [Add build time profiling to build script](https://github.com/wekan/wekan/commit/f968109e7390139e50375ee29bc7bc3cf1e1ab41).
+  Thanks to zodern.
+
+and fixes the following bugs:
+
+- [Downgrade stylus to v1.1.0 to speed up building Wekan](https://github.com/wekan/wekan/commit/fca4cdcebf1cc6642aefeb78b911cb5b95ebe473).
+  This is because building newer stylus v2 takes 52 minutes. After this change, building Wekan takes 3 minutes.
+  Thanks to zodern.
+- [Fix: Error when retrieve token from some OIDC due to not necessary scope
+  parameter](https://github.com/wekan/wekan/pull/2955).
+  Thanks to benoitm76.
+- [Fix: img tag did not allow width and height. Removed swipebox from markdown editor
+  img tag and updated marked markdown to newest version](https://github.com/wekan/wekan/commit/2b26bbe78a1a2b8b427963a6c44c3853efdb737e).
+  Thanks to hradec and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.83 2020-03-01 Wekan release
+
+This release tries to revert remaining the following changes:
+
+- [Revert](https://github.com/wekan/wekan/88573ad2cdb8596b795a82ef40a0662180e8a7d7) change made at Wekan v3.81,
+  because building did not work: [Try to make Meteor build time shorter
+  by excluding legacy and cordova. This was made possible by
+  Meteor 1.10-rc.2](https://github.com/wekan/wekan/commit/0d3002f69d97e646fa7368bfdade4f78c51e9884).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.82 2020-03-01 Wekan release
+
+This release reverts the following changes:
+
+- Revert change made at Wekan v3.81, because building did not work: [Try to make Meteor build time shorter
+  by excluding legacy and cordova. This was made possible by
+  Meteor 1.10-rc.2](https://github.com/wekan/wekan/commit/0d3002f69d97e646fa7368bfdade4f78c51e9884).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.81 2020-03-01 Wekan release
+
+This release [fixes](https://github.com/wekan/wekan/commit/aac7c380c8c389b0683b2bd64e2cc856993f0e30) the following CRITICAL SECURITY VULNERABILITIES and other bugs:
+
+- Fix critical and moderate security vulnerabilities reported at 2020-02-26 with
+  responsible disclosure by [Dejan Zelic](https://twitter.com/dejandayoff),
+  Justin Benjamin and others at [Offensive Security](https://twitter.com/offsectraining),
+  that follow standard 90 days before public disclosure.
+  Thanks to xet7.
+- Fix webhook error that prevented some card etc deleting from web UI of board.
+  Thanks to xet7.
+- Add missing Font Awesome icon to Board Settings Menu.
+  Thanks to xet7.
+- Remove autofocus from many form input boxes so that they would not cause warnings.
+  Thanks to xet7.
+
+and does the following upgrades:
+
+- [Upgrade Meteor to 1.10-rc.2](https://github.com/wekan/wekan/commit/26b521e86e6ac40b7ba25bbe8dac7bf4d48d43ce).
+  Thanks to xet7.
+- [Try to make Meteor build time shorter by excluding legacy and cordova. This was made possible by
+  Meteor 1.10-rc.2](https://github.com/wekan/wekan/commit/0d3002f69d97e646fa7368bfdade4f78c51e9884).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Try to fix afterwards loading of cards by adding fallback when requestIdleCallback is not
+  available](https://github.com/wekan/wekan/commit/2b9540ce02de604bf84ea082f2dcb1d01673708c).
+  Thanks to xet7.
+- [Make profile.initials available in publications](https://github.com/wekan/wekan/pull/2948).
+  Thanks to NicoP-S.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.80 2020-02-22 Wekan release
+
+This release adds the following features:
+
+- [Create New User in Admin Panel](https://github.com/wekan/wekan/commit/e0ca960a35cf006880019ba28fc82aa30f289a71).
+  Works, but does not save fullname yet, so currently it's needed to edit add fullname later.
+  Thanks to xet7.
+
+and adds the following updates:
+
+- [Update to Meteor 1.9.1, Node 12.16.1 etc newest dependencies](https://github.com/wekan/wekan/commit/cbbb5deff7d84a91c40becc9caaf70f5b6738b63).
+  Thanks to xet7.
+- [Update to Meteor 1.9.2](https://github.com/wekan/wekan/commit/9be3f3714ae680ff9fc1855c960c9831e84c2b07).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Update Sandstorm release build script](https://github.com/wekan/wekan/commit/a4ff6cc0af8545ca4d3e97fa2cabbe7981c025b2).
+  Thanks to xet7.
+- [Fix docker-compose link](https://github.com/wekan/wekan/pull/2937).
+  Thanks to pbek.
+- [Remove alethes:pages package, that had some indentation error.
+  Package is about pagination, but I did not find any pagination related code in Wekan
+  yet](https://github.com/wekan/wekan/commit/ec012060305bc16fbf8d2ac218f5c847e02c4301).
+  Thanks to xet7 !
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.79 2020-02-13 Wekan release
+
+This release fixes the following bugs:
+
+- [Fix Card Opened Webhook can not be disabled](https://github.com/wekan/wekan/commit/178f376e2138b5522c2e92ddfd2babb113df8d9f).
+  Thanks to mvanvoorden and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.78 2020-02-12 Wekan release
+
+This release adds the following features:
+
+- [Card Settings / Show on Card: Description Title and Description Text](https://github.com/wekan/wekan/commit/e89965f6422fd95b4ad2112ae407b1dde4853510).
+  Thanks to e-stoniauk, 2020product and xet7.
+
+and fixes the following bugs:
+
+- [Remove card element grouping to create compact card layout](https://github.com/wekan/wekan/commit/e89965f6422fd95b4ad2112ae407b1dde4853510).
+  Thanks to e-stoniauk, 2020product and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.77 2020-02-10 Wekan release
+
+This release removes the following features:
+
+- [Remove hiding comments and activities](https://github.com/wekan/wekan/commit/2a54218f3f68547032bd53a04a968b233be21e15).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- Fix Copy Card Link to Clipboard button at card title did not
+  work [Part 1](https://github.com/wekan/wekan/commit/9a21b0a1c933e7f778e4e57a8258e150ccea1620)
+  and [Part2](https://github.com/wekan/wekan/commit/4467a68b97a3fbf0fbae7f05177d978f2aa80287).
+  Thanks to 2020product and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.76 2020-02-07 Wekan release
+
+This release adds the following updates:
+
+- [Use Meteor 1.9 and Node.js 12.15.0 on Snap and Docker](https://github.com/wekan/wekan/commit/8384d68a060ef8f2c202ce2fa6064c5c823d28dc).
+  This also fixes bug that exporting some boards was not possible, downloading export file failed.
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Fix Bug enable/disable Comments in Card Settings](https://github.com/wekan/wekan/issues/2923).
+  Thanks to warnt, mdurokov and xet7.
+- [Try to disable dragging Swimlanes/Lists/Cards/Checklists/Subtasks on small mobile smartphones webbrowsers,
+  and hide drag handles on mobile web](https://github.com/wekan/wekan/commit/bf78b093bad7d463ee325ad96e8b485264d4a3be).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.75 2020-02-05 Wekan release
+
+This release adds the following new features:
+
+- [Fix](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16)
+  [adding comments](https://github.com/wekan/wekan/issues/2918).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Added some working layout changes like activities using less space from https://github.com/wekan/wekan/pull/2920](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16).
+  Thanks to 2020product.
+- [Fixed Card Settings not working at Sandstorm](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16).
+  Thanks to xet7.
+- Add [Card Description title](https://github.com/wekan/wekan/issues/2918#issuecomment-582346577)
+  [back](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.74 2020-02-05 Wekan release
+
+This release adds the following new features:
+
+- [For BoardAdmin, add way to hide parts of a card, at Board Settings/Card Settings/Show on Card: Received, Start, ... etc.
+  Add to card title bar Copy card to Clipboard button](https://github.com/wekan/wekan/pull/2915).
+  Thanks to 2020product and xet7.
+- [Set default to RICHER_CARD_COMMENT_EDITOR=false](https://github.com/wekan/wekan/commit/65fa2f626f503b8089e0d982901cffb3990426cb).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.73 2020-01-29 Wekan release
+
+This release adds the following new features:
+
+- [Login to Wekan with Nextcloud](https://github.com/wekan/wekan/pull/2897).
+  Thanks to bogie.
+- [Add rule action to move cards to other boards](https://github.com/wekan/wekan/pull/2899).
+  Thanks to peterverraedt.
+
+and fixes the following bugs:
+
+- [Show System Wide Announcement in one line](https://github.com/wekan/wekan/pull/2891).
+  Thanks to tsia.
+- [Fixed board export with attachment in Wekan Meteor 1.9.x version](https://github.com/wekan/wekan/pull/2898).
+  Thanks to izadpoor.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.72 2020-01-19 Sandstorm-only Wekan release
+
+This release fixes the following bugs:
+
+- Try to fix Wekan at Sandstorm.
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.71 2020-01-18 Sandstorm-only Wekan release
+
+This release fixes the following bugs:
+
+- [Try to fix Wekan at Sandstorm by using Meteor 1.8.x and Node 8.17.0 at Sandstorm](https://github.com/wekan/wekan/commit/5e5ab95410c715a4379631456fc5547c497898b0).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.70 2020-01-18 Wekan release
+
+This release fixes the following bugs:
+
+- [Add missing LD_LIBRARY_PATH to use libssl and libcurl](https://github.com/wekan/wekan/10f142a1a05acb98a175ccb0326fb0c1d3e3713f).
+  Thanks to xet7.
+- [Use Meteor 1.8.x](https://github.com/wekan/wekan/commit/55a2aa90cbbf44200e9b0b9f4bd08b6177f1bb95)
+  [on Snap](https://github.com/wekan/wekan/commit/6a01170d8696322462c4065ce0cf4a637a058975), because
+  Snap builds do not work yet for Meteor 1.9, Node 12.14.1 and MongoDB 4.2.2.
+  Docker version works with Meteor 1.9.
+  Thanks to xet7.
+- [Try to fix Node 12 Buffer() deprecation errors](https://github.com/wekan/wekan/commit/9b905c2833d54cf34d1875148075b2bf756d943a).
+  Thanks to xet7.
+- [Add Snap Meteor 1.8.x files to lint ignore files](https://github.com/wekan/wekan/commit/48f8050c25e40f737dfdd3a98923cb87cd4e77e2).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.69 2020-01-10 Wekan release
+
+This release fixes the following bugs:
+
+- [Fix docker-compose.yml to not use --smallfiles that is not supported in
+  MongoDB 4.x](https://github.com/wekan/wekan/commit/ecb76842fcbd81701afcab8db0ed106e6be0fdec).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.68 2020-01-10 Wekan release
+
+This release tries to fix the following bugs:
+
+- [Try to fix Snap by removing MongoDB option --smallfiles that is not supported
+  in MongoDB 4.x](https://github.com/wekan/wekan/commit/031df54a2e0a03dcb7a2586667e60e5bd4eef706)
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.67 2020-01-10 Wekan release
+
+This release tries to fix the following bugs:
+
+- [Try to fix Snap](https://github.com/wekan/wekan/commit/2b382b940be9af575fab4c2e955702d8cde55ae9).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.66 2020-01-10 Wekan release
+
+This release tries to fix the following bugs:
+
+- [Try to fix Snap](https://github.com/wekan/wekan/commit/39bf1e375e2962f824e6f8cfa425ea51aa4efa24).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.65 2020-01-10 Wekan release
+
+This release adds the following features:
+
+- [More keyboard shortcuts: c for archive card](https://github.com/wekan/wekan/commit/d16a601c04aeb1d3550c5c541be02a67276a34cf).
+  Thanks to xet7.
+
+and adds the following updates:
+
+- [Upgrade to Meteor 1.9, Node 12.14.1 and MongoDB 4.2.2](https://github.com/wekan/wekan/commit/785f3cf88b61f687ef5ad4a529768221d1a54c86).
+  Thanks to xet7.
+- [Add more issue repo links to GitHub issue template](https://github.com/wekan/wekan/commit/5724674e73246f4e52843a6d6906c0ecdd85cccc).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.64 2020-01-06 Wekan release
+
+This release adds the following warning for CentOS 7 users:
+
+- [WARNING: DO NOT USE SNAP ON CENTOS 7, THERE IS UPDATE BUG](https://github.com/wekan/wekan-snap/wiki/CentOS-7).
+  Thanks to andy-twosticks and xet7.
+
+and adds the following features:
+
+- [Wider sidebar](https://github.com/wekan/wekan/commit/5058233509e44916296e38fb8a6c5dd591c46d8b).
+  Thanks to vjrj.
+
+and removes the following features:
+
+- [Removed Custom HTML feature that does not work](https://github.com/wekan/wekan/commit/ddce0ada094e6450be260b4cda21fdfa09ae0133).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.63 2020-01-06 Wekan release
+
+This release fixes the following bugs:
+
+- [Fix: Unable to find Archive Card/List/Swimlane in board
+  settings](https://github.com/wekan/wekan/commit/8ce993921718f3e10c2daa5fabb145b939d789dd).
+  Thanks to neobradley and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.62 2020-01-05 Wekan release
+
+This release adds the following features:
+
+- [Add Worker role](https://github.com/wekan/wekan/issues/2788).
+  This was originally added at Wekan v3.58, reverted at Wekan v3.60 because of bugs,
+  and now after fixes added back.
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.61 2020-01-03 Wekan release
+
+This release adds the following features:
+
+- [Add more Font Awesome icons. This was originally added
+  at Wekan v3.58, removed at Wekan v3.60, and now
+  added back at Wekan v3.61](https://github.com/wekan/wekan/commit/cd253522a305523e3e36bb73313e8c4db500a717).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Fix browser javascript console errors when editing profile. This was originally added
+  at Wekan v3.58, removed at Wekan v3.60, and now added back at
+  Wekan v3.61](https://github.com/wekan/wekan/commit/cd253522a305523e3e36bb73313e8c4db500a717).
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.60 2020-01-03 Wekan release
+
+This release fixes the following bugs:
+
+- [Revert to Wekan v3.57 version of client and models directories,
+  removing Worker role temporarily, because Worker role changes
+  broke saving card](https://github.com/wekan/wekan/commit/27943796ade78ca3c503637a1340918bf06a1267).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.59 2020-01-03 Wekan release
+
+This release fixes the following bugs:
+
+- [Fix not being able to edit received date](https://github.com/wekan/wekan/commit/5376bc7b7905c0dd99fae1aeae3f63b4583a3e3f).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v3.58 2020-01-03 Wekan release
+
+This release adds the following features:
+
+- [Add Worker role](https://github.com/wekan/wekan/issues/2788). Thanks to xet7.
+- [Add more Font Awesome icons](https://github.com/wekan/wekan/commit/2bf004120d5a43cd3c3c060fc7c0c30d1b01f220).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Fix: k8s templates update for helm](https://github.com/wekan/wekan/pull/2867).
+  1. Upgrade mongo replica version.
+  2. Access mongo via service url.
+  3. Change the expose servicePort to numeric.
+  Thanks to jiangytcn.
+- [Fix browser console errors when editing user profile name](https://github.com/wekan/wekan/commit/2bf004120d5a43cd3c3c060fc7c0c30d1b01f220).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
 # v3.57 2019-12-22 Wekan release
 # v3.57 2019-12-22 Wekan release
 
 
 This release adds the following features:
 This release adds the following features:

+ 12 - 4
Dockerfile

@@ -4,10 +4,12 @@ LABEL maintainer="wekan"
 # Set the environment variables (defaults where required)
 # Set the environment variables (defaults where required)
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # ENV BUILD_DEPS="paxctl"
 # ENV BUILD_DEPS="paxctl"
+ARG DEBIAN_FRONTEND=noninteractive
+
 ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
 ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
     DEBUG=false \
     DEBUG=false \
-    NODE_VERSION=v8.17.0 \
-    METEOR_RELEASE=1.8.1 \
+    NODE_VERSION=v12.16.3 \
+    METEOR_RELEASE=1.10.2 \
     USE_EDGE=false \
     USE_EDGE=false \
     METEOR_EDGE=1.5-beta.17 \
     METEOR_EDGE=1.5-beta.17 \
     NPM_VERSION=latest \
     NPM_VERSION=latest \
@@ -21,11 +23,12 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
-    RICHER_CARD_COMMENT_EDITOR=true \
+    RICHER_CARD_COMMENT_EDITOR=false \
     CARD_OPENED_WEBHOOK_ENABLED=false \
     CARD_OPENED_WEBHOOK_ENABLED=false \
     ATTACHMENTS_STORE_PATH="" \
     ATTACHMENTS_STORE_PATH="" \
     MAX_IMAGE_PIXEL="" \
     MAX_IMAGE_PIXEL="" \
     IMAGE_COMPRESS_RATIO="" \
     IMAGE_COMPRESS_RATIO="" \
+    NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
     BIGEVENTS_PATTERN=NONE \
     BIGEVENTS_PATTERN=NONE \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     NOTIFY_DUE_AT_HOUR_OF_DAY="" \
     NOTIFY_DUE_AT_HOUR_OF_DAY="" \
@@ -110,7 +113,10 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     CORS="" \
     CORS="" \
     CORS_ALLOW_HEADERS="" \
     CORS_ALLOW_HEADERS="" \
     CORS_EXPOSE_HEADERS="" \
     CORS_EXPOSE_HEADERS="" \
-    DEFAULT_AUTHENTICATION_METHOD=""
+    DEFAULT_AUTHENTICATION_METHOD="" \
+    SCROLLINERTIA="0" \
+    SCROLLAMOUNT="auto" \
+    PASSWORD_LOGIN_ENABLED=true
 
 
 # Copy the app to the image
 # Copy the app to the image
 COPY ${SRC_PATH} /home/wekan/app
 COPY ${SRC_PATH} /home/wekan/app
@@ -267,6 +273,8 @@ RUN \
     cd /home/wekan/app_build/bundle/programs/server/ && \
     cd /home/wekan/app_build/bundle/programs/server/ && \
     gosu wekan:wekan npm install && \
     gosu wekan:wekan npm install && \
     #gosu wekan:wekan npm install bcrypt && \
     #gosu wekan:wekan npm install bcrypt && \
+    # Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
+		rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy && \
     mv /home/wekan/app_build/bundle /build && \
     mv /home/wekan/app_build/bundle /build && \
     \
     \
     # Put back the original tar
     # Put back the original tar

+ 77 - 0
Dockerfile.arm64v8

@@ -0,0 +1,77 @@
+FROM amd64/alpine:3.7 AS builder
+
+# Set the environment variables for builder
+ENV QEMU_VERSION=v4.2.0-6 \
+    QEMU_ARCHITECTURE=aarch64 \
+    NODE_ARCHITECTURE=linux-arm64 \
+    NODE_VERSION=v12.16.3 \
+    WEKAN_VERSION=3.96  \
+    WEKAN_ARCHITECTURE=arm64
+
+     # Install dependencies
+RUN  apk update && apk add ca-certificates outils-sha1 && \
+     \
+     # Download qemu static for our architecture
+     wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
+     \
+    # Download wekan and shasum
+    wget https://releases.wekan.team/raspi3/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
+    wget https://releases.wekan.team/raspi3/SHA256SUMS.txt && \
+    # Verify wekan
+    grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
+    \
+    # Unzip wekan
+    unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
+    \
+    # Download node and shasums
+    wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
+    wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
+    \
+    # Verify nodejs authenticity
+    grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt.asc | sha256sum -c - && \
+    \
+    # Extract node and remove tar.gz
+    tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
+
+# Build wekan dockerfile
+FROM arm64v8/ubuntu:19.10
+LABEL maintainer="wekan"
+
+# Set the environment variables (defaults where required)
+ENV QEMU_ARCHITECTURE=aarch64 \
+    NODE_ARCHITECTURE=linux-arm64 \
+    NODE_VERSION=v12.16.1 \
+    NODE_ENV=production \
+    NPM_VERSION=latest \
+    WITH_API=true \
+    PORT=8080 \
+    ROOT_URL=http://localhost \
+    MONGO_URL=mongodb://127.0.0.1:27017/wekan
+
+# Copy qemu-static to image
+COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
+
+# Copy the app to the image
+COPY --from=builder bundle /home/wekan/bundle
+
+# Copy
+COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
+
+RUN \
+    set -o xtrace && \
+    # Add non-root user wekan
+    useradd --user-group --system --home-dir /home/wekan wekan && \
+    \
+    # Install Node
+    ln -s /opt/nodejs/bin/node /usr/bin/node && \
+    ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
+    mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
+    chown wekan --recursive /home/wekan/.config && \
+    \
+    # Install Node dependencies
+    npm install -g npm@${NPM_VERSION}
+
+EXPOSE $PORT
+USER wekan
+
+CMD ["node", "/home/wekan/bundle/main.js"]

+ 4 - 2
README.md

@@ -1,3 +1,5 @@
+[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/wekan/wekan) 
+
 # Wekan - Open Source kanban
 # Wekan - Open Source kanban
 
 
 [![Contributors](https://img.shields.io/github/contributors/wekan/wekan.svg "Contributors")](https://github.com/wekan/wekan/graphs/contributors)
 [![Contributors](https://img.shields.io/github/contributors/wekan/wekan.svg "Contributors")](https://github.com/wekan/wekan/graphs/contributors)
@@ -32,7 +34,7 @@ and PWA app that can be added as icon on Android and bookmark on iOS, used like
 
 
 **NOTE**: 
 **NOTE**: 
 - Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
 - 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 :)
+- Please don't feed the [trolls](https://github.com/wekan/wekan/wiki/FAQ#why-am-i-called-a-troll) and [spammers](https://github.com/wekan/wekan/wiki/FAQ#why-am-i-called-a-spammer) that are mentioned in the FAQ :)
 
 
 ## About Wekan
 ## About Wekan
 
 
@@ -61,7 +63,7 @@ that by providing one-click installation on various platforms.
   [Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows).
   [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), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
   [More Platforms](https://github.com/wekan/wekan/wiki/Platforms), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
 - 1 GB RAM minimum free for Wekan. Production server should have minimum total 4 GB RAM.
 - 1 GB RAM minimum free for Wekan. Production server should have minimum 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,
+  For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/master/docker-compose.yml): 3 frontend servers,
   each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.  
   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.
 - 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.
 - SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.

+ 1 - 1
Stackerfile.yml

@@ -1,5 +1,5 @@
 appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
 appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
-appVersion: "v3.57.0"
+appVersion: "v4.01.0"
 files:
 files:
   userUploads:
   userUploads:
     - README.md
     - README.md

+ 6 - 0
client/00-startup.js

@@ -0,0 +1,6 @@
+// PWA
+if ('serviceWorker' in navigator) {
+  window.addEventListener('load', function() {
+    navigator.serviceWorker.register('/pwa-service-worker.js');
+  });
+}

+ 157 - 190
client/components/activities/activities.jade

@@ -8,234 +8,201 @@ template(name="activities")
       +cardActivities
       +cardActivities
 
 
 template(name="boardActivities")
 template(name="boardActivities")
-  each currentBoard.activities
-    .activity
-      +userAvatar(userId=user._id)
-      p.activity-desc
-        +memberName(user=user)
+  each activityData in currentBoard.activities
+    +activity(activity=activityData card=card mode=mode)
 
 
-        if($eq activityType 'deleteAttachment')
-          | {{{_ 'activity-delete-attach' cardLink}}}.
+template(name="cardActivities")
+  each activityData in currentCard.activities
+    +activity(activity=activityData card=card mode=mode)
+
+template(name="activity")
+  .activity
+    +userAvatar(userId=activity.user._id)
+    p.activity-desc
+      +memberName(user=activity.user)
+
+      //- attachment activity -------------------------------------------------
+      if($eq activity.activityType 'deleteAttachment')
+        | {{{_ 'activity-delete-attach' cardLink}}}.
+
+      if($eq activity.activityType 'addAttachment')
+        | {{{_ 'activity-attached' attachmentLink cardLink}}}.
+        if($neq mode 'board')
+          if activity.attachment.isImage
+            img.attachment-image-preview(src=activity.attachment.url)
+
+      //- board activity ------------------------------------------------------
+      if($eq mode 'board')
+        if($eq activity.activityType 'createBoard')
+          | {{_ 'activity-created' boardLabel}}.
 
 
-        if($eq activityType 'addAttachment')
-          | {{{_ 'activity-attached' attachmentLink cardLink}}}.
+        if($eq activity.activityType 'importBoard')
+          | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
 
 
-        if($eq activityType 'addBoardMember')
+        if($eq activity.activityType 'addBoardMember')
           | {{{_ 'activity-added' memberLink boardLabel}}}.
           | {{{_ 'activity-added' memberLink boardLabel}}}.
 
 
-        if($eq activityType 'addComment')
-          | {{{_ 'activity-on' cardLink}}}
-          a.activity-comment(href="{{ card.absoluteUrl }}")
-            +viewer
-              = comment.text
-
-        if($eq activityType 'addChecklist')
-          | {{{_ 'activity-checklist-added' cardLink}}}.
-          .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 activity.activityType 'removeBoardMember')
+          | {{{_ 'activity-excluded' memberLink boardLabel}}}.
 
 
-        if($eq activityType 'checklistUncompleted')
-          | {{{_ 'activity-checklist-uncompleted' checklist.title cardLink}}}.
+      //- card activity -------------------------------------------------------
+      if($eq activity.activityType 'createCard')
+        if($eq mode 'card')
+          | {{{_ 'activity-added' cardLabel activity.listName}}}.
+        else
+          | {{{_ 'activity-added' cardLabel boardLabel}}}.
 
 
-        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 activity.activityType 'importCard')
+        | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
 
 
-        if($eq activityType 'archivedCard')
-          | {{{_ 'activity-archived' cardLink}}}.
+      if($eq activity.activityType 'moveCard')
+        | {{{_ 'activity-moved' cardLabel activity.oldList.title activity.list.title}}}.
 
 
-        if($eq activityType 'archivedList')
-          | {{_ 'activity-archived' list.title}}.
+      if($eq activity.activityType 'moveCardBoard')
+        | {{{_ 'activity-moved' cardLink activity.oldBoardName activity.boardName}}}.
 
 
-        if($eq activityType 'archivedSwimlane')
-          | {{_ 'activity-archived' swimlane.title}}.
+      if($eq activity.activityType 'archivedCard')
+        | {{{_ 'activity-archived' cardLink}}}.
 
 
-        if($eq activityType 'createBoard')
-          | {{_ 'activity-created' boardLabel}}.
+      if($eq activity.activityType 'restoredCard')
+        | {{{_ 'activity-sent' cardLink boardLabel}}}.
 
 
-        if($eq activityType 'createCard')
-          | {{{_ 'activity-added' cardLink boardLabel}}}.
+      //- checklist activity --------------------------------------------------
+      if($eq activity.activityType 'addChecklist')
+        | {{{_ 'activity-checklist-added' cardLink}}}.
+        if($eq mode 'card')
+          .activity-checklist
+            +viewer
+              = activity.checklist.title
+        else
+          a.activity-checklist(href="{{ activity.card.absoluteUrl }}")
+            +viewer
+              = activity.checklist.title
 
 
-        if($eq activityType 'createCustomField')
-          | {{_ 'activity-customfield-created' customField}}.
+      if($eq activity.activityType 'removedChecklist')
+        | {{{_ 'activity-checklist-removed' cardLink}}}.
 
 
-        if($eq activityType 'createList')
-          | {{_ 'activity-added' list.title boardLabel}}.
+      if($eq activity.activityType 'completeChecklist')
+        | {{{_ 'activity-checklist-completed' activity.checklist.title cardLink}}}.
 
 
-        if($eq activityType 'createSwimlane')
-          | {{_ 'activity-added' swimlane.title boardLabel}}.
+      if($eq activity.activityType 'uncompleteChecklist')
+        | {{{_ 'activity-checklist-uncompleted' activity.checklist.title cardLink}}}.
 
 
-        if($eq activityType 'removeList')
-          | {{_ 'activity-removed' title boardLabel}}.
+      if($eq activity.activityType 'checkedItem')
+        | {{{_ 'activity-checked-item' checkItem activity.checklist.title cardLink}}}.
 
 
-        if($eq activityType 'importBoard')
-          | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
+      if($eq activity.activityType 'uncheckedItem')
+        | {{{_ 'activity-unchecked-item' checkItem activity.checklist.title cardLink}}}.
 
 
-        if($eq activityType 'importCard')
-          | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
+      if($eq activity.activityType 'addChecklistItem')
+        | {{{_ 'activity-checklist-item-added' activity.checklist.title cardLink}}}.
+        .activity-checklist(href="{{ activity.card.absoluteUrl }}")
+          +viewer
+            = activity.checklistItem.title
 
 
-        if($eq activityType 'importList')
-          | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
+      if($eq activity.activityType 'removedChecklistItem')
+        | {{{_ 'activity-checklist-item-removed' activity.checklist.title cardLink}}}.
 
 
-        if($eq activityType 'joinMember')
-          if($eq user._id member._id)
-            | {{{_ 'activity-joined' cardLink}}}.
+      //- comment activity ----------------------------------------------------
+      if($eq mode 'card')
+        //- if we are in card mode we display the comment in a way that it
+        //- can be edited by the owner
+        if($eq activity.activityType 'addComment')
+          +inlinedForm(classNames='js-edit-comment')
+            +editor(autofocus=true)
+              = activity.comment.text
+            .edit-controls
+              button.primary(type="submit") {{_ 'edit'}}
           else
           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}}}.
-
-        if($eq activityType 'removeBoardMember')
-          | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+            .activity-comment
+              +viewer
+                = activity.comment.text
+            span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
+              if ($eq currentUser._id activity.comment.userId)
+                = ' - '
+                a.js-open-inlined-form {{_ "edit"}}
+                = ' - '
+                a.js-delete-comment {{_ "delete"}}
 
 
-        if($eq activityType 'restoredCard')
-          | {{{_ 'activity-sent' cardLink boardLabel}}}.
+        if($eq activity.activityType 'deleteComment')
+          | {{{_ 'activity-deleteComment' currentData.commentId}}}.
 
 
-        if($eq activityType 'addedLabel')
-          | {{{_ 'activity-added-label' lastLabel cardLink}}}.
+        if($eq activity.activityType 'editComment')
+          | {{{_ 'activity-editComment' currentData.commentId}}}.
+      else
+        //- if we are not in card mode we only display a summary of the comment
+        if($eq activity.activityType 'addComment')
+          | {{{_ 'activity-on' cardLink}}}
+          a.activity-comment(href="{{ activity.card.absoluteUrl }}")
+            +viewer
+              = activity.comment.text
 
 
-        if($eq activityType 'removedLabel')
-          | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
+      //- customField activity ------------------------------------------------
+      if($eq mode 'board')
+        if($eq activity.activityType 'createCustomField')
+          | {{_ 'activity-customfield-created' customField}}.
 
 
-        if($eq activityType 'setCustomField')
+        if($eq activity.activityType 'setCustomField')
           | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
           | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
 
 
-        if($eq activityType 'unsetCustomField')
+        if($eq activity.activityType 'unsetCustomField')
           | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
           | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
 
 
-        if($eq activityType 'unjoinMember')
-          if($eq user._id member._id)
-            | {{{_ 'activity-unjoined' cardLink}}}.
-          else
-            | {{{_ 'activity-removed' memberLink cardLink}}}.
+      //- label activity ------------------------------------------------------
+      if($eq activity.activityType 'addedLabel')
+        | {{{_ 'activity-added-label' lastLabel cardLink}}}.
 
 
-        span(title=createdAt).activity-meta {{ moment createdAt }}
+      if($eq activity.activityType 'removedLabel')
+        | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
 
 
-template(name="cardActivities")
-  each currentCard.activities
-    .activity
-      +userAvatar(userId=user._id)
-      p.activity-desc
-        +memberName(user=user)
-        if($eq activityType 'createCard')
-          | {{_ 'activity-added' cardLabel listName}}.
-        if($eq activityType 'importCard')
-          | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
-        if($eq activityType 'joinMember')
-          if($eq user._id member._id)
-            | {{_ 'activity-joined' cardLabel}}.
-          else
-            | {{{_ 'activity-added' memberLink cardLabel}}}.
-        if($eq activityType 'unjoinMember')
-          if($eq user._id member._id)
-            | {{_ 'activity-unjoined' cardLabel}}.
-          else
-            | {{{_ 'activity-removed' cardLabel memberLink}}}.
-        if($eq activityType 'archivedCard')
-          | {{_ 'activity-archived' cardLabel}}.
+      //- list activity -------------------------------------------------------
+      if($neq mode 'card')
+        if($eq activity.activityType 'createList')
+          | {{{_ 'activity-added' listLabel boardLabel}}}.
 
 
-        if($eq activityType 'addedLabel')
-          | {{{_ 'activity-added-label-card' lastLabel }}}.
-
-        if($eq activityType 'removedLabel')
-          | {{{_ 'activity-removed-label-card' lastLabel }}}.
+        if($eq activity.activityType 'importList')
+          | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
 
 
-        if($eq activityType 'removeChecklist')
-          | {{{_ 'activity-checklist-removed' cardLabel}}}.
+        if($eq activity.activityType 'removeList')
+          | {{{_ 'activity-removed' activity.title boardLabel}}}.
 
 
-        if($eq activityType 'checkedItem')
-          | {{{_ 'activity-checked-item-card' checkItem checklist.title }}}.
+        if($eq activity.activityType 'archivedList')
+          | {{_ 'activity-archived' listLabel}}.
 
 
-        if($eq activityType 'uncheckedItem')
-          | {{{_ 'activity-unchecked-item-card' checkItem checklist.title }}}.
+      //- member activity ----------------------------------------------------
+      if($eq activity.activityType 'joinMember')
+        if($eq user._id activity.member._id)
+          | {{{_ 'activity-joined' cardLink}}}.
+        else
+          | {{{_ 'activity-added' memberLink cardLink}}}.
 
 
-        if($eq activityType 'checklistCompleted')
-          | {{{_ 'activity-checklist-completed-card' checklist.title }}}.
+      if($eq activity.activityType 'unjoinMember')
+        if($eq user._id activity.member._id)
+          | {{{_ 'activity-unjoined' cardLink}}}.
+        else
+          | {{{_ 'activity-removed' memberLink cardLink}}}.
 
 
-        if($eq activityType 'checklistUncompleted')
-          | {{{_ 'activity-checklist-uncompleted-card' checklist.title }}}.
+      //- swimlane activity --------------------------------------------------
+      if($neq mode 'card')
+        if($eq activity.activityType 'createSwimlane')
+          | {{{_ 'activity-added' activity.swimlane.title boardLabel}}}.
 
 
-        if($eq activityType 'restoredCard')
-          | {{_ 'activity-sent' cardLabel boardLabel}}.
-        if($eq activityType 'moveCard')
-          | {{_ 'activity-moved' cardLabel oldList.title list.title}}.
+        if($eq activity.activityType 'archivedSwimlane')
+          | {{_ 'activity-archived' activity.swimlane.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
-            +viewer
-              = checklist.title
-        if($eq activityType 'addChecklistItem')
-          | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
-          .activity-checklist(href="{{ card.absoluteUrl }}")
-            +viewer
-              = checklistItem.title
-        
-        if(currentData.timeKey)
-          | {{{_ activityType }}}
+      //- I don't understand this part ----------------------------------------
+      if(currentData.timeKey)
+        | {{{_ activity.activityType }}}
+        = ' '
+        i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
+        if (currentData.timeOldValue)
           = ' '
           = ' '
-          i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
-          if (currentData.timeOldValue)
-              = ' '                                                                                    
-              | {{{_ "previous_as" }}}
-              = ' '
-              i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
-          = ' @'
-        else if(currentData.timeValue)
-          | {{{_ activityType currentData.timeValue}}}
-        
-
-        if($eq activityType 'deleteComment')
-          | {{{_ 'activity-deleteComment' currentData.commentId}}}.
-        if($eq activityType 'editComment')
-          | {{{_ 'activity-editComment' currentData.commentId}}}.
-        if($eq activityType 'addComment')
-          +inlinedForm(classNames='js-edit-comment')
-            +editor(autofocus=true)
-              = comment.text
-            .edit-controls
-              button.primary(type="submit") {{_ 'edit'}}
-          else
-            .activity-comment
-              +viewer
-                = comment.text
-            span(title=createdAt).activity-meta {{ moment createdAt }}
-              if ($eq currentUser._id comment.userId)
-                = ' - '
-                a.js-open-inlined-form {{_ "edit"}}
-                = ' - '
-                a.js-delete-comment {{_ "delete"}}
-
-        else
-          span(title=createdAt).activity-meta {{ moment createdAt }}
+            | {{{_ "previous_as" }}}
+            = ' '
+            i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
+        = ' @'
+      else if(currentData.timeValue)
+        | {{{_ activity.activityType currentData.timeValue}}}
+
+      span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}

+ 74 - 41
client/components/activities/activities.js

@@ -41,7 +41,9 @@ BlazeComponent.extendComponent({
       });
       });
     });
     });
   },
   },
+}).register('activities');
 
 
+BlazeComponent.extendComponent({
   loadNextPage() {
   loadNextPage() {
     if (this.loadNextPageLocked === false) {
     if (this.loadNextPageLocked === false) {
       this.page.set(this.page.get() + 1);
       this.page.set(this.page.get() + 1);
@@ -50,41 +52,37 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   checkItem() {
   checkItem() {
-    const checkItemId = this.currentData().checklistItemId;
+    const checkItemId = this.currentData().activity.checklistItemId;
     const checkItem = ChecklistItems.findOne({ _id: checkItemId });
     const checkItem = ChecklistItems.findOne({ _id: checkItemId });
-    return checkItem.title;
+    return checkItem && checkItem.title;
   },
   },
 
 
   boardLabel() {
   boardLabel() {
+    const data = this.currentData();
+    if (data.mode !== 'board') {
+      return createBoardLink(data.activity.board(), data.activity.listName);
+    }
     return TAPi18n.__('this-board');
     return TAPi18n.__('this-board');
   },
   },
 
 
   cardLabel() {
   cardLabel() {
+    const data = this.currentData();
+    if (data.mode !== 'card') {
+      return createCardLink(this.currentData().activity.card());
+    }
     return TAPi18n.__('this-card');
     return TAPi18n.__('this-card');
   },
   },
 
 
   cardLink() {
   cardLink() {
-    const card = this.currentData().card();
-    return (
-      card &&
-      Blaze.toHTML(
-        HTML.A(
-          {
-            href: card.absoluteUrl(),
-            class: 'action-card',
-          },
-          card.title,
-        ),
-      )
-    );
+    return createCardLink(this.currentData().activity.card());
   },
   },
 
 
   lastLabel() {
   lastLabel() {
-    const lastLabelId = this.currentData().labelId;
+    const lastLabelId = this.currentData().activity.labelId;
     if (!lastLabelId) return null;
     if (!lastLabelId) return null;
-    const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(
-      lastLabelId,
-    );
+    const lastLabel = Boards.findOne(
+      this.currentData().activity.boardId,
+    ).getLabelById(lastLabelId);
     if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
     if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
       return lastLabel.color;
       return lastLabel.color;
     } else {
     } else {
@@ -94,7 +92,7 @@ BlazeComponent.extendComponent({
 
 
   lastCustomField() {
   lastCustomField() {
     const lastCustomField = CustomFields.findOne(
     const lastCustomField = CustomFields.findOne(
-      this.currentData().customFieldId,
+      this.currentData().activity.customFieldId,
     );
     );
     if (!lastCustomField) return null;
     if (!lastCustomField) return null;
     return lastCustomField.name;
     return lastCustomField.name;
@@ -102,10 +100,10 @@ BlazeComponent.extendComponent({
 
 
   lastCustomFieldValue() {
   lastCustomFieldValue() {
     const lastCustomField = CustomFields.findOne(
     const lastCustomField = CustomFields.findOne(
-      this.currentData().customFieldId,
+      this.currentData().activity.customFieldId,
     );
     );
     if (!lastCustomField) return null;
     if (!lastCustomField) return null;
-    const value = this.currentData().value;
+    const value = this.currentData().activity.value;
     if (
     if (
       lastCustomField.settings.dropdownItems &&
       lastCustomField.settings.dropdownItems &&
       lastCustomField.settings.dropdownItems.length > 0
       lastCustomField.settings.dropdownItems.length > 0
@@ -122,11 +120,13 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   listLabel() {
   listLabel() {
-    return this.currentData().list().title;
+    const activity = this.currentData().activity;
+    const list = activity.list();
+    return (list && list.title) || activity.title;
   },
   },
 
 
   sourceLink() {
   sourceLink() {
-    const source = this.currentData().source;
+    const source = this.currentData().activity.source;
     if (source) {
     if (source) {
       if (source.url) {
       if (source.url) {
         return Blaze.toHTML(
         return Blaze.toHTML(
@@ -146,30 +146,31 @@ BlazeComponent.extendComponent({
 
 
   memberLink() {
   memberLink() {
     return Blaze.toHTMLWithData(Template.memberName, {
     return Blaze.toHTMLWithData(Template.memberName, {
-      user: this.currentData().member(),
+      user: this.currentData().activity.member(),
     });
     });
   },
   },
 
 
   attachmentLink() {
   attachmentLink() {
-    const attachment = this.currentData().attachment();
+    const attachment = this.currentData().activity.attachment();
     // trying to display url before file is stored generates js errors
     // trying to display url before file is stored generates js errors
     return (
     return (
-      attachment &&
-      attachment.url({ download: true }) &&
-      Blaze.toHTML(
-        HTML.A(
-          {
-            href: attachment.url({ download: true }),
-            target: '_blank',
-          },
-          attachment.name(),
-        ),
-      )
+      (attachment &&
+        attachment.url({ download: true }) &&
+        Blaze.toHTML(
+          HTML.A(
+            {
+              href: attachment.url({ download: true }),
+              target: '_blank',
+            },
+            attachment.name(),
+          ),
+        )) ||
+      this.currentData().activity.attachmentName
     );
     );
   },
   },
 
 
   customField() {
   customField() {
-    const customField = this.currentData().customField();
+    const customField = this.currentData().activity.customField();
     if (!customField) return null;
     if (!customField) return null;
     return customField.name;
     return customField.name;
   },
   },
@@ -179,7 +180,7 @@ BlazeComponent.extendComponent({
       {
       {
         // XXX We should use Popup.afterConfirmation here
         // XXX We should use Popup.afterConfirmation here
         'click .js-delete-comment'() {
         'click .js-delete-comment'() {
-          const commentId = this.currentData().commentId;
+          const commentId = this.currentData().activity.commentId;
           CardComments.remove(commentId);
           CardComments.remove(commentId);
         },
         },
         'submit .js-edit-comment'(evt) {
         'submit .js-edit-comment'(evt) {
@@ -187,7 +188,7 @@ BlazeComponent.extendComponent({
           const commentText = this.currentComponent()
           const commentText = this.currentComponent()
             .getValue()
             .getValue()
             .trim();
             .trim();
-          const commentId = Template.parentData().commentId;
+          const commentId = Template.parentData().activity.commentId;
           if (commentText) {
           if (commentText) {
             CardComments.update(commentId, {
             CardComments.update(commentId, {
               $set: {
               $set: {
@@ -199,4 +200,36 @@ BlazeComponent.extendComponent({
       },
       },
     ];
     ];
   },
   },
-}).register('activities');
+}).register('activity');
+
+function createCardLink(card) {
+  return (
+    card &&
+    Blaze.toHTML(
+      HTML.A(
+        {
+          href: card.absoluteUrl(),
+          class: 'action-card',
+        },
+        card.title,
+      ),
+    )
+  );
+}
+
+function createBoardLink(board, list) {
+  let text = board.title;
+  if (list) text += `: ${list}`;
+  return (
+    board &&
+    Blaze.toHTML(
+      HTML.A(
+        {
+          href: board.absoluteUrl(),
+          class: 'action-board',
+        },
+        text,
+      ),
+    )
+  );
+}

+ 1 - 1
client/components/activities/activities.styl

@@ -9,7 +9,7 @@
   clear: both
   clear: both
 
 
   .activity
   .activity
-    margin: 10px 0
+    margin: 0.5px 0
     display: flex
     display: flex
 
 
     .member
     .member

+ 20 - 0
client/components/activities/comments.styl

@@ -46,3 +46,23 @@
 
 
     &:is-open
     &:is-open
       cursor: auto
       cursor: auto
+
+.comment-item
+  background-color: #fff
+  border: 0
+  box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
+  color: #8c8c8c
+  height: 36px
+  margin: 4px 4px 6px 0
+  width: 92%
+
+  &:hover
+    background: darken(white, 12%)
+
+  &.add-comment
+    display: flex
+    margin: 5px
+
+    a
+      display: block
+      margin: auto

+ 1 - 1
client/components/boards/boardArchive.js

@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
     return Boards.find(
     return Boards.find(
       { archived: true },
       { archived: true },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
   },
   },

+ 4 - 10
client/components/boards/boardBody.js

@@ -1,7 +1,7 @@
 import { Cookies } from 'meteor/ostrio:cookies';
 import { Cookies } from 'meteor/ostrio:cookies';
 const cookies = new Cookies();
 const cookies = new Cookies();
 const subManager = new SubsManager();
 const subManager = new SubsManager();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
 const swimlaneWhileSortingHeight = 150;
 const swimlaneWhileSortingHeight = 150;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
@@ -191,9 +191,6 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.js-swimlane:not(.placeholder)');
-
     this.autorun(() => {
     this.autorun(() => {
       let showDesktopDragHandles = false;
       let showDesktopDragHandles = false;
       currentUser = Meteor.user();
       currentUser = Meteor.user();
@@ -205,20 +202,17 @@ BlazeComponent.extendComponent({
       } else {
       } else {
         showDesktopDragHandles = false;
         showDesktopDragHandles = false;
       }
       }
-      if (
-        Utils.isMiniScreen() ||
-        (!Utils.isMiniScreen() && showDesktopDragHandles)
-      ) {
+      if (Utils.isMiniScreen() || showDesktopDragHandles) {
         $swimlanesDom.sortable({
         $swimlanesDom.sortable({
           handle: '.js-swimlane-header-handle',
           handle: '.js-swimlane-header-handle',
         });
         });
-      } else {
+      } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
         $swimlanesDom.sortable({
         $swimlanesDom.sortable({
           handle: '.swimlane-header',
           handle: '.swimlane-header',
         });
         });
       }
       }
 
 
-      // Disable drag-dropping if the current user is not a board member or is comment only
+      // Disable drag-dropping if the current user is not a board member
       $swimlanesDom.sortable('option', 'disabled', !userIsMember());
       $swimlanesDom.sortable('option', 'disabled', !userIsMember());
     });
     });
 
 

+ 0 - 14
client/components/boards/boardHeader.jade

@@ -193,20 +193,6 @@ template(name="boardChangeViewPopup")
           | {{_ 'board-view-cal'}}
           | {{_ 'board-view-cal'}}
           if $eq Utils.boardView "board-view-cal"
           if $eq Utils.boardView "board-view-cal"
             i.fa.fa-check
             i.fa.fa-check
-    if currentUser.isAdmin
-      hr
-      li
-        with "board-view-rules"
-          a.js-open-rules-view(title="{{_ 'rules'}}")
-            i.fa.fa-magic
-            | {{_ 'rules'}}
-    else if currentUser.isBoardAdmin
-      hr
-      li
-        with "board-view-rules"
-          a.js-open-rules-view(title="{{_ 'rules'}}")
-            i.fa.fa-magic
-            | {{_ 'rules'}}
 
 
 template(name="createBoard")
 template(name="createBoard")
   form
   form

+ 1 - 20
client/components/boards/boardHeader.js

@@ -30,22 +30,7 @@ Template.boardMenuPopup.events({
   'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
   'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
   'click .js-import-board': Popup.open('chooseBoardSource'),
   'click .js-import-board': Popup.open('chooseBoardSource'),
   'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
   'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
-});
-
-Template.boardMenuPopup.helpers({
-  exportUrl() {
-    const params = {
-      boardId: Session.get('currentBoard'),
-    };
-    const queryParams = {
-      authToken: Accounts._storedLoginToken(),
-    };
-    return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
-  },
-  exportFilename() {
-    const boardId = Session.get('currentBoard');
-    return `wekan-export-board-${boardId}.json`;
-  },
+  'click .js-card-settings': Popup.open('boardCardSettings'),
 });
 });
 
 
 Template.boardChangeTitlePopup.events({
 Template.boardChangeTitlePopup.events({
@@ -190,10 +175,6 @@ Template.boardChangeViewPopup.events({
     Utils.setBoardView('board-view-cal');
     Utils.setBoardView('board-view-cal');
     Popup.close();
     Popup.close();
   },
   },
-  'click .js-open-rules-view'() {
-    Modal.openWide('rulesMain');
-    Popup.close();
-  },
 });
 });
 
 
 const CreateBoard = BlazeComponent.extendComponent({
 const CreateBoard = BlazeComponent.extendComponent({

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

@@ -1,10 +1,10 @@
 template(name="boardList")
 template(name="boardList")
   .wrapper
   .wrapper
-    ul.board-list.clearfix
+    ul.board-list.clearfix.js-boards
       li.js-add-board
       li.js-add-board
         a.board-list-item.label {{_ 'add-board'}}
         a.board-list-item.label {{_ 'add-board'}}
       each boards
       each boards
-        li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
+        li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
           if isInvited
           if isInvited
             .board-list-item
             .board-list-item
               span.details
               span.details
@@ -39,7 +39,7 @@ template(name="boardList")
                     i.fa.js-archive-board(
                     i.fa.js-archive-board(
                         class="fa-archive"
                         class="fa-archive"
                         title="{{_ 'archive-board'}}")
                         title="{{_ 'archive-board'}}")
-                  else if currentUser.isBoardAdmin
+                  else if isAdministrable
                     i.fa.js-clone-board(
                     i.fa.js-clone-board(
                         class="fa-clone"
                         class="fa-clone"
                         title="{{_ 'duplicate-board'}}")
                         title="{{_ 'duplicate-board'}}")
@@ -55,7 +55,7 @@ template(name="boardList")
                         title="{{_ 'archive-board'}}")
                         title="{{_ 'archive-board'}}")
 
 
 template(name="boardListHeaderBar")
 template(name="boardListHeaderBar")
-  h1 {{_ 'my-boards'}}
+  h1 {{_ title }}
   .board-header-btns.right
   .board-header-btns.right
     a.board-header-btn.js-open-archived-board
     a.board-header-btn.js-open-archived-board
       i.fa.fa-archive
       i.fa.fa-archive

+ 72 - 8
client/components/boards/boardsList.js

@@ -1,4 +1,5 @@
 const subManager = new SubsManager();
 const subManager = new SubsManager();
+const { calculateIndex, enableClickOnTouch } = Utils;
 
 
 Template.boardListHeaderBar.events({
 Template.boardListHeaderBar.events({
   'click .js-open-archived-board'() {
   'click .js-open-archived-board'() {
@@ -7,6 +8,9 @@ Template.boardListHeaderBar.events({
 });
 });
 
 
 Template.boardListHeaderBar.helpers({
 Template.boardListHeaderBar.helpers({
+  title() {
+    return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public';
+  },
   templatesBoardId() {
   templatesBoardId() {
     return Meteor.user() && Meteor.user().getTemplatesBoardId();
     return Meteor.user() && Meteor.user().getTemplatesBoardId();
   },
   },
@@ -20,20 +24,80 @@ BlazeComponent.extendComponent({
     Meteor.subscribe('setting');
     Meteor.subscribe('setting');
   },
   },
 
 
-  boards() {
-    return Boards.find(
-      {
-        archived: false,
-        'members.userId': Meteor.userId(),
-        type: 'board',
+  onRendered() {
+    const self = this;
+    function userIsAllowedToMove() {
+      return Meteor.user();
+    }
+
+    const itemsSelector = '.js-board:not(.placeholder)';
+
+    const $boards = this.$('.js-boards');
+    $boards.sortable({
+      connectWith: '.js-boards',
+      tolerance: 'pointer',
+      appendTo: '.board-list',
+      helper: 'clone',
+      distance: 7,
+      items: itemsSelector,
+      placeholder: 'board-wrapper placeholder',
+      start(evt, ui) {
+        ui.helper.css('z-index', 1000);
+        ui.placeholder.height(ui.helper.height());
+        EscapeActions.executeUpTo('popup-close');
+      },
+      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 prevBoardDom = ui.item.prev('.js-board').get(0);
+        const nextBoardBom = ui.item.next('.js-board').get(0);
+        const sortIndex = calculateIndex(prevBoardDom, nextBoardBom, 1);
+
+        const boardDomElement = ui.item.get(0);
+        const board = Blaze.getData(boardDomElement);
+        // Normally the jquery-ui sortable library moves the dragged DOM element
+        // to its new position, which disrupts Blaze reactive updates mechanism
+        // (especially when we move the last card of a list, or when multiple
+        // users move some cards at the same time). To prevent these UX glitches
+        // we ask sortable to gracefully cancel the move, and to put back the
+        // DOM in its initial state. The card move is then handled reactively by
+        // Blaze with the below query.
+        $boards.sortable('cancel');
+
+        board.move(sortIndex.base);
       },
       },
-      { sort: ['title'] },
-    );
+    });
+
+    // ugly touch event hotfix
+    enableClickOnTouch(itemsSelector);
+
+    // Disable drag-dropping if the current user is not a board member or is comment only
+    this.autorun(() => {
+      $boards.sortable('option', 'disabled', !userIsAllowedToMove());
+    });
+  },
+
+  boards() {
+    let query = {
+      archived: false,
+      type: 'board',
+    };
+    if (FlowRouter.getRouteName() === 'home')
+      query['members.userId'] = Meteor.userId();
+    else query.permission = 'public';
+
+    return Boards.find(query, {
+      sort: { sort: 1 /* boards default sorting */ },
+    });
   },
   },
   isStarred() {
   isStarred() {
     const user = Meteor.user();
     const user = Meteor.user();
     return user && user.hasStarred(this.currentData()._id);
     return user && user.hasStarred(this.currentData()._id);
   },
   },
+  isAdministrable() {
+    const user = Meteor.user();
+    return user && user.isBoardAdmin(this.currentData()._id);
+  },
 
 
   hasOvertimeCards() {
   hasOvertimeCards() {
     subManager.subscribe('board', this.currentData()._id, false);
     subManager.subscribe('board', this.currentData()._id, false);

+ 17 - 3
client/components/boards/boardsList.styl

@@ -11,6 +11,19 @@ $spaceBetweenTiles = 16px
     box-sizing: border-box
     box-sizing: border-box
     position: relative
     position: relative
 
 
+    &.placeholder:after
+      content: '';
+      display: block;
+      background: darken(white, 20%)
+      border-radius: 3px;
+      height: 106px;
+      margin: 8px;
+
+    &.ui-sortable-helper
+      cursor: grabbing
+      transform: rotate(4deg)
+      display: block !important
+
     &.starred
     &.starred
       .fa-star,
       .fa-star,
       .fa-star-o
       .fa-star-o
@@ -20,7 +33,7 @@ $spaceBetweenTiles = 16px
     overflow: hidden;
     overflow: hidden;
     background-color: #999
     background-color: #999
     color: #f6f6f6
     color: #f6f6f6
-    height: 90px
+    height: auto
     font-size: 16px
     font-size: 16px
     line-height: 22px
     line-height: 22px
     border-radius: 3px
     border-radius: 3px
@@ -31,6 +44,7 @@ $spaceBetweenTiles = 16px
     margin: ($spaceBetweenTiles/2)
     margin: ($spaceBetweenTiles/2)
     position: relative
     position: relative
     text-decoration: none
     text-decoration: none
+    word-wrap: break-word
 
 
     &.tile
     &.tile
       background-size: auto
       background-size: auto
@@ -55,7 +69,7 @@ $spaceBetweenTiles = 16px
 
 
     .label
     .label
       font-weight: normal
       font-weight: normal
-      line-height:90px
+      line-height: 56px
 
 
     :hover
     :hover
       background-color:#939393
       background-color:#939393
@@ -183,7 +197,7 @@ $spaceBetweenTiles = 16px
     overflow: scroll
     overflow: scroll
 
 
     li
     li
-      width: 50% 
+      width: 50%
 
 
     .board-list-item
     .board-list-item
       overflow: hidden
       overflow: hidden

+ 16 - 12
client/components/cards/attachments.jade

@@ -38,18 +38,22 @@ template(name="attachmentsGalery")
               | {{_ 'download'}}
               | {{_ 'download'}}
             if currentUser.isBoardMember
             if currentUser.isBoardMember
               unless currentUser.isCommentOnly
               unless currentUser.isCommentOnly
-                if isImage
-                  a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
-                    i.fa.fa-thumb-tack
-                    if($eq ../coverId _id)
-                      | {{_ 'remove-cover'}}
-                    else
-                      | {{_ 'add-cover'}}
-                a.js-confirm-delete
-                  i.fa.fa-close
-                  | {{_ 'delete'}}
+                unless currentUser.isWorker
+                  if isImage
+                    a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
+                      i.fa.fa-thumb-tack
+                      if($eq ../coverId _id)
+                        | {{_ 'remove-cover'}}
+                      else
+                        | {{_ 'add-cover'}}
+                  a.js-confirm-delete
+                    i.fa.fa-close
+                    | {{_ 'delete'}}
 
 
     if currentUser.isBoardMember
     if currentUser.isBoardMember
       unless currentUser.isCommentOnly
       unless currentUser.isCommentOnly
-        li.attachment-item.add-attachment
-          a.js-add-attachment {{_ 'add-attachment' }}
+        unless currentUser.isWorker
+          //li.attachment-item.add-attachment
+          a.js-add-attachment
+            i.fa.fa-plus
+            | {{_ 'add-attachment' }}

+ 2 - 1
client/components/cards/cardDate.js

@@ -97,7 +97,8 @@ Template.dateBadge.helpers({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 });
 });

+ 355 - 170
client/components/cards/cardDetails.jade

@@ -8,16 +8,23 @@ template(name="cardDetails")
           a.fa.fa-times-thin.close-card-details.js-close-card-details
           a.fa.fa-times-thin.close-card-details.js-close-card-details
           if currentUser.isBoardMember
           if currentUser.isBoardMember
             a.fa.fa-navicon.card-details-menu.js-open-card-details-menu
             a.fa.fa-navicon.card-details-menu.js-open-card-details-menu
+            input.inline-input(type="text" id="cardURL_copy" value="{{ absoluteUrl }}")
+            a.fa.fa-link.card-copy-button.js-copy-link(
+              class="fa-link"
+              title="{{_ 'copy-card-link-to-clipboard'}}"
+              value="{{ absoluteUrl }}"
+            )
         if isMiniScreen
         if isMiniScreen
           a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details
           a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details
           if currentUser.isBoardMember
           if currentUser.isBoardMember
             a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu
             a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu
+            a.fa.fa-link.card-copy-mobile-button
         h2.card-details-title.js-card-title(
         h2.card-details-title.js-card-title(
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
             +viewer
             +viewer
               = getTitle
               = getTitle
-              if isWatching
-                i.fa.fa-eye.card-details-watch
+            if isWatching
+              i.card-details-watch.fa.fa-eye
         .card-details-path
         .card-details-path
           each parentList
           each parentList
             | &nbsp; &gt; &nbsp;
             | &nbsp; &gt; &nbsp;
@@ -25,7 +32,7 @@ template(name="cardDetails")
           // else
           // else
             {{_ 'top-level-card'}}
             {{_ 'top-level-card'}}
         if isLinkedCard
         if isLinkedCard
-          h3.linked-card-location
+          a.linked-card-location.js-go-to-linked-card
             +viewer
             +viewer
               | {{getBoardTitle}} > {{getTitle}}
               | {{getBoardTitle}} > {{getTitle}}
 
 
@@ -36,70 +43,105 @@ template(name="cardDetails")
         p.warning {{_ 'card-archived'}}
         p.warning {{_ 'card-archived'}}
 
 
     .card-details-items
     .card-details-items
-      .card-details-item.card-details-item-received
-        h3.card-details-item-title {{_ 'card-received'}}
-        if getReceived
-          +cardReceivedDate
-        else
-          if canModifyCard
-            a.js-received-date {{_ 'add'}}
-
-      .card-details-item.card-details-item-start
-        h3.card-details-item-title {{_ 'card-start'}}
-        if getStart
-          +cardStartDate
-        else
-          if canModifyCard
-            a.js-start-date {{_ 'add'}}
-
-      .card-details-item.card-details-item-due
-        h3.card-details-item-title {{_ 'card-due'}}
-        if getDue
-          +cardDueDate
-        else
+      if currentBoard.allowsReceivedDate
+        .card-details-item.card-details-item-received
+          h3
+            i.fa.fa-sign-out
+            card-details-item-title {{_ 'card-received'}}
+          if getReceived
+            +cardReceivedDate
+          else
+            if canModifyCard
+              unless currentUser.isWorker
+                a.card-label.add-label.js-received-date
+                  i.fa.fa-plus
+
+      if currentBoard.allowsStartDate
+        .card-details-item.card-details-item-start
+          h3
+            i.fa.fa-hourglass-start
+            card-details-item-title {{_ 'card-start'}}
+          if getStart
+            +cardStartDate
+          else
+            if canModifyCard
+              unless currentUser.isWorker
+                a.card-label.add-label.js-start-date
+                  i.fa.fa-plus
+
+      if currentBoard.allowsDueDate
+        .card-details-item.card-details-item-due
+          h3
+            i.fa.fa-sign-in
+            card-details-item-title {{_ 'card-due'}}
+          if getDue
+            +cardDueDate
+          else
+            if canModifyCard
+              unless currentUser.isWorker
+                a.card-label.add-label.js-due-date
+                  i.fa.fa-plus
+
+      if currentBoard.allowsEndDate
+        .card-details-item.card-details-item-end
+          h3
+            i.fa.fa-hourglass-end
+            card-details-item-title {{_ 'card-end'}}
+          if getEnd
+            +cardEndDate
+          else
+            if canModifyCard
+              unless currentUser.isWorker
+                a.card-label.add-label.js-end-date
+                  i.fa.fa-plus
+
+      //.card-details-items
+      if currentBoard.allowsMembers
+        .card-details-item.card-details-item-members
+          h3
+            i.fa.fa-users
+            card-details-item-title {{_ 'members'}}
+          each getMembers
+            +userAvatar(userId=this cardId=../_id)
+            | {{! XXX Hack to hide syntaxic coloration /// }}
           if canModifyCard
           if canModifyCard
-            a.js-due-date {{_ 'add'}}
-
-      .card-details-item.card-details-item-end
-        h3.card-details-item-title {{_ 'card-end'}}
-        if getEnd
-          +cardEndDate
-        else
+            unless currentUser.isWorker
+              a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
+                i.fa.fa-plus
+
+      //if assigneeSelected
+      if currentBoard.allowsAssignee
+        .card-details-item.card-details-item-assignees
+          h3
+            i.fa.fa-user
+            card-details-item-title {{_ 'assignee'}}
+          each getAssignees
+            +userAvatarAssignee(userId=this cardId=../_id)
+            | {{! XXX Hack to hide syntaxic coloration /// }}
           if canModifyCard
           if canModifyCard
-            a.js-end-date {{_ 'add'}}
-
-    .card-details-items
-      .card-details-item.card-details-item-members
-        h3.card-details-item-title {{_ 'members'}}
-        each getMembers
-          +userAvatar(userId=this cardId=../_id)
-          | {{! XXX Hack to hide syntaxic coloration /// }}
-        if canModifyCard
-          a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
-            i.fa.fa-plus
-
-      .card-details-item.card-details-item-assignees
-        h3.card-details-item-title {{_ 'assignee'}}
-        each getAssignees
-          +userAvatarAssignee(userId=this cardId=../_id)
-          | {{! XXX Hack to hide syntaxic coloration /// }}
-        if canModifyCard
-          unless assigneeSelected
             a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
             a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
               i.fa.fa-plus
               i.fa.fa-plus
+          if currentUser.isWorker
+            unless assigneeSelected
+              a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
+                i.fa.fa-plus
+
+      if currentBoard.allowsLabels
+        .card-details-item.card-details-item-labels
+          h3
+            i.fa.fa-tags
+            card-details-item-title {{_ 'labels'}}
+          a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
+            each labels
+              span.card-label(class="card-label-{{color}}" title=name)
+                +viewer
+                  = name
+          if canModifyCard
+            unless currentUser.isWorker
+              a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
+                i.fa.fa-plus
 
 
-      .card-details-item.card-details-item-labels
-        h3.card-details-item-title {{_ 'labels'}}
-        a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
-          each labels
-            span.card-label(class="card-label-{{color}}" title=name)
-              +viewer
-                = name
-        if canModifyCard
-          a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
-            i.fa.fa-plus
-
-    .card-details-items
+      //.card-details-items
       each customFieldsWD
       each customFieldsWD
         .card-details-item.card-details-item-customfield
         .card-details-item.card-details-item-customfield
           h3.card-details-item-title
           h3.card-details-item-title
@@ -107,7 +149,7 @@ template(name="cardDetails")
               = definition.name
               = definition.name
           +cardCustomField
           +cardCustomField
 
 
-    .card-details-items
+      //.card-details-items
       if getSpentTime
       if getSpentTime
         .card-details-item.card-details-item-spent
         .card-details-item.card-details-item-spent
           if getIsOvertime
           if getIsOvertime
@@ -116,84 +158,124 @@ template(name="cardDetails")
             h3.card-details-item-title {{_ 'spent-time-hours'}}
             h3.card-details-item-title {{_ 'spent-time-hours'}}
           +cardSpentTime
           +cardSpentTime
 
 
-    //- XXX We should use "editable" to avoid repetiting ourselves
-    if canModifyCard
-      h3.card-details-item-title {{_ 'description'}}
-      +inlinedCardDescription(classNames="card-description js-card-description")
-        +editor(autofocus=true)
-          | {{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 getDescription
+      //.card-details-items
+      if currentBoard.allowsRequestedBy
+        .card-details-item.card-details-item-name
+          h3
+            i.fa.fa-shopping-cart
+            card-details-item-title {{_ 'requested-by'}}
+          if canModifyCard
+            unless currentUser.isWorker
+              +inlinedForm(classNames="js-card-details-requester")
+                +editCardRequesterForm
+              else
+                a.js-open-inlined-form
+                  if getRequestedBy
+                    +viewer
+                      = getRequestedBy
+                  else
+                    | {{_ 'add'}}
+          else if getRequestedBy
             +viewer
             +viewer
-              = getDescription
-          else
-            | {{_ 'edit'}}
-        if (hasUnsavedValue 'cardDescription' _id)
-          p.quiet
-            | {{_ 'unsaved-description'}}
-            a.js-open-inlined-form {{_ 'view-it'}}
-            = ' - '
-            a.js-close-inlined-form {{_ 'discard'}}
-    else if getDescription
-      h3.card-details-item-title {{_ 'description'}}
-      +viewer
-        = getDescription
+              = getRequestedBy
 
 
-    .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
+      if currentBoard.allowsAssignedBy
+        .card-details-item.card-details-item-name
+          h3
+            i.fa.fa-user-plus
+            card-details-item-title {{_ 'assigned-by'}}
+          if canModifyCard
+            unless currentUser.isWorker
+              +inlinedForm(classNames="js-card-details-assigner")
+                +editCardAssignerForm
               else
               else
-                | {{_ 'add'}}
-        else if getRequestedBy
-          +viewer
-            = getAssignedBy
-
-    hr
-    +checklists(cardId = _id)
+                a.js-open-inlined-form
+                  if getAssignedBy
+                    +viewer
+                      = getAssignedBy
+                  else
+                    | {{_ 'add'}}
+          else if getRequestedBy
+            +viewer
+              = getAssignedBy
 
 
-    if currentBoard.allowsSubtasks
+    if getVoteQuestion
       hr
       hr
-      +subtasks(cardId = _id)
-
-    hr
-    h3
-      i.fa.fa-paperclip
-      | {{_ 'attachments'}}
+      .vote-title
+        h3
+          i.fa.fa-thumbs-up
+          card-details-item-title {{_ 'vote-question'}}
+        .vote-result
+          if votePublic
+            a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }}
+            a.card-label.card-label-red.js-show-negative-votes {{ voteCountNegative }}
+          else
+            .card-label.card-label-green {{ voteCountPositive }}
+            .card-label.card-label-red {{ voteCountNegative }}
+      +viewer
+        = getVoteQuestion
+      button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}") {{_ 'vote-for-it'}}
+      button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'vote-against'}}
 
 
-    +attachmentsGalery
+    //- XXX We should use "editable" to avoid repetiting ourselves
+    if canModifyCard
+      unless currentUser.isWorker
+        if currentBoard.allowsDescriptionTitle
+          hr
+          h3
+            i.fa.fa-align-left
+            card-details-item-title {{_ 'description'}}
+        if currentBoard.allowsDescriptionText
+          +inlinedCardDescription(classNames="card-description js-card-description")
+            +editor(autofocus=true)
+              | {{getUnsavedValue 'cardDescription' _id getDescription}}
+            .edit-controls.clearfix
+              button.primary(type="submit") {{_ 'save'}}
+              a.fa.fa-times-thin.js-close-inlined-form
+          else
+            if currentBoard.allowsDescriptionText
+              a.js-open-inlined-form
+                if getDescription
+                  +viewer
+                    = getDescription
+                else
+                  | {{_ 'edit'}}
+              if (hasUnsavedValue 'cardDescription' _id)
+                p.quiet
+                  | {{_ 'unsaved-description'}}
+                  a.js-open-inlined-form {{_ 'view-it'}}
+                  = ' - '
+                  a.js-close-inlined-form {{_ 'discard'}}
+    else if getDescription
+      if currentBoard.allowsDescriptionTitle
+        hr
+        h3.card-details-item-title {{_ 'description'}}
+      if currentBoard.allowsDescriptionText
+        +viewer
+          = getDescription
+
+    .card-checklist-attachmentGalerys
+      .card-checklist-attachmentGalery.card-checklists
+        if currentBoard.allowsChecklists
+          hr
+          +checklists(cardId = _id)
+        if currentBoard.allowsSubtasks
+          hr
+          +subtasks(cardId = _id)
+      if currentBoard.allowsAttachments
+        hr
+        h3
+          i.fa.fa-paperclip
+          | {{_ 'attachments'}}
+        .card-checklist-attachmentGalery.card-attachmentGalery
+          +attachmentsGalery
 
 
     hr
     hr
     unless currentUser.isNoComments
     unless currentUser.isNoComments
       .activity-title
       .activity-title
-        h3 {{ _ 'activity'}}
+        h3
+          i.fa.fa-history
+          | {{ _ 'activity'}}
         if currentUser.isBoardMember
         if currentUser.isBoardMember
           .material-toggle-switch
           .material-toggle-switch
             span.toggle-switch-title {{_ 'hide-system-messages'}}
             span.toggle-switch-title {{_ 'hide-system-messages'}}
@@ -202,9 +284,10 @@ template(name="cardDetails")
             else
             else
               input.toggle-switch(type="checkbox" id="toggleButton")
               input.toggle-switch(type="checkbox" id="toggleButton")
             label.toggle-label(for="toggleButton")
             label.toggle-label(for="toggleButton")
-    if currentUser.isBoardMember
-      unless currentUser.isNoComments
-        +commentForm
+    if currentBoard.allowsComments
+      if currentUser.isBoardMember
+        unless currentUser.isNoComments
+          +commentForm
     unless currentUser.isNoComments
     unless currentUser.isNoComments
       if isLoaded.get
       if isLoaded.get
         if isLinkedCard
         if isLinkedCard
@@ -235,32 +318,89 @@ template(name="editCardAssignerForm")
 
 
 template(name="cardDetailsActionsPopup")
 template(name="cardDetailsActionsPopup")
   ul.pop-over-list
   ul.pop-over-list
-    li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
+    li
+      a.js-toggle-watch-card
+        if isWatching
+          i.fa.fa-eye
+          |  {{_ 'unwatch'}}
+        else
+          i.fa.fa-eye-slash
+          |  {{_ 'watch'}}
   if canModifyCard
   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-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'}}
-      li: a.js-move-card-to-bottom {{_ 'moveCardToBottom-title'}}
-    hr
+    unless currentUser.isWorker
+      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'}}
+        if getVoteQuestion
+          li
+            a.js-cancel-voting
+              i.fa.fa-thumbs-up
+              | {{_ 'card-cancel-voting'}}
+        else
+          li
+            a.js-start-voting
+              i.fa.fa-thumbs-up
+              | {{_ 'card-start-voting'}}
+        li
+          a.js-custom-fields
+            i.fa.fa-list-alt
+            | {{_ '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
+            i.fa.fa-clock-o
+            | {{_ 'editCardSpentTimePopup-title'}}
+        li
+          a.js-set-card-color
+            i.fa.fa-paint-brush
+            | {{_ 'setCardColorPopup-title'}}
+      hr
     ul.pop-over-list
     ul.pop-over-list
-      li: a.js-move-card {{_ 'moveCardPopup-title'}}
-      li: a.js-copy-card {{_ 'copyCardPopup-title'}}
-      li: a.js-copy-checklist-cards {{_ 'copyChecklistToManyCardsPopup-title'}}
+      li
+        a.js-move-card-to-top
+          i.fa.fa-arrow-up
+          | {{_ 'moveCardToTop-title'}}
+      li
+        a.js-move-card-to-bottom
+          i.fa.fa-arrow-down
+          | {{_ 'moveCardToBottom-title'}}
+    unless currentUser.isWorker
+      hr
+      ul.pop-over-list
+        li
+          a.js-move-card
+            i.fa.fa-arrow-right
+            | {{_ 'moveCardPopup-title'}}
+        li
+          a.js-copy-card
+            i.fa.fa-copy
+            | {{_ 'copyCardPopup-title'}}
+      hr
+      ul.pop-over-list
+        li
+          a.js-copy-checklist-cards
+            i.fa.fa-list
+            i.fa.fa-copy
+            | {{_ 'copyChecklistToManyCardsPopup-title'}}
       unless archived
       unless archived
-        li: a.js-archive {{_ 'archive-card'}}
-      li: a.js-more {{_ 'cardMorePopup-title'}}
+        hr
+        ul.pop-over-list
+          li
+            a.js-archive
+              i.fa.fa-arrow-right
+              i.fa.fa-archive
+              | {{_ 'archive-card'}}
+      hr
+      ul.pop-over-list
+        li
+          a.js-more
+            i.fa.fa-link
+            | {{_ 'cardMorePopup-title'}}
 
 
 template(name="moveCardPopup")
 template(name="moveCardPopup")
   +boardsAndLists
   +boardsAndLists
@@ -312,16 +452,27 @@ template(name="cardMembersPopup")
             i.fa.fa-check
             i.fa.fa-check
 
 
 template(name="cardAssigneesPopup")
 template(name="cardAssigneesPopup")
-  ul.pop-over-list.js-card-assignee-list
-    each board.activeMembers
-      li.item(class="{{#if isCardAssignee}}active{{/if}}")
-        a.name.js-select-assignee(href="#")
-          +userAvatar(userId=user._id)
-          span.full-name
-            = user.profile.fullname
-            | (<span class="username">{{ user.username }}</span>)
-          if isCardAssignee
-            i.fa.fa-check
+  unless currentUser.isWorker
+    ul.pop-over-list.js-card-assignee-list
+      each board.activeMembers
+        li.item(class="{{#if isCardAssignee}}active{{/if}}")
+          a.name.js-select-assignee(href="#")
+            +userAvatar(userId=user._id)
+            span.full-name
+              = user.profile.fullname
+              | (<span class="username">{{ user.username }}</span>)
+            if isCardAssignee
+              i.fa.fa-check
+  if currentUser.isWorker
+    ul.pop-over-list.js-card-assignee-list
+        li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
+          a.name.js-select-assignee(href="#")
+            +userAvatar(userId=currentUser._id)
+            span.full-name
+              = currentUser.profile.fullname
+              | (<span class="username">{{ currentUser.username }}</span>)
+            if currentUser.isCardAssignee
+              i.fa.fa-check
 
 
 template(name="userAvatarAssignee")
 template(name="userAvatarAssignee")
   a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
   a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
@@ -349,11 +500,13 @@ template(name="cardAssigneePopup")
         p.quiet @{{ user.username }}
         p.quiet @{{ user.username }}
     ul.pop-over-list
     ul.pop-over-list
       if currentUser.isNotCommentOnly
       if currentUser.isNotCommentOnly
+        unless currentUser.isWorker
           li: a.js-remove-assignee {{_ 'remove-member-from-card'}}
           li: a.js-remove-assignee {{_ 'remove-member-from-card'}}
 
 
-      if $eq currentUser._id user._id
-        with currentUser
-          li: a.js-edit-profile {{_ 'edit-profile'}}
+      unless currentUser.isWorker
+        if $eq currentUser._id user._id
+          with currentUser
+            li: a.js-edit-profile {{_ 'edit-profile'}}
 
 
 template(name="userAvatarAssigneeInitials")
 template(name="userAvatarAssigneeInitials")
   svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15")
   svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15")
@@ -413,3 +566,35 @@ template(name="cardDeletePopup")
   unless archived
   unless archived
    p {{_ "card-delete-suggest-archive"}}
    p {{_ "card-delete-suggest-archive"}}
   button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
   button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="cardStartVotingPopup")
+  form.edit-vote-question
+    .fields
+      label(for="vote") {{_ 'vote-question'}}
+      input.js-vote-field#vote(type="text" name="vote" value="{{card.getVoteQuestion}}" autofocus)
+      label(for="vote-public") {{_ 'vote-public'}}
+        a.js-toggle-vote-public
+          .materialCheckBox#vote-public(name="vote-public")
+
+    button.primary.confirm.js-submit {{_ 'save'}}
+    //- button.js-remove-color.negate.wide.right {{_ 'delete'}}
+
+template(name="positiveVoteMembersPopup")
+  ul.pop-over-list.js-card-member-list
+    each m in voteMemberPositive
+      li.item
+        a.name
+          +userAvatar(userId=m._id)
+          span.full-name
+            = m.profile.fullname
+            | (<span class="username">{{ m.username }}</span>)
+
+template(name="negativeVoteMembersPopup")
+  ul.pop-over-list.js-card-member-list
+    each m in voteMemberNegative
+      li.item
+        a.name
+          +userAvatar(userId=m._id)
+          span.full-name
+            = m.profile.fullname
+            | (<span class="username">{{ m.username }}</span>)

+ 179 - 14
client/components/cards/cardDetails.js

@@ -1,5 +1,5 @@
 const subManager = new SubsManager();
 const subManager = new SubsManager();
-const { calculateIndexData, enableClickOnTouch } = Utils;
+const { calculateIndexData } = Utils;
 
 
 let cardColors;
 let cardColors;
 Meteor.startup(() => {
 Meteor.startup(() => {
@@ -38,6 +38,37 @@ BlazeComponent.extendComponent({
     Meteor.subscribe('unsaved-edits');
     Meteor.subscribe('unsaved-edits');
   },
   },
 
 
+  voteState() {
+    const card = this.currentData();
+    const userId = Meteor.userId();
+    let state;
+    if (card.vote) {
+      if (card.vote.positive) {
+        state = _.contains(card.vote.positive, userId);
+        if (state === true) return true;
+      }
+      if (card.vote.negative) {
+        state = _.contains(card.vote.negative, userId);
+        if (state === true) return false;
+      }
+    }
+    return null;
+  },
+  votePublic() {
+    const card = this.currentData();
+    if (card.vote) return card.vote.public;
+    return null;
+  },
+  voteCountPositive() {
+    const card = this.currentData();
+    if (card.vote && card.vote.positive) return card.vote.positive.length;
+    return null;
+  },
+  voteCountNegative() {
+    const card = this.currentData();
+    if (card.vote && card.vote.negative) return card.vote.negative.length;
+    return null;
+  },
   isWatching() {
   isWatching() {
     const card = this.currentData();
     const card = this.currentData();
     return card.findWatcher(Meteor.userId());
     return card.findWatcher(Meteor.userId());
@@ -51,7 +82,8 @@ BlazeComponent.extendComponent({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 
 
@@ -199,9 +231,6 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.card-checklist-items .js-checklist');
-
     const $subtasksDom = this.$('.card-subtasks-items');
     const $subtasksDom = this.$('.card-subtasks-items');
 
 
     $subtasksDom.sortable({
     $subtasksDom.sortable({
@@ -237,20 +266,21 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.card-subtasks-items .js-subtasks');
-
     function userIsMember() {
     function userIsMember() {
       return Meteor.user() && Meteor.user().isBoardMember();
       return Meteor.user() && Meteor.user().isBoardMember();
     }
     }
 
 
     // Disable sorting if the current user is not a board member
     // Disable sorting if the current user is not a board member
     this.autorun(() => {
     this.autorun(() => {
-      if ($checklistsDom.data('sortable')) {
-        $checklistsDom.sortable('option', 'disabled', !userIsMember());
+      const disabled = !userIsMember() || Utils.isMiniScreen();
+      if (
+        $checklistsDom.data('uiSortable') ||
+        $checklistsDom.data('sortable')
+      ) {
+        $checklistsDom.sortable('option', 'disabled', disabled);
       }
       }
-      if ($subtasksDom.data('sortable')) {
-        $subtasksDom.sortable('option', 'disabled', !userIsMember());
+      if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
+        $subtasksDom.sortable('option', 'disabled', disabled);
       }
       }
     });
     });
   },
   },
@@ -278,6 +308,29 @@ BlazeComponent.extendComponent({
         'click .js-close-card-details'() {
         'click .js-close-card-details'() {
           Utils.goBoardId(this.data().boardId);
           Utils.goBoardId(this.data().boardId);
         },
         },
+        'click .js-copy-link'() {
+          StringToCopyElement = document.getElementById('cardURL_copy');
+          StringToCopyElement.select();
+          if (document.execCommand('copy')) {
+            StringToCopyElement.blur();
+          } else {
+            document.getElementById('cardURL_copy').selectionStart = 0;
+            document.getElementById('cardURL_copy').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-open-card-details-menu': Popup.open('cardDetailsActions'),
         'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
         'submit .js-card-description'(event) {
         'submit .js-card-description'(event) {
           event.preventDefault();
           event.preventDefault();
@@ -317,6 +370,9 @@ BlazeComponent.extendComponent({
             this.data().setRequestedBy('');
             this.data().setRequestedBy('');
           }
           }
         },
         },
+        'click .js-go-to-linked-card'() {
+          Utils.goCardId(this.data().linkedId);
+        },
         'click .js-member': Popup.open('cardMember'),
         'click .js-member': Popup.open('cardMember'),
         'click .js-add-members': Popup.open('cardMembers'),
         'click .js-add-members': Popup.open('cardMembers'),
         'click .js-assignee': Popup.open('cardAssignee'),
         'click .js-assignee': Popup.open('cardAssignee'),
@@ -326,6 +382,8 @@ BlazeComponent.extendComponent({
         'click .js-start-date': Popup.open('editCardStartDate'),
         'click .js-start-date': Popup.open('editCardStartDate'),
         'click .js-due-date': Popup.open('editCardDueDate'),
         'click .js-due-date': Popup.open('editCardDueDate'),
         'click .js-end-date': Popup.open('editCardEndDate'),
         'click .js-end-date': Popup.open('editCardEndDate'),
+        'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
+        'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
         'mouseenter .js-card-details'() {
         'mouseenter .js-card-details'() {
           const parentComponent = this.parentComponent().parentComponent();
           const parentComponent = this.parentComponent().parentComponent();
           //on mobile view parent is Board, not BoardBody.
           //on mobile view parent is Board, not BoardBody.
@@ -349,6 +407,18 @@ BlazeComponent.extendComponent({
         'click #toggleButton'() {
         'click #toggleButton'() {
           Meteor.call('toggleSystemMessages');
           Meteor.call('toggleSystemMessages');
         },
         },
+        'click .js-vote'(e) {
+          const forIt = $(e.target).hasClass('js-vote-positive');
+          let newState = null;
+          if (
+            this.voteState() === null ||
+            (this.voteState() === false && forIt) ||
+            (this.voteState() === true && !forIt)
+          ) {
+            newState = forIt;
+          }
+          this.data().setVote(Meteor.userId(), newState);
+        },
       },
       },
     ];
     ];
   },
   },
@@ -370,6 +440,54 @@ Template.cardDetails.helpers({
     });
     });
   },
   },
 
 
+  receivedSelected() {
+    if (this.getReceived().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
+  startSelected() {
+    if (this.getStart().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
+  endSelected() {
+    if (this.getEnd().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
+  dueSelected() {
+    if (this.getDue().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
+  memberSelected() {
+    if (this.getMembers().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
+  labelSelected() {
+    if (this.getLabels().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
   assigneeSelected() {
   assigneeSelected() {
     if (this.getAssignees().length === 0) {
     if (this.getAssignees().length === 0) {
       return false;
       return false;
@@ -378,6 +496,22 @@ Template.cardDetails.helpers({
     }
     }
   },
   },
 
 
+  requestBySelected() {
+    if (this.getRequestBy().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
+  assigneeBySelected() {
+    if (this.getAssigneeBy().length === 0) {
+      return false;
+    } else {
+      return true;
+    }
+  },
+
   memberType() {
   memberType() {
     const user = Users.findOne(this.userId);
     const user = Users.findOne(this.userId);
     return user && user.isBoardAdmin() ? 'admin' : 'normal';
     return user && user.isBoardAdmin() ? 'admin' : 'normal';
@@ -466,6 +600,7 @@ Template.cardDetailsActionsPopup.events({
   'click .js-assignees': Popup.open('cardAssignees'),
   'click .js-assignees': Popup.open('cardAssignees'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
   'click .js-attachments': Popup.open('cardAttachments'),
+  'click .js-start-voting': Popup.open('cardStartVoting'),
   'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
@@ -476,6 +611,11 @@ Template.cardDetailsActionsPopup.events({
   'click .js-copy-card': Popup.open('copyCard'),
   'click .js-copy-card': Popup.open('copyCard'),
   'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
   'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
   'click .js-set-card-color': Popup.open('setCardColor'),
   'click .js-set-card-color': Popup.open('setCardColor'),
+  'click .js-cancel-voting'(event) {
+    event.preventDefault();
+    this.unsetVote();
+    Popup.close();
+  },
   'click .js-move-card-to-top'(event) {
   'click .js-move-card-to-top'(event) {
     event.preventDefault();
     event.preventDefault();
     const minOrder = _.min(
     const minOrder = _.min(
@@ -578,7 +718,7 @@ BlazeComponent.extendComponent({
         _id: { $ne: Meteor.user().getTemplatesBoardId() },
         _id: { $ne: Meteor.user().getTemplatesBoardId() },
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
     return boards;
     return boards;
@@ -754,7 +894,7 @@ BlazeComponent.extendComponent({
         },
         },
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
     return boards;
     return boards;
@@ -851,6 +991,31 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('cardMorePopup');
 }).register('cardMorePopup');
 
 
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentCard = this.currentData();
+    this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion);
+  },
+
+  events() {
+    return [
+      {
+        'submit .edit-vote-question'(evt) {
+          evt.preventDefault();
+          const voteQuestion = evt.target.vote.value;
+          const publicVote = $('#vote-public').hasClass('is-checked');
+          this.currentCard.setVoteQuestion(voteQuestion, publicVote);
+          Popup.close();
+        },
+        'click a.js-toggle-vote-public'(event) {
+          event.preventDefault();
+          $('#vote-public').toggleClass('is-checked');
+        },
+      },
+    ];
+  },
+}).register('cardStartVotingPopup');
+
 // Close the card details pane by pressing escape
 // Close the card details pane by pressing escape
 EscapeActions.register(
 EscapeActions.register(
   'detailsPane',
   'detailsPane',

+ 32 - 3
client/components/cards/cardDetails.styl

@@ -4,6 +4,12 @@
 
 
 avatar-radius = 50%
 avatar-radius = 50%
 
 
+#cardURL_copy
+  // Have clipboard text not visible by moving it to far left
+  position: absolute
+  left: -2000px
+  top: 0px
+
 .assignee
 .assignee
   border-radius: 3px
   border-radius: 3px
   display: block
   display: block
@@ -88,17 +94,18 @@ avatar-radius = 50%
   animation: flexGrowIn 0.1s
   animation: flexGrowIn 0.1s
   box-shadow: 0 0 7px 0 darken(white, 30%)
   box-shadow: 0 0 7px 0 darken(white, 30%)
   transition: flex-basis 0.1s
   transition: flex-basis 0.1s
+  box-sizing: border-box
 
 
   .mCustomScrollBox
   .mCustomScrollBox
     padding-left: 0
     padding-left: 0
 
 
   .ps-scrollbar-y-rail
   .ps-scrollbar-y-rail
     pointer-event: all
     pointer-event: all
-    position: absolute;
+    position: absolute
 
 
   .card-details-canvas
   .card-details-canvas
     width: 470px
     width: 470px
-    padding-left: 20px;
+    padding-left: 20px
 
 
   .card-details-header
   .card-details-header
     margin: 0 -20px 5px
     margin: 0 -20px 5px
@@ -108,6 +115,8 @@ avatar-radius = 50%
 
 
     .close-card-details,
     .close-card-details,
     .card-details-menu,
     .card-details-menu,
+    .card-copy-button,
+    .card-copy-mobile-button,
     .close-card-details-mobile-web,
     .close-card-details-mobile-web,
     .card-details-menu-mobile-web
     .card-details-menu-mobile-web
       float: right
       float: right
@@ -122,6 +131,16 @@ avatar-radius = 50%
       padding: 5px
       padding: 5px
       margin-right: 40px
       margin-right: 40px
 
 
+    .card-copy-button
+      font-size: 17px
+      padding: 10px
+      margin-right: 10px
+
+    .card-copy-mobile-button
+      font-size: 17px
+      padding: 10px
+      margin-right: 10px
+
     .card-details-menu
     .card-details-menu
       font-size: 17px
       font-size: 17px
       padding: 10px
       padding: 10px
@@ -223,7 +242,7 @@ input[type="submit"].attachment-add-link-submit
 
 
     .card-details-canvas
     .card-details-canvas
       width: 100%
       width: 100%
-      padding-left: 0px;
+      padding-left: 0px
 
 
     .card-details-header
     .card-details-header
       .close-card-details
       .close-card-details
@@ -312,3 +331,13 @@ card-details-color(background, color...)
 
 
 .card-details-indigo
 .card-details-indigo
   card-details-color(#4b0082, #ffffff) //White text for better visibility
   card-details-color(#4b0082, #ffffff) //White text for better visibility
+
+.voted
+  opacity: .7
+.vote-title
+  display: flex
+  justify-content: space-between
+.vote-result
+  display: flex
+.js-show-positive-votes
+  cursor: pointer

+ 5 - 2
client/components/cards/checklists.jade

@@ -1,5 +1,7 @@
 template(name="checklists")
 template(name="checklists")
-  h3 {{_ 'checklists'}}
+  h3
+    i.fa.fa-check
+    | {{_ 'checklists'}}
   if toggleDeleteDialog.get
   if toggleDeleteDialog.get
     .board-overlay#card-details-overlay
     .board-overlay#card-details-overlay
     +checklistDeleteDialog(checklist = checklistToDelete)
     +checklistDeleteDialog(checklist = checklistToDelete)
@@ -86,7 +88,8 @@ template(name="checklistItems")
 template(name='checklistItemDetail')
 template(name='checklistItemDetail')
   .js-checklist-item.checklist-item
   .js-checklist-item.checklist-item
     if canModifyCard
     if canModifyCard
-      .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      .check-box-container
+        .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}}")
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
         +viewer
         +viewer
           = item.title
           = item.title

+ 26 - 11
client/components/cards/checklists.js

@@ -1,4 +1,4 @@
-const { calculateIndexData, enableClickOnTouch } = Utils;
+const { calculateIndexData, capitalize } = Utils;
 
 
 function initSorting(items) {
 function initSorting(items) {
   items.sortable({
   items.sortable({
@@ -36,9 +36,6 @@ function initSorting(items) {
       checklistItem.move(checklistId, sortIndex.base);
       checklistItem.move(checklistId, sortIndex.base);
     },
     },
   });
   });
-
-  // ugly touch event hotfix
-  enableClickOnTouch('.js-checklist-item:not(.placeholder)');
 }
 }
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
@@ -54,11 +51,15 @@ BlazeComponent.extendComponent({
       return Meteor.user() && Meteor.user().isBoardMember();
       return Meteor.user() && Meteor.user().isBoardMember();
     }
     }
 
 
-    // Disable sorting if the current user is not a board member
+    // Disable sorting if the current user is not a board member or is a miniscreen
     self.autorun(() => {
     self.autorun(() => {
       const $itemsDom = $(self.itemsDom);
       const $itemsDom = $(self.itemsDom);
-      if ($itemsDom.data('sortable')) {
-        $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
+      if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
+        $(self.itemsDom).sortable(
+          'option',
+          'disabled',
+          !userIsMember() || Utils.isMiniScreen(),
+        );
       }
       }
     });
     });
   },
   },
@@ -67,7 +68,8 @@ BlazeComponent.extendComponent({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 }).register('checklistDetail');
 }).register('checklistDetail');
@@ -120,7 +122,8 @@ BlazeComponent.extendComponent({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 
 
@@ -172,6 +175,16 @@ BlazeComponent.extendComponent({
     }
     }
   },
   },
 
 
+  focusChecklistItem(event) {
+    // If a new checklist is created, pre-fill the title and select it.
+    const checklist = this.currentData().checklist;
+    if (!checklist) {
+      const textarea = event.target;
+      textarea.value = capitalize(TAPi18n.__('r-checklist'));
+      textarea.select();
+    }
+  },
+
   events() {
   events() {
     const events = {
     const events = {
       'click .toggle-delete-checklist-dialog'(event) {
       'click .toggle-delete-checklist-dialog'(event) {
@@ -191,6 +204,7 @@ BlazeComponent.extendComponent({
         'submit .js-edit-checklist-item': this.editChecklistItem,
         'submit .js-edit-checklist-item': this.editChecklistItem,
         'click .js-delete-checklist-item': this.deleteItem,
         'click .js-delete-checklist-item': this.deleteItem,
         'click .confirm-checklist-delete': this.deleteChecklist,
         'click .confirm-checklist-delete': this.deleteChecklist,
+        'focus .js-add-checklist-item': this.focusChecklistItem,
         keydown: this.pressKey,
         keydown: this.pressKey,
       },
       },
     ];
     ];
@@ -228,7 +242,8 @@ Template.checklistItemDetail.helpers({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 });
 });
@@ -244,7 +259,7 @@ BlazeComponent.extendComponent({
   events() {
   events() {
     return [
     return [
       {
       {
-        'click .js-checklist-item .check-box': this.toggleItem,
+        'click .js-checklist-item .check-box-container': this.toggleItem,
       },
       },
     ];
     ];
   },
   },

+ 4 - 1
client/components/cards/checklists.styl

@@ -113,6 +113,9 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   &:hover
   &:hover
     background-color: darken(white, 8%)
     background-color: darken(white, 8%)
 
 
+  .check-box-container
+    padding-right: 1px;
+
   .check-box
   .check-box
     margin: 0.1em 0 0 0;
     margin: 0.1em 0 0 0;
     &.is-checked
     &.is-checked
@@ -121,7 +124,7 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
 
 
   .item-title
   .item-title
     flex: 1
     flex: 1
-    padding-left: 10px;
+    margin-left: 10px;
     &.is-checked
     &.is-checked
       color: #8c8c8c
       color: #8c8c8c
       font-style: italic
       font-style: italic

+ 2 - 0
client/components/cards/labels.styl

@@ -158,6 +158,8 @@
 
 
 .edit-labels-pop-over
 .edit-labels-pop-over
   margin-bottom: 8px
   margin-bottom: 8px
+  .card-label .viewer p
+    margin: 0
 
 
 .edit-labels-pop-over .shortcut
 .edit-labels-pop-over .shortcut
   display: inline-block
   display: inline-block

+ 4 - 0
client/components/cards/minicard.jade

@@ -100,6 +100,10 @@ template(name="minicard")
       if getDescription
       if getDescription
         .badge.badge-state-image-only(title=getDescription)
         .badge.badge-state-image-only(title=getDescription)
           span.badge-icon.fa.fa-align-left
           span.badge-icon.fa.fa-align-left
+      if getVoteQuestion
+        .badge.badge-state-image-only(title=getVoteQuestion)
+          span.badge-icon.fa.fa-thumbs-up
+          span.badge-icon.fa.fa-thumbs-down
       if attachments.count
       if attachments.count
         .badge
         .badge
           span.badge-icon.fa.fa-paperclip
           span.badge-icon.fa.fa-paperclip

+ 6 - 10
client/components/cards/minicard.js

@@ -36,24 +36,20 @@ Template.minicard.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
   hiddenMinicardLabelText() {
   hiddenMinicardLabelText() {
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).hiddenMinicardLabelText;
       return (currentUser.profile || {}).hiddenMinicardLabelText;
+    } else if (cookies.has('hiddenMinicardLabelText')) {
+      return true;
     } else {
     } else {
-      if (cookies.has('hiddenMinicardLabelText')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
 });
 });

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

@@ -79,7 +79,7 @@
     border-radius: top 2px
     border-radius: top 2px
 
 
   .minicard-labels
   .minicard-labels
-    float: right
+    float: none
     display: flex
     display: flex
     flex-wrap: wrap
     flex-wrap: wrap
 
 

+ 3 - 1
client/components/cards/subtasks.jade

@@ -1,5 +1,7 @@
 template(name="subtasks")
 template(name="subtasks")
-  h3 {{_ 'subtasks'}}
+  h3
+    i.fa.fa-sitemap
+    | {{_ 'subtasks'}}
   if toggleDeleteDialog.get
   if toggleDeleteDialog.get
     .board-overlay#card-details-overlay
     .board-overlay#card-details-overlay
     +subtaskDeleteDialog(subtask = subtaskToDelete)
     +subtaskDeleteDialog(subtask = subtaskToDelete)

+ 22 - 4
client/components/cards/subtasks.js

@@ -3,7 +3,8 @@ BlazeComponent.extendComponent({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 }).register('subtaskDetail');
 }).register('subtaskDetail');
@@ -19,7 +20,22 @@ BlazeComponent.extendComponent({
     const crtBoard = Boards.findOne(card.boardId);
     const crtBoard = Boards.findOne(card.boardId);
     const targetBoard = crtBoard.getDefaultSubtasksBoard();
     const targetBoard = crtBoard.getDefaultSubtasksBoard();
     const listId = targetBoard.getDefaultSubtasksListId();
     const listId = targetBoard.getDefaultSubtasksListId();
-    const swimlaneId = targetBoard.getDefaultSwimline()._id;
+
+    //Get the full swimlane data for the parent task.
+    const parentSwimlane = Swimlanes.findOne({
+      boardId: crtBoard._id,
+      _id: card.swimlaneId,
+    });
+    //find the swimlane of the same name in the target board.
+    const targetSwimlane = Swimlanes.findOne({
+      boardId: targetBoard._id,
+      title: parentSwimlane.title,
+    });
+    //If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
+    const swimlaneId =
+      targetSwimlane === undefined
+        ? targetBoard.getDefaultSwimline()._id
+        : targetSwimlane._id;
 
 
     if (title) {
     if (title) {
       const _id = Cards.insert({
       const _id = Cards.insert({
@@ -55,7 +71,8 @@ BlazeComponent.extendComponent({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 
 
@@ -154,7 +171,8 @@ Template.subtaskItemDetail.helpers({
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 });
 });

+ 0 - 3
client/components/import/import.jade

@@ -15,9 +15,6 @@ template(name="importTextarea")
     p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
     p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
     textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
     textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
       | {{jsonText}}
       | {{jsonText}}
-    if isSandstorm
-      h1.warning {{_ 'import-sandstorm-backup-warning'}}
-      p.warning {{_ 'import-sandstorm-warning'}}
     input.primary.wide(type="submit" value="{{_ 'import'}}")
     input.primary.wide(type="submit" value="{{_ 'import'}}")
 
 
 template(name="importMapMembers")
 template(name="importMapMembers")

+ 16 - 13
client/components/lists/list.js

@@ -1,6 +1,6 @@
 import { Cookies } from 'meteor/ostrio:cookies';
 import { Cookies } from 'meteor/ostrio:cookies';
 const cookies = new Cookies();
 const cookies = new Cookies();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   // Proxy
   // Proxy
@@ -114,9 +114,6 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch(itemsSelector);
-
     this.autorun(() => {
     this.autorun(() => {
       let showDesktopDragHandles = false;
       let showDesktopDragHandles = false;
       currentUser = Meteor.user();
       currentUser = Meteor.user();
@@ -129,18 +126,26 @@ BlazeComponent.extendComponent({
         showDesktopDragHandles = false;
         showDesktopDragHandles = false;
       }
       }
 
 
-      if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+      if (Utils.isMiniScreen() || showDesktopDragHandles) {
         $cards.sortable({
         $cards.sortable({
           handle: '.handle',
           handle: '.handle',
         });
         });
-      } else {
+      } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
         $cards.sortable({
         $cards.sortable({
           handle: '.minicard',
           handle: '.minicard',
         });
         });
       }
       }
 
 
-      // Disable drag-dropping if the current user is not a board member or is comment only
-      $cards.sortable('option', 'disabled', !userIsMember());
+      if ($cards.data('uiSortable') || $cards.data('sortable')) {
+        $cards.sortable(
+          'option',
+          'disabled',
+          // Disable drag-dropping when user is not member
+          !userIsMember(),
+          // Not disable drag-dropping while in multi-selection mode
+          // MultiSelection.isActive() || !userIsMember(),
+        );
+      }
     });
     });
 
 
     // We want to re-run this function any time a card is added.
     // We want to re-run this function any time a card is added.
@@ -176,12 +181,10 @@ Template.list.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
 });
 });

+ 3 - 3
client/components/lists/list.styl

@@ -43,9 +43,6 @@
       background: white
       background: white
       margin: -3px 0 8px
       margin: -3px 0 8px
 
 
-.list-header-card-count
-  height: 35px
-
 .list-header-add
 .list-header-add
   flex: 0 0 auto
   flex: 0 0 auto
   padding: 20px 12px 4px
   padding: 20px 12px 4px
@@ -60,6 +57,9 @@
   background-color: #e4e4e4;
   background-color: #e4e4e4;
   border-bottom: 6px solid #e4e4e4;
   border-bottom: 6px solid #e4e4e4;
 
 
+  &.list-header-card-count
+    min-height: 35px
+    height: auto
 
 
   &.ui-sortable-handle
   &.ui-sortable-handle
     cursor: grab
     cursor: grab

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

@@ -189,7 +189,8 @@ BlazeComponent.extendComponent({
       !this.reachedWipLimit() &&
       !this.reachedWipLimit() &&
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 
 
@@ -410,7 +411,7 @@ BlazeComponent.extendComponent({
         type: 'board',
         type: 'board',
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
     return boards;
     return boards;
@@ -596,7 +597,7 @@ BlazeComponent.extendComponent({
         type: 'board',
         type: 'board',
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
     return boards;
     return boards;
@@ -742,9 +743,25 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   updateList() {
   updateList() {
+    // Use fallback when requestIdleCallback is not available on iOS and Safari
+    // https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/
+    checkIdleTime =
+      window.requestIdleCallback ||
+      function(handler) {
+        const startTime = Date.now();
+        return setTimeout(function() {
+          handler({
+            didTimeout: false,
+            timeRemaining() {
+              return Math.max(0, 50.0 - (Date.now() - startTime));
+            },
+          });
+        }, 1);
+      };
+
     if (this.spinnerInView()) {
     if (this.spinnerInView()) {
       this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
       this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
-      window.requestIdleCallback(() => this.updateList());
+      checkIdleTime(() => this.updateList());
     }
     }
   },
   },
 
 

+ 36 - 14
client/components/lists/listHeader.jade

@@ -10,7 +10,7 @@ template(name="listHeader")
           a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
           a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
       h2.list-header-name(
       h2.list-header-name(
         title="{{ moment modifiedAt 'LLL' }}"
         title="{{ moment modifiedAt 'LLL' }}"
-        class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}js-open-inlined-form is-editable{{/unless}}{{/if}}")
+        class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
         +viewer
         +viewer
           = title
           = title
         if wipLimit.enabled
         if wipLimit.enabled
@@ -30,7 +30,6 @@ template(name="listHeader")
               if canSeeAddCard
               if canSeeAddCard
                 a.js-add-card.fa.fa-plus.list-header-plus-icon
                 a.js-add-card.fa.fa-plus.list-header-plus-icon
               a.fa.fa-navicon.js-open-list-menu
               a.fa.fa-navicon.js-open-list-menu
-          a.list-header-handle.handle.fa.fa-arrows.js-list-handle
         else
         else
           a.list-header-menu-icon.fa.fa-angle-right.js-select-list
           a.list-header-menu-icon.fa.fa-angle-right.js-select-list
           a.list-header-handle.handle.fa.fa-arrows.js-list-handle
           a.list-header-handle.handle.fa.fa-arrows.js-list-handle
@@ -56,25 +55,47 @@ template(name="editListTitleForm")
 
 
 template(name="listActionPopup")
 template(name="listActionPopup")
   ul.pop-over-list
   ul.pop-over-list
-    li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
+    li
+      a.js-toggle-watch-list
+        if isWatching
+          i.fa.fa-eye
+          |  {{_ 'unwatch'}}
+        else
+          i.fa.fa-eye-slash
+          |  {{_ 'watch'}}
   unless currentUser.isCommentOnly
   unless currentUser.isCommentOnly
-    hr
-    ul.pop-over-list
-      li: a.js-set-color-list {{_ 'set-color-list'}}
-    hr
+    unless currentUser.isWorker
+      ul.pop-over-list
+        li
+          a.js-set-color-list
+            i.fa.fa-paint-brush
+            | {{_ 'set-color-list'}}
     ul.pop-over-list
     ul.pop-over-list
       if cards.count
       if cards.count
-        li: a.js-select-cards {{_ 'list-select-cards'}}
-        hr
+        li
+          a.js-select-cards
+            i.fa.fa-check-square
+            | {{_ 'list-select-cards'}}
     if currentUser.isBoardAdmin
     if currentUser.isBoardAdmin
       ul.pop-over-list
       ul.pop-over-list
-        li: a.js-set-wip-limit {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
+        li
+          a.js-set-wip-limit
+            i.fa.fa-ban
+            | {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
+    unless currentUser.isWorker
       hr
       hr
-    ul.pop-over-list
-      li: a.js-close-list {{_ 'archive-list'}}
+      ul.pop-over-list
+        li
+          a.js-close-list
+            i.fa.fa-arrow-right
+            i.fa.fa-archive
+            | {{_ 'archive-list'}}
     hr
     hr
     ul.pop-over-list
     ul.pop-over-list
-      li: a.js-more {{_ 'listMorePopup-title'}}
+      li
+        a.js-more
+          i.fa.fa-link
+          | {{_ 'listMorePopup-title'}}
 
 
 template(name="boardLists")
 template(name="boardLists")
   ul.pop-over-list
   ul.pop-over-list
@@ -94,7 +115,8 @@ template(name="listMorePopup")
       input.inline-input(type="text" readonly value="{{ rootUrl }}")
       input.inline-input(type="text" readonly value="{{ rootUrl }}")
     | {{_ 'added'}}
     | {{_ 'added'}}
     span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
     span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
-    a.js-delete {{_ 'delete'}}
+    unless currentUser.isWorker
+      a.js-delete {{_ 'delete'}}
 
 
 template(name="listDeletePopup")
 template(name="listDeletePopup")
   p {{_ "list-delete-pop"}}
   p {{_ "list-delete-pop"}}

+ 7 - 8
client/components/lists/listHeader.js

@@ -9,9 +9,10 @@ BlazeComponent.extendComponent({
   canSeeAddCard() {
   canSeeAddCard() {
     const list = Template.currentData();
     const list = Template.currentData();
     return (
     return (
-      !list.getWipLimit('enabled') ||
-      list.getWipLimit('soft') ||
-      !this.reachedWipLimit()
+      (!list.getWipLimit('enabled') ||
+        list.getWipLimit('soft') ||
+        !this.reachedWipLimit()) &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 
 
@@ -109,12 +110,10 @@ Template.listHeader.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
 });
 });

+ 64 - 128
client/components/main/editor.js

@@ -1,87 +1,3 @@
-import _sanitizeXss from 'xss';
-const ASIS = 'asis';
-const sanitizeXss = (input, options) => {
-  const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i;
-  const allowedIframeSrcRegex = (function() {
-    let reg = defaultAllowedIframeSrc;
-    const SAFE_IFRAME_SRC_PATTERN =
-      Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN;
-    try {
-      if (SAFE_IFRAME_SRC_PATTERN !== undefined) {
-        reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i');
-      }
-    } catch (e) {
-      /*eslint no-console: ["error", { allow: ["warn", "error"] }] */
-
-      console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e);
-    }
-    return reg;
-  })();
-  const targetWindow = '_blank';
-  const getHtmlDOM = html => {
-    const i = document.createElement('i');
-    i.innerHTML = html;
-    return i.firstChild;
-  };
-  options = {
-    onTag(tag, html, options) {
-      const htmlDOM = getHtmlDOM(html);
-      const getAttr = attr => {
-        return htmlDOM && attr && htmlDOM.getAttribute(attr);
-      };
-      if (tag === 'iframe') {
-        const clipCls = 'note-vide-clip';
-        if (!options.isClosing) {
-          const iframeCls = getAttr('class');
-          let safe = iframeCls.indexOf(clipCls) > -1;
-          const src = getAttr('src');
-          if (allowedIframeSrcRegex.exec(src)) {
-            safe = true;
-          }
-          if (safe)
-            return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`;
-        } else {
-          // remove </iframe> tag
-          return '';
-        }
-      } else if (tag === 'a') {
-        if (!options.isClosing) {
-          if (getAttr(ASIS) === 'true') {
-            // if has a ASIS attribute, don't do anything, it's a member id
-            return html;
-          } else {
-            const href = getAttr('href');
-            if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) {
-              // a valid url
-              return `<a href=${href} target=${targetWindow}>`;
-            }
-          }
-        }
-      } else if (tag === 'img') {
-        if (!options.isClosing) {
-          const src = getAttr('src');
-          if (src) {
-            return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`;
-          }
-        }
-      }
-      return undefined;
-    },
-    onTagAttr(tag, name, value) {
-      if (tag === 'img' && name === 'src') {
-        if (value && value.substr(0, 5) === 'data:') {
-          // allow image with dataURI src
-          return `${name}='${value}'`;
-        }
-      } else if (tag === 'a' && name === 'target') {
-        return `${name}='${targetWindow}'`; // always change a href target to a new window
-      }
-      return undefined;
-    },
-    ...options,
-  };
-  return _sanitizeXss(input, options);
-};
 Template.editor.onRendered(() => {
 Template.editor.onRendered(() => {
   const textareaSelector = 'textarea';
   const textareaSelector = 'textarea';
   const mentions = [
   const mentions = [
@@ -94,13 +10,7 @@ Template.editor.onRendered(() => {
           currentBoard
           currentBoard
             .activeMembers()
             .activeMembers()
             .map(member => {
             .map(member => {
-              const user = Users.findOne(member.userId);
-              if (user._id === Meteor.userId()) {
-                return null;
-              }
-              const value = user.username;
-              const username =
-                value && value.match(/\s+/) ? `"${value}"` : value;
+              const username = Users.findOne(member.userId).username;
               return username.includes(term) ? username : null;
               return username.includes(term) ? username : null;
             })
             })
             .filter(Boolean),
             .filter(Boolean),
@@ -126,10 +36,9 @@ Template.editor.onRendered(() => {
       ? [
       ? [
           ['view', ['fullscreen']],
           ['view', ['fullscreen']],
           ['table', ['table']],
           ['table', ['table']],
-          ['font', ['bold']],
-          ['color', ['color']],
-          ['insert', ['video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
+          ['font', ['bold', 'underline']],
           //['fontsize', ['fontsize']],
           //['fontsize', ['fontsize']],
+          ['color', ['color']],
         ]
         ]
       : [
       : [
           ['style', ['style']],
           ['style', ['style']],
@@ -139,11 +48,47 @@ Template.editor.onRendered(() => {
           ['color', ['color']],
           ['color', ['color']],
           ['para', ['ul', 'ol', 'paragraph']],
           ['para', ['ul', 'ol', 'paragraph']],
           ['table', ['table']],
           ['table', ['table']],
-          ['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
+          //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
           //['insert', ['link', 'picture']], // modal popup has issue somehow :(
           //['insert', ['link', 'picture']], // modal popup has issue somehow :(
           ['view', ['fullscreen', 'help']],
           ['view', ['fullscreen', 'help']],
         ];
         ];
-    const cleanPastedHTML = sanitizeXss;
+    const cleanPastedHTML = function(input) {
+      const badTags = [
+        'style',
+        'script',
+        'applet',
+        'embed',
+        'noframes',
+        'noscript',
+        'meta',
+        'link',
+        'button',
+        'form',
+      ].join('|');
+      const badPatterns = new RegExp(
+        `(?:${[
+          `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`,
+          `<(${badTags})[^>]*?\\/>`,
+        ].join('|')})`,
+        'gi',
+      );
+      let output = input;
+      // remove bad Tags
+      output = output.replace(badPatterns, '');
+      // remove attributes ' style="..."'
+      const badAttributes = new RegExp(
+        `(?:${[
+          'on\\S+=([\'"]?).*?\\1',
+          'href=([\'"]?)javascript:.*?\\2',
+          'style=([\'"]?).*?\\3',
+          'target=\\S+',
+        ].join('|')})`,
+        'gi',
+      );
+      output = output.replace(badAttributes, '');
+      output = output.replace(/(<a )/gi, '$1target=_ '); // always to new target
+      return output;
+    };
     const editor = '.editor';
     const editor = '.editor';
     const selectors = [
     const selectors = [
       `.js-new-comment-form ${editor}`,
       `.js-new-comment-form ${editor}`,
@@ -163,37 +108,14 @@ Template.editor.onRendered(() => {
         }
         }
         return undefined;
         return undefined;
       };
       };
-      let popupShown = false;
       inputs.each(function(idx, input) {
       inputs.each(function(idx, input) {
         mSummernotes[idx] = $(input).summernote({
         mSummernotes[idx] = $(input).summernote({
           placeholder,
           placeholder,
           callbacks: {
           callbacks: {
-            onKeydown(e) {
-              if (popupShown) {
-                e.preventDefault();
-              }
-            },
-            onKeyup(e) {
-              if (popupShown) {
-                e.preventDefault();
-              }
-            },
             onInit(object) {
             onInit(object) {
               const originalInput = this;
               const originalInput = this;
-              const setAutocomplete = function(jEditor) {
-                if (jEditor !== undefined) {
-                  jEditor.escapeableTextComplete(mentions).on({
-                    'textComplete:show'() {
-                      popupShown = true;
-                    },
-                    'textComplete:hide'() {
-                      popupShown = false;
-                    },
-                  });
-                }
-              };
               $(originalInput).on('submitted', function() {
               $(originalInput).on('submitted', function() {
-                // resetCommentInput has been called
+                // when comment is submitted, the original textarea will be set to '', so shall we
                 if (!this.value) {
                 if (!this.value) {
                   const sn = getSummernote(this);
                   const sn = getSummernote(this);
                   sn && sn.summernote('code', '');
                   sn && sn.summernote('code', '');
@@ -201,7 +123,9 @@ Template.editor.onRendered(() => {
               });
               });
               const jEditor = object && object.editable;
               const jEditor = object && object.editable;
               const toolbar = object && object.toolbar;
               const toolbar = object && object.toolbar;
-              setAutocomplete(jEditor);
+              if (jEditor !== undefined) {
+                jEditor.escapeableTextComplete(mentions);
+              }
               if (toolbar !== undefined) {
               if (toolbar !== undefined) {
                 const fBtn = toolbar.find('.btn-fullscreen');
                 const fBtn = toolbar.find('.btn-fullscreen');
                 fBtn.on('click', function() {
                 fBtn.on('click', function() {
@@ -211,6 +135,7 @@ Template.editor.onRendered(() => {
                 });
                 });
               }
               }
             },
             },
+
             onImageUpload(files) {
             onImageUpload(files) {
               const $summernote = getSummernote(this);
               const $summernote = getSummernote(this);
               if (files && files.length > 0) {
               if (files && files.length > 0) {
@@ -289,6 +214,12 @@ Template.editor.onRendered(() => {
               const thisNote = this;
               const thisNote = this;
               const updatePastedText = function(object) {
               const updatePastedText = function(object) {
                 const someNote = getSummernote(object);
                 const someNote = getSummernote(object);
+                // Fix Pasting text into a card is adding a line before and after
+                // (and multiplies by pasting more) by changing paste "p" to "br".
+                // Fixes https://github.com/wekan/wekan/2890 .
+                // == Fix Start ==
+                someNote.execCommand('defaultParagraphSeparator', false, 'br');
+                // == Fix End ==
                 const original = someNote.summernote('code');
                 const original = someNote.summernote('code');
                 const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
                 const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
                 someNote.summernote('code', ''); //clear original
                 someNote.summernote('code', ''); //clear original
@@ -331,6 +262,8 @@ Template.editor.onRendered(() => {
   }
   }
 });
 });
 
 
+import sanitizeXss from 'xss';
+
 // XXX I believe we should compute a HTML rendered field on the server that
 // XXX I believe we should compute a HTML rendered field on the server that
 // would handle markdown and user mentions. We can simply have two
 // would handle markdown and user mentions. We can simply have two
 // fields, one source, and one compiled version (in HTML) and send only the
 // fields, one source, and one compiled version (in HTML) and send only the
@@ -352,7 +285,7 @@ Blaze.Template.registerHelper(
       }
       }
       return member;
       return member;
     });
     });
-    const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username
+    const mentionRegex = /\B@([\w.]*)/gi;
 
 
     let currentMention;
     let currentMention;
     while ((currentMention = mentionRegex.exec(content)) !== null) {
     while ((currentMention = mentionRegex.exec(content)) !== null) {
@@ -368,7 +301,12 @@ Blaze.Template.registerHelper(
       if (knowedUser.userId === Meteor.userId()) {
       if (knowedUser.userId === Meteor.userId()) {
         linkClass += ' me';
         linkClass += ' me';
       }
       }
-      const link = HTML.A(
+      // This @user mention link generation did open same Wekan
+      // window in new tab, so now A is changed to U so it's
+      // underlined and there is no link popup. This way also
+      // text can be selected more easily.
+      //const link = HTML.A(
+      const link = HTML.U(
         {
         {
           class: linkClass,
           class: linkClass,
           // XXX Hack. Since we stringify this render function result below with
           // XXX Hack. Since we stringify this render function result below with
@@ -376,16 +314,17 @@ Blaze.Template.registerHelper(
           // `userId` to the popup as usual, and we need to store it in the DOM
           // `userId` to the popup as usual, and we need to store it in the DOM
           // using a data attribute.
           // using a data attribute.
           'data-userId': knowedUser.userId,
           'data-userId': knowedUser.userId,
-          [ASIS]: 'true',
         },
         },
         linkValue,
         linkValue,
       );
       );
 
 
       content = content.replace(fullMention, Blaze.toHTML(link));
       content = content.replace(fullMention, Blaze.toHTML(link));
     }
     }
+
     return HTML.Raw(sanitizeXss(content));
     return HTML.Raw(sanitizeXss(content));
   }),
   }),
 );
 );
+
 Template.viewer.events({
 Template.viewer.events({
   // Viewer sometimes have click-able wrapper around them (for instance to edit
   // Viewer sometimes have click-able wrapper around them (for instance to edit
   // the corresponding text). Clicking a link shouldn't fire these actions, stop
   // the corresponding text). Clicking a link shouldn't fire these actions, stop
@@ -397,10 +336,7 @@ Template.viewer.events({
       Popup.open('member').call({ userId }, event, templateInstance);
       Popup.open('member').call({ userId }, event, templateInstance);
     } else {
     } else {
       const href = event.currentTarget.href;
       const href = event.currentTarget.href;
-      const child = event.currentTarget.firstElementChild;
-      if (child && child.tagName === 'IMG') {
-        prevent = false;
-      } else if (href) {
+      if (href) {
         window.open(href, '_blank');
         window.open(href, '_blank');
       }
       }
     }
     }

+ 7 - 0
client/components/main/header.jade

@@ -24,6 +24,11 @@ template(name="header")
             a(href="{{pathFor 'home'}}")
             a(href="{{pathFor 'home'}}")
               span.fa.fa-home
               span.fa.fa-home
               | {{_ 'all-boards'}}
               | {{_ 'all-boards'}}
+          li.separator -
+          li
+            a(href="{{pathFor 'public'}}")
+              span.fa.fa-globe
+              | {{_ 'public'}}
           each currentUser.starredBoards
           each currentUser.starredBoards
             li.separator -
             li.separator -
             li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
             li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
@@ -35,6 +40,8 @@ template(name="header")
       a#header-new-board-icon.js-create-board
       a#header-new-board-icon.js-create-board
         i.fa.fa-plus(title="Create a new board")
         i.fa.fa-plus(title="Create a new board")
 
 
+      +notifications
+
       +headerUserBar
       +headerUserBar
 
 
   #header(class=currentBoard.colorClass)
   #header(class=currentBoard.colorClass)

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

@@ -99,7 +99,7 @@
   height: 28px
   height: 28px
   font-size: 12px
   font-size: 12px
   display: flex
   display: flex
-  z-index: 17
+  z-index: 21
 
 
   #header-user-bar,
   #header-user-bar,
   #header-new-board-icon,
   #header-new-board-icon,
@@ -127,7 +127,7 @@
       &.current
       &.current
         color: darken(white, 5%)
         color: darken(white, 5%)
 
 
-      &:first-child .fa-home
+      &:first-child .fa-home,&:nth-child(3) .fa-globe
         margin-right: 5px
         margin-right: 5px
 
 
       a.js-create-board
       a.js-create-board
@@ -175,7 +175,7 @@
       .board-header-btn
       .board-header-btn
         height: 32px
         height: 32px
         line-height: @height
         line-height: @height
-        font-size: 16px
+        font-size: 15px
 
 
         i.fa
         i.fa
           line-height: 32px
           line-height: 32px
@@ -218,6 +218,9 @@
       padding: 10px
       padding: 10px
       margin: -10px 0 -10px -10px
       margin: -10px 0 -10px -10px
 
 
+.announcement .viewer
+  display: inline-block
+
 .announcement,
 .announcement,
 .offline-warning
 .offline-warning
   width: 100%
   width: 100%

+ 10 - 4
client/components/main/layouts.jade

@@ -6,10 +6,16 @@ head
     where the application is deployed with a path prefix, but it seems to be
     where the application is deployed with a path prefix, but it seems to be
     difficult to do that cleanly with Blaze -- at least without adding extra
     difficult to do that cleanly with Blaze -- at least without adding extra
     packages.
     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")
+  link(rel="shortcut icon" type="image/x-icon" href="/favicon.ico")
+  link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png")
+  link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png")
+  link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png")
+  link(rel="manifest" href="/site.webmanifest")
+  link(rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5")
+  meta(name="apple-mobile-web-app-title" content="Wekan")
+  meta(name="application-name" content="Wekan")
+  meta(name="msapplication-TileColor" content="#00aba9")
+  meta(name="theme-color" content="#ffffff")
 
 
 template(name="userFormsLayout")
 template(name="userFormsLayout")
   section.auth-layout
   section.auth-layout

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

@@ -31,6 +31,11 @@ Template.userFormsLayout.onCreated(function() {
       return this.stop();
       return this.stop();
     },
     },
   });
   });
+  Meteor.call('isPasswordLoginDisabled', (_, result) => {
+    if (result) {
+      $('.at-pwd-form').hide();
+    }
+  });
 });
 });
 
 
 Template.userFormsLayout.onRendered(() => {
 Template.userFormsLayout.onRendered(() => {
@@ -73,6 +78,8 @@ Template.userFormsLayout.helpers({
         name = 'Igbo';
         name = 'Igbo';
       } else if (lang.name === 'oc') {
       } else if (lang.name === 'oc') {
         name = 'Occitan';
         name = 'Occitan';
+      } else if (lang.name === '繁体中文(台湾)') {
+        name = '繁體中文(台灣)';
       }
       }
       return { tag, name };
       return { tag, name };
     }).sort(function(a, b) {
     }).sort(function(a, b) {

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

@@ -135,6 +135,10 @@ $popupWidth = 300px
   margin-bottom: 8px
   margin-bottom: 8px
 
 
 .pop-over-list
 .pop-over-list
+  li
+    display: block
+    clear: both
+
   li > a
   li > a
     clear: both
     clear: both
     cursor: pointer
     cursor: pointer
@@ -316,6 +320,7 @@ $popupWidth = 300px
         input[type="file"]
         input[type="file"]
           margin: 4px 0 12px
           margin: 4px 0 12px
           width: 100%
           width: 100%
+          box-sizing: border-box
 
 
     .pop-over-list
     .pop-over-list
       li > a
       li > a

+ 10 - 0
client/components/notifications/notification.jade

@@ -0,0 +1,10 @@
+template(name='notification')
+  li.notification(class="{{#if read}}read{{/if}}")
+    .read-status
+      .materialCheckBox(class="{{#if read}}is-checked{{/if}}")
+      +notificationIcon(activityData)
+    .details
+      +activity(activity=activityData mode='none')
+    if read
+      .remove
+        a.fa.fa-trash

+ 28 - 0
client/components/notifications/notification.js

@@ -0,0 +1,28 @@
+Template.notification.events({
+  'click .read-status .materialCheckBox'() {
+    const update = {};
+    update[`profile.notifications.${this.index}.read`] = this.read
+      ? null
+      : Date.now();
+    Users.update(Meteor.userId(), { $set: update });
+  },
+  'click .remove a'() {
+    Meteor.user().removeNotification(this.activityData._id);
+  },
+});
+
+Template.notification.helpers({
+  mode: 'board',
+  isOfActivityType(activityId, type) {
+    const activity = Activities.findOne(activityId);
+    return activity && activity.activityType === type;
+  },
+  activityType(activityId) {
+    const activity = Activities.findOne(activityId);
+    return activity ? activity.activityType : '';
+  },
+  activityUser(activityId) {
+    const activity = Activities.findOne(activityId);
+    return activity && activity.userId;
+  },
+});

+ 57 - 0
client/components/notifications/notification.styl

@@ -0,0 +1,57 @@
+#notifications-drawer
+  &.show-read .notification.read
+    display: flex
+
+  .notification
+    display: flex
+    float: none
+    padding: 12px 8px 8px
+    color: black
+    border-bottom: 1px solid #dbdbdb
+
+    &.read
+      display: none
+
+    .read-status
+      width: 30px
+
+      input
+        width: 24px
+        height: 24px
+
+      .activity-type
+        margin: 16px 0 0
+        width: 17px
+        height: 17px
+        font-size: 17px
+        display: block
+        color: #bbb
+
+    .details
+      width: calc(100% - 30px)
+
+      .activity
+        display: flex
+
+        .activity-desc
+          width: 100%;
+
+        .activity-comment
+          display: block
+          width: 100%
+          border-radius: 3px
+          background: #fff
+          text-decoration: none
+          box-shadow: 0 1px 2px rgba(0,0,0,0.2)
+          margin-top: 5px
+          padding: 5px
+
+        .activity-meta
+          display: block
+          font-size: 0.8em
+          color: #999
+          font-style: italic
+
+    .remove
+      a:hover
+        color #eb4646 !important

+ 53 - 0
client/components/notifications/notificationIcon.jade

@@ -0,0 +1,53 @@
+template(name='notificationIcon')
+  if($in activityType 'deleteAttachment' 'addAttachment')
+    i.fa.fa-paperclip.activity-type(title="attachment")
+  else if($in activityType 'createBoard' 'importBoard')
+    i.fa.fa-chalkboard.activity-type(title="board")
+
+  else if($in activityType 'createCard' 'importCard' 'moveCard')
+    +cardNotificationIcon
+  else if($in activityType 'moveCardBoard' 'archivedCard' 'restoredCard')
+    +cardNotificationIcon
+    //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
+    //- DRY and consistant
+
+  else if($in activityType 'addChecklist' 'removedChecklist' 'completeChecklist')
+    +checklistNotificationIcon
+  else if($in activityType 'uncompleteChecklist')
+    +checklistNotificationIcon
+    //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
+    //- DRY and consistant
+
+  else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
+    i.fa.fa-check-square.activity-type(title="checklist item")
+  else if($in activityType 'addComment')
+    i.fa.fa-comment-o.activity-type(title="comment")
+  else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
+    i.fa.fa-code.activity-type(title="custom field")
+  else if($in activityType 'addedLabel' 'removedLabel')
+    i.fa.fa-tag.activity-type(title="label")
+
+  else if($in activityType 'createList' 'removeList' 'archivedList')
+    +listNotificationIcon
+  else if($in activityType  'importList')
+    +listNotificationIcon
+    //- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
+    //- DRY and consistant
+
+    //- elswhere in the app we use fa-trello to indicate lists...
+    //- i personally like fa-columns a bit better
+  else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember')
+    i.fa.fa-user.activity-type(title="member")
+  else if($in activityType 'createSwimlane' 'archivedSwimlane')
+    i.fa.fa-th-large.activity-type(title="swimlane")
+  else
+    i.fa.fa-bug.activity-type(title="can't find icon for #{activityType}")
+
+template(name='cardNotificationIcon')
+  i.fa.fa-clone.activity-type(title="card")
+
+template(name='checklistNotificationIcon')
+  i.fa.fa-list.activity-type(title="checklist")
+
+template(name='listNotificationIcon')
+  i.fa.fa-columns.activity-type(title="list")

+ 5 - 0
client/components/notifications/notifications.jade

@@ -0,0 +1,5 @@
+template(name='notifications')
+  #notifications.board-header-btns.right
+    a.notifications-drawer-toggle.fa.fa-bell(class="{{#if $gt unreadNotifications 0}}alert{{/if}}")
+    if $.Session.get 'showNotificationsDrawer'
+      +notificationsDrawer(unreadNotifications=unreadNotifications)

+ 32 - 0
client/components/notifications/notifications.js

@@ -0,0 +1,32 @@
+// this hides the notifications drawer if anyone clicks off of the panel
+Template.body.events({
+  click(event) {
+    if (
+      !$(event.target).is('#notifications *') &&
+      Session.get('showNotificationsDrawer')
+    ) {
+      toggleNotificationsDrawer();
+    }
+  },
+});
+
+Template.notifications.helpers({
+  unreadNotifications() {
+    const notifications = Users.findOne(Meteor.userId()).notifications();
+    const unreadNotifications = _.filter(notifications, v => !v.read);
+    return unreadNotifications.length;
+  },
+});
+
+Template.notifications.events({
+  'click .notifications-drawer-toggle'() {
+    toggleNotificationsDrawer();
+  },
+});
+
+export function toggleNotificationsDrawer() {
+  Session.set(
+    'showNotificationsDrawer',
+    !Session.get('showNotificationsDrawer'),
+  );
+}

+ 17 - 0
client/components/notifications/notifications.styl

@@ -0,0 +1,17 @@
+#notifications
+  position: relative
+
+  .notifications-drawer-toggle
+    display: block
+    line-height: 28px
+    color: #f2f2f2
+    margin: 0 10px
+    width: 28px
+    height: 28px
+    text-align: center
+    border: 0
+    padding: 0
+
+    &.alert
+      background-color: #eb4646;
+

+ 20 - 0
client/components/notifications/notificationsDrawer.jade

@@ -0,0 +1,20 @@
+template(name='notificationsDrawer')
+  section#notifications-drawer(class="{{#if $.Session.get 'showReadNotifications'}}show-read{{/if}}")
+    .header
+      if $.Session.get 'showReadNotifications'
+        a.toggle-read {{_ 'filter-by-unread'}}
+      else
+        a.toggle-read {{_ 'view-all'}}
+      h5 {{_ 'notifications'}}
+        if($gt unreadNotifications 0)
+          |(#{unreadNotifications})
+      a.fa.fa-times-thin.close
+    ul.notifications
+      each transformedProfile.notifications
+        +notification(activityData=activity index=dbIndex read=read)
+    if($gt unreadNotifications 0)
+      a.all-read {{_ 'mark-all-as-read'}}
+    if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
+      a.remove-read
+        i.fa.fa-trash
+        | {{_ 'remove-all-read'}}

+ 53 - 0
client/components/notifications/notificationsDrawer.js

@@ -0,0 +1,53 @@
+import { toggleNotificationsDrawer } from './notifications.js';
+
+Template.notificationsDrawer.onCreated(function() {
+  Meteor.subscribe('notificationActivities');
+  Meteor.subscribe('notificationCards');
+  Meteor.subscribe('notificationUsers');
+  Meteor.subscribe('notificationsAttachments');
+  Meteor.subscribe('notificationChecklistItems');
+  Meteor.subscribe('notificationChecklists');
+  Meteor.subscribe('notificationComments');
+  Meteor.subscribe('notificationLists');
+  Meteor.subscribe('notificationSwimlanes');
+});
+
+Template.notificationsDrawer.helpers({
+  transformedProfile() {
+    return Users.findOne(Meteor.userId());
+  },
+  readNotifications() {
+    const readNotifications = _.filter(
+      Meteor.user().profile.notifications,
+      v => !!v.read,
+    );
+    return readNotifications.length;
+  },
+});
+
+Template.notificationsDrawer.events({
+  'click .all-read'() {
+    const notifications = Meteor.user().profile.notifications;
+    for (const index in notifications) {
+      if (notifications.hasOwnProperty(index) && !notifications[index].read) {
+        const update = {};
+        update[`profile.notifications.${index}.read`] = Date.now();
+        Users.update(Meteor.userId(), { $set: update });
+      }
+    }
+  },
+  'click .close'() {
+    toggleNotificationsDrawer();
+  },
+  'click .toggle-read'() {
+    Session.set('showReadNotifications', !Session.get('showReadNotifications'));
+  },
+  'click .remove-read'() {
+    const user = Meteor.user();
+    for (const notification of user.profile.notifications) {
+      if (notification.read) {
+        user.removeNotification(notification.activity);
+      }
+    }
+  },
+});

+ 69 - 0
client/components/notifications/notificationsDrawer.styl

@@ -0,0 +1,69 @@
+belize = #2980b9
+
+section#notifications-drawer
+  position: fixed
+  top: 28px
+  right: 0
+  width: 400px
+  background-color: #fafafa
+  box-shadow: 0 1px 2px rgba(0,0,0,0.15)
+  border-radius: 2px
+  max-height: calc(100vh - 28px - 36px)
+  color: black
+  padding-top 36px
+
+  a:hover
+    color: belize !important
+
+  .header
+    position: fixed
+    top 28px
+    right 0
+    width calc(400px - 32px)
+    padding: 8px 16px
+    background: #ededed
+    border-bottom: 1px solid #dbdbdb
+    z-index 2
+
+    .toggle-read
+      position absolute
+      left 16px
+      top calc(50% - 8px)
+      color belize
+
+    h5
+      text-align: center
+      margin: 0
+
+    .close
+      position: absolute
+      top: calc(50% - 12px)
+      right: 12px
+      font-size: 24px
+      height: 24px
+      line-height: 24px
+      opacity 1
+
+  .all-read,
+  .remove-read
+    color belize
+    background-color: #fafafa
+    margin 8px 16px 12px
+    display inline-block
+
+  .remove-read
+    float right
+
+    &:hover
+      color #eb4646 !important
+
+      i.fa
+        color inherit
+
+
+  ul.notifications
+    display: block
+    padding: 0px 16px
+    margin: 0
+    height: calc(100vh - 102px)
+    overflow-y: scroll

+ 25 - 12
client/components/rules/actions/boardActions.jade

@@ -1,29 +1,42 @@
 template(name="boardActions")
 template(name="boardActions")
   div.trigger-item
   div.trigger-item
     div.trigger-content
     div.trigger-content
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-move-card-to'}}
         | {{_'r-move-card-to'}}
       div.trigger-dropdown
       div.trigger-dropdown
         select(id="move-gen-action")
         select(id="move-gen-action")
           option(value="top") {{_'r-top-of'}}
           option(value="top") {{_'r-top-of'}}
           option(value="bottom") {{_'r-bottom-of'}}
           option(value="bottom") {{_'r-bottom-of'}}
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-its-list'}}
         | {{_'r-its-list'}}
     div.trigger-button.js-add-gen-move-action.js-goto-rules
     div.trigger-button.js-add-gen-move-action.js-goto-rules
       i.fa.fa-plus
       i.fa.fa-plus
 
 
   div.trigger-item
   div.trigger-item
     div.trigger-content
     div.trigger-content
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-move-card-to'}}
         | {{_'r-move-card-to'}}
       div.trigger-dropdown
       div.trigger-dropdown
         select(id="move-spec-action")
         select(id="move-spec-action")
           option(value="top") {{_'r-top-of'}}
           option(value="top") {{_'r-top-of'}}
           option(value="bottom") {{_'r-bottom-of'}}
           option(value="bottom") {{_'r-bottom-of'}}
-      div.trigger-text 
-        | {{_'r-list'}}
+      div.trigger-text
+        | {{_'r-the-board'}}
+      div.trigger-dropdown
+        select(id="board-id")
+          each boards
+            if $eq _id currentBoard._id
+              option(value="{{_id}}" selected) {{_ 'current'}}
+            else
+              option(value="{{_id}}") {{title}}
+      div.trigger-text
+        | {{_'r-in-list'}}
       div.trigger-dropdown
       div.trigger-dropdown
         input(id="listName",type=text,placeholder="{{_'r-name'}}")
         input(id="listName",type=text,placeholder="{{_'r-name'}}")
+      div.trigger-text
+        | {{_'r-in-swimlane'}}
+      div.trigger-dropdown
+        input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}")
     div.trigger-button.js-add-spec-move-action.js-goto-rules
     div.trigger-button.js-add-spec-move-action.js-goto-rules
       i.fa.fa-plus
       i.fa.fa-plus
 
 
@@ -33,14 +46,14 @@ template(name="boardActions")
         select(id="arch-action")
         select(id="arch-action")
           option(value="archive") {{_'r-archive'}}
           option(value="archive") {{_'r-archive'}}
           option(value="unarchive") {{_'r-unarchive'}}
           option(value="unarchive") {{_'r-unarchive'}}
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-card'}}
         | {{_'r-card'}}
     div.trigger-button.js-add-arch-action.js-goto-rules
     div.trigger-button.js-add-arch-action.js-goto-rules
       i.fa.fa-plus
       i.fa.fa-plus
 
 
   div.trigger-item
   div.trigger-item
     div.trigger-content
     div.trigger-content
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-add-swimlane'}}
         | {{_'r-add-swimlane'}}
       div.trigger-dropdown
       div.trigger-dropdown
         input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
         input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
@@ -49,15 +62,15 @@ template(name="boardActions")
 
 
   div.trigger-item
   div.trigger-item
     div.trigger-content
     div.trigger-content
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-create-card'}}
         | {{_'r-create-card'}}
       div.trigger-dropdown
       div.trigger-dropdown
         input(id="card-name",type=text,placeholder="{{_'r-name'}}")
         input(id="card-name",type=text,placeholder="{{_'r-name'}}")
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-in-list'}}
         | {{_'r-in-list'}}
       div.trigger-dropdown
       div.trigger-dropdown
         input(id="list-name",type=text,placeholder="{{_'r-name'}}")
         input(id="list-name",type=text,placeholder="{{_'r-name'}}")
-      div.trigger-text 
+      div.trigger-text
         | {{_'r-in-swimlane'}}
         | {{_'r-in-swimlane'}}
       div.trigger-dropdown
       div.trigger-dropdown
         input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
         input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
@@ -65,8 +78,8 @@ template(name="boardActions")
       i.fa.fa-plus
       i.fa.fa-plus
 
 
 
 
-   
-  
+
+
 
 
 
 
 
 

+ 25 - 5
client/components/rules/actions/boardActions.js

@@ -1,6 +1,22 @@
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {},
   onCreated() {},
 
 
+  boards() {
+    const boards = Boards.find(
+      {
+        archived: false,
+        'members.userId': Meteor.userId(),
+        _id: {
+          $ne: Meteor.user().getTemplatesBoardId(),
+        },
+      },
+      {
+        sort: { sort: 1 /* boards default sorting */ },
+      },
+    );
+    return boards;
+  },
+
   events() {
   events() {
     return [
     return [
       {
       {
@@ -52,15 +68,18 @@ BlazeComponent.extendComponent({
           const ruleName = this.data().ruleName.get();
           const ruleName = this.data().ruleName.get();
           const trigger = this.data().triggerVar.get();
           const trigger = this.data().triggerVar.get();
           const actionSelected = this.find('#move-spec-action').value;
           const actionSelected = this.find('#move-spec-action').value;
-          const listTitle = this.find('#listName').value;
+          const swimlaneName = this.find('#swimlaneName').value;
+          const listName = this.find('#listName').value;
           const boardId = Session.get('currentBoard');
           const boardId = Session.get('currentBoard');
+          const destBoardId = this.find('#board-id').value;
           const desc = Utils.getTriggerActionDesc(event, this);
           const desc = Utils.getTriggerActionDesc(event, this);
           if (actionSelected === 'top') {
           if (actionSelected === 'top') {
             const triggerId = Triggers.insert(trigger);
             const triggerId = Triggers.insert(trigger);
             const actionId = Actions.insert({
             const actionId = Actions.insert({
               actionType: 'moveCardToTop',
               actionType: 'moveCardToTop',
-              listTitle,
-              boardId,
+              listName,
+              swimlaneName,
+              boardId: destBoardId,
               desc,
               desc,
             });
             });
             Rules.insert({
             Rules.insert({
@@ -74,8 +93,9 @@ BlazeComponent.extendComponent({
             const triggerId = Triggers.insert(trigger);
             const triggerId = Triggers.insert(trigger);
             const actionId = Actions.insert({
             const actionId = Actions.insert({
               actionType: 'moveCardToBottom',
               actionType: 'moveCardToBottom',
-              listTitle,
-              boardId,
+              listName,
+              swimlaneName,
+              boardId: destBoardId,
               desc,
               desc,
             });
             });
             Rules.insert({
             Rules.insert({

+ 6 - 2
client/components/settings/informationBody.jade

@@ -4,12 +4,16 @@ template(name='information')
       | {{_ 'error-notAuthorized'}}
       | {{_ 'error-notAuthorized'}}
     else
     else
       .content-title
       .content-title
-        span {{_ 'info'}}
+        span
+          i.fa.fa-info-circle
+          | {{_ 'info'}}
       .content-body
       .content-body
         .side-menu
         .side-menu
           ul
           ul
             li.active
             li.active
-              a.js-setting-menu(data-id="information-display") {{_ 'info'}}
+              a.js-setting-menu(data-id="information-display")
+                i.fa.fa-info-circle
+                | {{_ 'info'}}
         .main-body
         .main-body
           +statistics
           +statistics
 
 

+ 63 - 4
client/components/settings/peopleBody.jade

@@ -5,16 +5,22 @@ template(name="people")
     else
     else
       .content-title.ext-box
       .content-title.ext-box
         .ext-box-left
         .ext-box-left
-          span {{_ 'people'}}
+          span
+            i.fa.fa-users
+            | {{_ 'people'}}
           input#searchInput(placeholder="{{_ 'search'}}")
           input#searchInput(placeholder="{{_ 'search'}}")
-          button#searchButton {{_ 'search'}}
+          button#searchButton
+            i.fa.fa-search
+            | {{_ 'search'}}
         .ext-box-right
         .ext-box-right
           span {{_ 'people-number'}} #{peopleNumber}
           span {{_ 'people-number'}} #{peopleNumber}
       .content-body
       .content-body
         .side-menu
         .side-menu
           ul
           ul
             li.active
             li.active
-              a.js-setting-menu(data-id="people-setting") {{_ 'people'}}
+              a.js-setting-menu(data-id="people-setting")
+                i.fa.fa-users
+                | {{_ 'people'}}
         .main-body
         .main-body
           if loading.get
           if loading.get
             +spinner
             +spinner
@@ -34,9 +40,15 @@ template(name="peopleGeneral")
         th {{_ 'active'}}
         th {{_ 'active'}}
         th {{_ 'authentication-method'}}
         th {{_ 'authentication-method'}}
         th
         th
+          +newUserRow
       each user in peopleList
       each user in peopleList
         +peopleRow(userId=user._id)
         +peopleRow(userId=user._id)
 
 
+template(name="newUserRow")
+  a.new-user
+    i.fa.fa-edit
+    | {{_ 'new'}}
+
 template(name="peopleRow")
 template(name="peopleRow")
   tr
   tr
     if userData.loginDisabled
     if userData.loginDisabled
@@ -90,6 +102,7 @@ template(name="peopleRow")
       td {{_ userData.authenticationMethod }}
       td {{_ userData.authenticationMethod }}
     td
     td
       a.edit-user
       a.edit-user
+        i.fa.fa-edit
         | {{_ 'edit'}}
         | {{_ 'edit'}}
 
 
 template(name="editUserPopup")
 template(name="editUserPopup")
@@ -97,7 +110,7 @@ template(name="editUserPopup")
     label.hide.userId(type="text" value=user._id)
     label.hide.userId(type="text" value=user._id)
     label
     label
       | {{_ 'fullname'}}
       | {{_ 'fullname'}}
-      input.js-profile-fullname(type="text" value=user.profile.fullname autofocus)
+      input.js-profile-fullname(type="text" value=user.profile.fullname)
     label
     label
       | {{_ 'username'}}
       | {{_ 'username'}}
       span.error.hide.username-taken
       span.error.hide.username-taken
@@ -141,3 +154,49 @@ template(name="editUserPopup")
     //  div
     //  div
     //  input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}")
     //  input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}")
 
 
+template(name="newUserPopup")
+  form
+    //label.hide.userId(type="text" value=user._id)
+    label
+      | {{_ 'fullname'}}
+      input.js-profile-fullname(type="text" value="")
+    label
+      | {{_ 'username'}}
+      span.error.hide.username-taken
+        | {{_ 'error-username-taken'}}
+      //if isLdap
+      //  input.js-profile-username(type="text" value=user.username readonly)
+      //else
+      input.js-profile-username(type="text" value="")
+    label
+      | {{_ 'email'}}
+      span.error.hide.email-taken
+        | {{_ 'error-email-taken'}}
+      //if isLdap
+      //  input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
+      //else
+      input.js-profile-email(type="email" value="")
+    label
+      | {{_ 'admin'}}
+      select.select-role.js-profile-isadmin
+        option(value="false" selected="selected") {{_ 'no'}}
+        option(value="true") {{_ 'yes'}}
+    label
+      | {{_ 'active'}}
+      select.select-active.js-profile-isactive
+        option(value="false" selected="selected") {{_ 'yes'}}
+        option(value="true") {{_ 'no'}}
+    label
+      | {{_ 'authentication-type'}}
+      select.select-authenticationMethod.js-authenticationMethod
+        each authentications
+          if isSelected value
+            option(value="{{value}}" selected) {{_ value}}
+          else
+            option(value="{{value}}") {{_ value}}
+    hr
+    label
+      | {{_ 'password'}}
+      input.js-profile-password(type="password")
+    div.buttonsContainer
+      input.primary.wide(type="submit" value="{{_ 'save'}}")

+ 95 - 0
client/components/settings/peopleBody.js

@@ -39,6 +39,9 @@ BlazeComponent.extendComponent({
             this.filterPeople();
             this.filterPeople();
           }
           }
         },
         },
+        'click #newUserButton'() {
+          Popup.open('newUser');
+        },
       },
       },
     ];
     ];
   },
   },
@@ -141,6 +144,47 @@ Template.editUserPopup.helpers({
   },
   },
 });
 });
 
 
+Template.newUserPopup.onCreated(function() {
+  this.authenticationMethods = new ReactiveVar([]);
+  this.errorMessage = 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] })),
+      ]);
+    }
+  });
+});
+
+Template.newUserPopup.helpers({
+  //user() {
+  //  return Users.findOne(this.userId);
+  //},
+  authentications() {
+    return Template.instance().authenticationMethods.get();
+  },
+  //isSelected(match) {
+  //  const userId = Template.instance().data.userId;
+  //  const selected = Users.findOne(userId).authenticationMethod;
+  //  return selected === match;
+  //},
+  //isLdap() {
+  //  const userId = Template.instance().data.userId;
+  //  const selected = Users.findOne(userId).authenticationMethod;
+  //  return selected === 'ldap';
+  //},
+  errorMessage() {
+    return Template.instance().errorMessage.get();
+  },
+});
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {},
   onCreated() {},
   user() {
   user() {
@@ -155,6 +199,16 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('peopleRow');
 }).register('peopleRow');
 
 
+BlazeComponent.extendComponent({
+  events() {
+    return [
+      {
+        'click a.new-user': Popup.open('newUser'),
+      },
+    ];
+  },
+}).register('newUserRow');
+
 Template.editUserPopup.events({
 Template.editUserPopup.events({
   submit(event, templateInstance) {
   submit(event, templateInstance) {
     event.preventDefault();
     event.preventDefault();
@@ -248,3 +302,44 @@ Template.editUserPopup.events({
     Popup.close();
     Popup.close();
   }),
   }),
 });
 });
+
+Template.newUserPopup.events({
+  submit(event, templateInstance) {
+    event.preventDefault();
+    const fullname = templateInstance.find('.js-profile-fullname').value.trim();
+    const username = templateInstance.find('.js-profile-username').value.trim();
+    const password = templateInstance.find('.js-profile-password').value;
+    const isAdmin = templateInstance.find('.js-profile-isadmin').value.trim();
+    const isActive = templateInstance.find('.js-profile-isactive').value.trim();
+    const email = templateInstance.find('.js-profile-email').value.trim();
+
+    Meteor.call(
+      'setCreateUser',
+      fullname,
+      username,
+      password,
+      isAdmin,
+      isActive,
+      email.toLowerCase(),
+      function(error) {
+        const usernameMessageElement = templateInstance.$('.username-taken');
+        const emailMessageElement = templateInstance.$('.email-taken');
+        if (error) {
+          const errorElement = error.error;
+          if (errorElement === 'username-already-taken') {
+            usernameMessageElement.show();
+            emailMessageElement.hide();
+          } else if (errorElement === 'email-already-taken') {
+            usernameMessageElement.hide();
+            emailMessageElement.show();
+          }
+        } else {
+          usernameMessageElement.hide();
+          emailMessageElement.hide();
+          Popup.close();
+        }
+      },
+    );
+    Popup.close();
+  },
+});

+ 1 - 1
client/components/settings/peopleBody.styl

@@ -33,7 +33,7 @@ table
     padding: 0;
     padding: 0;
 
 
   button
   button
-    min-width: 60px;
+    min-width: 90px;
 
 
 .content-wrapper
 .content-wrapper
   margin-top: 10px
   margin-top: 10px

+ 19 - 12
client/components/settings/settingBody.jade

@@ -4,22 +4,35 @@ template(name="setting")
       | {{_ 'error-notAuthorized'}}
       | {{_ 'error-notAuthorized'}}
     else
     else
       .content-title
       .content-title
+        i.fa.fa-cog
         span {{_ 'settings'}}
         span {{_ 'settings'}}
       .content-body
       .content-body
         .side-menu
         .side-menu
           ul
           ul
             li.active
             li.active
-              a.js-setting-menu(data-id="registration-setting") {{_ 'registration'}}
+              a.js-setting-menu(data-id="registration-setting")
+                i.fa.fa-sign-in
+                | {{_ 'registration'}}
             li
             li
-              a.js-setting-menu(data-id="email-setting") {{_ 'email'}}
+              a.js-setting-menu(data-id="email-setting")
+                i.fa.fa-envelope
+                | {{_ 'email'}}
             li
             li
-              a.js-setting-menu(data-id="account-setting") {{_ 'accounts'}}
+              a.js-setting-menu(data-id="account-setting")
+                i.fa.fa-users
+                | {{_ 'accounts'}}
             li
             li
-              a.js-setting-menu(data-id="announcement-setting") {{_ 'admin-announcement'}}
+              a.js-setting-menu(data-id="announcement-setting")
+                i.fa.fa-bullhorn
+                | {{_ 'admin-announcement'}}
             li
             li
-              a.js-setting-menu(data-id="layout-setting") {{_ 'layout'}}
+              a.js-setting-menu(data-id="layout-setting")
+                i.fa.fa-object-group
+                | {{_ 'layout'}}
             li
             li
-              a.js-setting-menu(data-id="webhook-setting") {{_ 'global-webhook'}}
+              a.js-setting-menu(data-id="webhook-setting")
+                i.fa.fa-globe
+                | {{_ 'global-webhook'}}
         .main-body
         .main-body
           if loading.get
           if loading.get
             +spinner
             +spinner
@@ -171,12 +184,6 @@ template(name='layoutSettings')
       .title {{_ 'custom-product-name'}}
       .title {{_ 'custom-product-name'}}
       .form-group
       .form-group
         input.wekan-form-control#product-name(type="text", placeholder="" value="{{currentSetting.productName}}")
         input.wekan-form-control#product-name(type="text", placeholder="" value="{{currentSetting.productName}}")
-    li.layout-form
-      .title {{_ 'add-custom-html-after-body-start'}}
-      textarea#customHTMLafterBodyStart.wekan-form-control= currentSetting.customHTMLafterBodyStart
-    li.layout-form
-      .title {{_ 'add-custom-html-before-body-end'}}
-      textarea#customHTMLbeforeBodyEnd.wekan-form-control= currentSetting.customHTMLbeforeBodyEnd
     li
     li
       button.js-save-layout.primary {{_ 'save'}}
       button.js-save-layout.primary {{_ 'save'}}
 
 

+ 1 - 9
client/components/settings/settingBody.js

@@ -48,7 +48,7 @@ BlazeComponent.extendComponent({
         'members.isAdmin': true,
         'members.isAdmin': true,
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
   },
   },
@@ -171,20 +171,12 @@ BlazeComponent.extendComponent({
     const displayAuthenticationMethod =
     const displayAuthenticationMethod =
       $('input[name=displayAuthenticationMethod]:checked').val() === 'true';
       $('input[name=displayAuthenticationMethod]:checked').val() === 'true';
     const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
     const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
-    const customHTMLafterBodyStart = $('#customHTMLafterBodyStart')
-      .val()
-      .trim();
-    const customHTMLbeforeBodyEnd = $('#customHTMLbeforeBodyEnd')
-      .val()
-      .trim();
 
 
     try {
     try {
       Settings.update(Settings.findOne()._id, {
       Settings.update(Settings.findOne()._id, {
         $set: {
         $set: {
           productName,
           productName,
           hideLogo: hideLogoChange,
           hideLogo: hideLogoChange,
-          customHTMLafterBodyStart,
-          customHTMLbeforeBodyEnd,
           displayAuthenticationMethod,
           displayAuthenticationMethod,
           defaultAuthenticationMethod,
           defaultAuthenticationMethod,
         },
         },

+ 4 - 1
client/components/settings/settingBody.styl

@@ -41,15 +41,18 @@
           &:hover
           &:hover
             background #fff
             background #fff
             box-shadow 0 1px 2px rgba(0,0,0,0.15);
             box-shadow 0 1px 2px rgba(0,0,0,0.15);
+
           a
           a
             @extends .flex
             @extends .flex
             padding: 1rem 0 1rem 1rem
             padding: 1rem 0 1rem 1rem
             width: 100% - 5rem
             width: 100% - 5rem
 
 
-
             span
             span
               font-size: 13px
               font-size: 13px
 
 
+            i
+              margin-right: 20px
+
     .main-body
     .main-body
       padding: 0.1em 1em
       padding: 0.1em 1em
       -webkit-user-select: text // Safari 3.1+
       -webkit-user-select: text // Safari 3.1+

+ 200 - 29
client/components/sidebar/sidebar.jade

@@ -37,11 +37,12 @@ template(name='homeSidebar')
 template(name="membersWidget")
 template(name="membersWidget")
   .board-widget.board-widget-members
   .board-widget.board-widget-members
     h3
     h3
-      i.fa.fa-user
+      i.fa.fa-users
       | {{_ 'members'}}
       | {{_ 'members'}}
       unless currentUser.isCommentOnly
       unless currentUser.isCommentOnly
-        a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
-          i.board-header-btn-icon.fa.fa-cog
+        unless currentUser.isWorker
+          a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
+            i.board-header-btn-icon.fa.fa-cog
 
 
     .board-widget-content
     .board-widget-content
       each currentBoard.activeMembers
       each currentBoard.activeMembers
@@ -71,6 +72,108 @@ template(name="boardChangeColorPopup")
           if isSelected
           if isSelected
             i.fa.fa-check
             i.fa.fa-check
 
 
+template(name="boardCardSettingsPopup")
+  form.board-card-settings
+    h3 {{_ 'show-on-card'}}
+    div.check-div
+      a.flex.js-field-has-receiveddate(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
+        span
+          i.fa.fa-sign-out
+          | {{_ 'card-received'}}
+    div.check-div
+      a.flex.js-field-has-startdate(class="{{#if allowsStartDate}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsStartDate}}is-checked{{/if}}")
+        span
+          i.fa.fa-hourglass-start
+          | {{_ 'card-start'}}
+    div.check-div
+      a.flex.js-field-has-duedate(class="{{#if allowsDueDate}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsDueDate}}is-checked{{/if}}")
+        span
+          i.fa.fa-sign-in
+          | {{_ 'card-due'}}
+    div.check-div
+      a.flex.js-field-has-enddate(class="{{#if allowsEndDate}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsEndDate}}is-checked{{/if}}")
+        span
+          i.fa.fa-hourglass-end
+          | {{_ 'card-end'}}
+    div.check-div
+      a.flex.js-field-has-members(class="{{#if allowsMembers}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsMembers}}is-checked{{/if}}")
+        span
+          i.fa.fa-users
+          | {{_ 'members'}}
+    div.check-div
+      a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}")
+        span
+          i.fa.fa-user
+          | {{_ 'assignee'}}
+    div.check-div
+      a.flex.js-field-has-assigned-by(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
+        span
+          i.fa.fa-shopping-cart
+          | {{_ 'assigned-by'}}
+    div.check-div
+      a.flex.js-field-has-requested-by(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
+        span
+          i.fa.fa-user-plus
+          | {{_ 'requested-by'}}
+    div.check-div
+      a.flex.js-field-has-labels(class="{{#if allowsLabels}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsLabels}}is-checked{{/if}}")
+        span
+          i.fa.fa-tags
+          | {{_ 'labels'}}
+    div.check-div
+      a.flex.js-field-has-description-title(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
+        span
+          i.fa.fa-align-left
+          | {{_ 'description'}}
+          | {{_ 'title'}}
+    div.check-div
+      a.flex.js-field-has-description-text(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
+        span
+          i.fa.fa-align-left
+          | {{_ 'description'}}
+          | {{_ 'custom-field-text'}}
+    div.check-div
+      a.flex.js-field-has-checklists(class="{{#if allowsChecklists}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsChecklists}}is-checked{{/if}}")
+        span
+          i.fa.fa-check
+          | {{_ 'checklists'}}
+    div.check-div
+      a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
+        span
+          i.fa.fa-sitemap
+          | {{_ 'subtasks'}}
+    div.check-div
+      a.flex.js-field-has-attachments(class="{{#if allowsAttachments}}is-checked{{/if}}")
+        .materialCheckBox(class="{{#if allowsAttachments}}is-checked{{/if}}")
+        span
+          i.fa.fa-paperclip
+          | {{_ 'attachments'}}
+    //div.check-div
+    //  a.flex.js-field-has-comments(class="{{#if allowsComments}}is-checked{{/if}}")
+    //    .materialCheckBox(class="{{#if allowsComments}}is-checked{{/if}}")
+    //    span
+    //      i.fa.fa-comment-o
+    //      | {{_ 'comment'}}
+    //div.check-div
+    //  a.flex.js-field-has-activities(class="{{#if allowsActivities}}is-checked{{/if}}")
+    //    .materialCheckBox(class="{{#if allowsActivities}}is-checked{{/if}}")
+    //    span
+    //      i.fa.fa-history
+    //      | {{_ 'activities'}}
+
 template(name="boardSubtaskSettingsPopup")
 template(name="boardSubtaskSettingsPopup")
   form.board-subtask-settings
   form.board-subtask-settings
     h3 {{_ 'show-parent-in-minicard'}}
     h3 {{_ 'show-parent-in-minicard'}}
@@ -130,7 +233,9 @@ template(name="chooseBoardSource")
 
 
 template(name="archiveBoardPopup")
 template(name="archiveBoardPopup")
   p {{_ 'close-board-pop'}}
   p {{_ 'close-board-pop'}}
-  button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
+  button.js-confirm.negate.full(type="submit")
+    i.fa.fa-archive
+    | {{_ 'archive'}}
 
 
 template(name="outgoingWebhooksPopup")
 template(name="outgoingWebhooksPopup")
   each integrations
   each integrations
@@ -140,7 +245,7 @@ template(name="outgoingWebhooksPopup")
         b &nbsp;
         b &nbsp;
         .materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
         .materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
       input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
       input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
-      input.js-outgoing-webhooks-url(type="text" name="url" value=url autofocus)
+      input.js-outgoing-webhooks-url(type="text" name="url" value=url)
       input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
       input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
       select.js-outgoing-webhooks-type(name="type")
       select.js-outgoing-webhooks-type(name="type")
           each _type in types
           each _type in types
@@ -152,7 +257,7 @@ template(name="outgoingWebhooksPopup")
       input(type="hidden" value=_id name="id")
       input(type="hidden" value=_id name="id")
       input.primary.wide(type="submit" value="{{_ 'save'}}")
       input.primary.wide(type="submit" value="{{_ 'save'}}")
   form.integration-form
   form.integration-form
-    input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus)
+    input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title")
     input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
     input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
     input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
     input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
     select.js-outgoing-webhooks-type(name="type")
     select.js-outgoing-webhooks-type(name="type")
@@ -162,38 +267,98 @@ template(name="outgoingWebhooksPopup")
 
 
 template(name="boardMenuPopup")
 template(name="boardMenuPopup")
   ul.pop-over-list
   ul.pop-over-list
-    li: a.js-custom-fields {{_ 'custom-fields'}}
-    li: a.js-open-archives {{_ 'archived-items'}}
+    li
+      a.js-open-rules-view(title="{{_ 'rules'}}")
+        i.fa.fa-magic
+        | {{_ 'rules'}}
+    li
+      a.js-custom-fields
+        i.fa.fa-list-alt
+        | {{_ 'custom-fields'}}
+    li
+      a.js-open-archives
+        i.fa.fa-archive
+        | {{_ 'archived-items'}}
     if currentUser.isBoardAdmin
     if currentUser.isBoardAdmin
-      li: a.js-change-board-color {{_ 'board-change-color'}}
+      li
+        a.js-change-board-color
+          i.fa.fa-paint-brush
+          | {{_ 'board-change-color'}}
+
     //-
     //-
       XXX Language should be handled by sandstorm, but for now display a
       XXX Language should be handled by sandstorm, but for now display a
       language selection link in the board menu. This link is normally present
       language selection link in the board menu. This link is normally present
       in the header bar that is not displayed on sandstorm.
       in the header bar that is not displayed on sandstorm.
     if isSandstorm
     if isSandstorm
-      li: a.js-change-language {{_ 'language'}}
+      li
+        a.js-change-language
+          i.fa.fa-flag
+          | {{_ 'language'}}
   unless isSandstorm
   unless isSandstorm
     if currentUser.isBoardAdmin
     if currentUser.isBoardAdmin
       hr
       hr
       ul.pop-over-list
       ul.pop-over-list
-        li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
-        unless currentBoard.isTemplatesBoard
-          li: a.js-archive-board {{_ 'archive-board'}}
-        li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
-      hr
-      ul.pop-over-list
-        li: a.js-subtask-settings {{_ 'subtask-settings'}}
+        if withApi
+          li
+            a(href="{{exportUrl}}", download="{{exportFilename}}")
+              i.fa.fa-share-alt
+              | {{_ 'export-board'}}
+        li
+          a.js-outgoing-webhooks
+            i.fa.fa-globe
+            | {{_ 'outgoing-webhooks'}}
+        li
+          a.js-card-settings
+            i.fa.fa-id-card-o
+            | {{_ 'card-settings'}}
+        li
+          a.js-subtask-settings
+            i.fa.fa-sitemap
+            | {{_ 'subtask-settings'}}
+      unless currentBoard.isTemplatesBoard
+        hr
+        ul.pop-over-list
+          li
+            a.js-archive-board
+              i.fa.fa-arrow-right
+              i.fa.fa-archive
+              | {{_ 'archive-board'}}
 
 
   if isSandstorm
   if isSandstorm
     hr
     hr
     ul.pop-over-list
     ul.pop-over-list
-      li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
-      li: a.js-import-board {{_ 'import-board-c'}}
-      li: a.js-archive-board {{_ 'archive-board'}}
-      li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
+      if withApi
+        li
+          a(href="{{exportUrl}}", download="{{exportFilename}}")
+            i.fa.fa-share-alt
+            i.fa.fa-sign-out
+            | {{_ 'export-board'}}
+      li
+        a.js-import-board
+          i.fa.fa-share-alt
+          i.fa.fa-sign-in
+          | {{_ 'import-board-c'}}
+      li
+        a.js-archive-board
+          i.fa.fa-arrow-right
+          i.fa.fa-archive
+          | {{_ 'archive-board'}}
+      li
+        a.js-outgoing-webhooks
+          i.fa.fa-globe
+          | {{_ 'outgoing-webhooks'}}
     hr
     hr
     ul.pop-over-list
     ul.pop-over-list
-      li: a.js-subtask-settings {{_ 'subtask-settings'}}
+      li
+        a.js-card-settings
+          i.fa.fa-id-card-o
+          | {{_ 'card-settings'}}
+    hr
+    ul.pop-over-list
+      li
+        a.js-subtask-settings
+          i.fa.fa-sitemap
+          | {{_ 'subtask-settings'}}
 
 
 template(name="labelsWidget")
 template(name="labelsWidget")
   .board-widget.board-widget-labels
   .board-widget.board-widget-labels
@@ -203,7 +368,7 @@ template(name="labelsWidget")
     .board-widget-content
     .board-widget-content
       each currentBoard.labels
       each currentBoard.labels
           a.card-label(class="card-label-{{color}}"
           a.card-label(class="card-label-{{color}}"
-            class="{{#if currentUser.isNotCommentOnly}}js-label{{/if}}")
+            class="{{#if currentUser.isNotCommentOnly}}{{#if currentUser.isNotWorker}}js-label{{/if}}{{/if}}")
             span.card-label-name
             span.card-label-name
               +viewer
               +viewer
                 = name
                 = name
@@ -232,12 +397,12 @@ template(name="memberPopup")
           a.js-change-role
           a.js-change-role
             | {{_ 'change-permissions'}}
             | {{_ 'change-permissions'}}
             span.quiet (#{memberType})
             span.quiet (#{memberType})
-      li
-        if $eq currentUser._id userId
-          a.js-leave-member {{_ 'leave-board'}}
-        else if currentUser.isBoardAdmin
-          a.js-remove-member {{_ 'remove-from-board'}}
-
+      unless currentUser.isWorker
+        li
+          if $eq currentUser._id userId
+            a.js-leave-member {{_ 'leave-board'}}
+          else if currentUser.isBoardAdmin
+            a.js-remove-member {{_ 'remove-from-board'}}
 
 
 template(name="removeMemberPopup")
 template(name="removeMemberPopup")
   p {{_ 'remove-member-pop' name=user.profile.fullname username=user.username boardTitle=board.title}}
   p {{_ 'remove-member-pop' name=user.profile.fullname username=user.username boardTitle=board.title}}
@@ -301,6 +466,12 @@ template(name="changePermissionsPopup")
         if isCommentOnly
         if isCommentOnly
           i.fa.fa-check
           i.fa.fa-check
         span.sub-name {{_ 'comment-only-desc'}}
         span.sub-name {{_ 'comment-only-desc'}}
+    li
+      a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}")
+        | {{_ 'worker'}}
+        if isWorker
+          i.fa.fa-check
+        span.sub-name {{_ 'worker-desc'}}
   if isLastAdmin
   if isLastAdmin
     hr
     hr
     p.quiet.bottom {{_ 'last-admin-desc'}}
     p.quiet.bottom {{_ 'last-admin-desc'}}

+ 402 - 13
client/components/sidebar/sidebar.js

@@ -112,12 +112,10 @@ BlazeComponent.extendComponent({
           currentUser = Meteor.user();
           currentUser = Meteor.user();
           if (currentUser) {
           if (currentUser) {
             Meteor.call('toggleMinicardLabelText');
             Meteor.call('toggleMinicardLabelText');
+          } else if (cookies.has('hiddenMinicardLabelText')) {
+            cookies.remove('hiddenMinicardLabelText');
           } else {
           } else {
-            if (cookies.has('hiddenMinicardLabelText')) {
-              cookies.remove('hiddenMinicardLabelText');
-            } else {
-              cookies.set('hiddenMinicardLabelText', 'true');
-            }
+            cookies.set('hiddenMinicardLabelText', 'true');
           }
           }
         },
         },
         'click .js-shortcuts'() {
         'click .js-shortcuts'() {
@@ -135,12 +133,10 @@ Template.homeSidebar.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).hiddenMinicardLabelText;
       return (currentUser.profile || {}).hiddenMinicardLabelText;
+    } else if (cookies.has('hiddenMinicardLabelText')) {
+      return true;
     } else {
     } else {
-      if (cookies.has('hiddenMinicardLabelText')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
 });
 });
@@ -165,10 +161,13 @@ Template.memberPopup.helpers({
       const currentBoard = Boards.findOne(Session.get('currentBoard'));
       const currentBoard = Boards.findOne(Session.get('currentBoard'));
       const commentOnly = currentBoard.hasCommentOnly(this.userId);
       const commentOnly = currentBoard.hasCommentOnly(this.userId);
       const noComments = currentBoard.hasNoComments(this.userId);
       const noComments = currentBoard.hasNoComments(this.userId);
+      const worker = currentBoard.hasWorker(this.userId);
       if (commentOnly) {
       if (commentOnly) {
         return TAPi18n.__('comment-only').toLowerCase();
         return TAPi18n.__('comment-only').toLowerCase();
       } else if (noComments) {
       } else if (noComments) {
         return TAPi18n.__('no-comments').toLowerCase();
         return TAPi18n.__('no-comments').toLowerCase();
+      } else if (worker) {
+        return TAPi18n.__('worker').toLowerCase();
       } else {
       } else {
         return TAPi18n.__(type).toLowerCase();
         return TAPi18n.__(type).toLowerCase();
       }
       }
@@ -183,6 +182,10 @@ Template.memberPopup.helpers({
 
 
 Template.boardMenuPopup.events({
 Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
   'click .js-rename-board': Popup.open('boardChangeTitle'),
+  'click .js-open-rules-view'() {
+    Modal.openWide('rulesMain');
+    Popup.close();
+  },
   'click .js-custom-fields'() {
   'click .js-custom-fields'() {
     Sidebar.setView('customFields');
     Sidebar.setView('customFields');
     Popup.close();
     Popup.close();
@@ -209,9 +212,20 @@ Template.boardMenuPopup.events({
   'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
   'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
   'click .js-import-board': Popup.open('chooseBoardSource'),
   'click .js-import-board': Popup.open('chooseBoardSource'),
   'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
   'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
+  'click .js-card-settings': Popup.open('boardCardSettings'),
+});
+
+Template.boardMenuPopup.onCreated(function() {
+  this.apiEnabled = new ReactiveVar(false);
+  Meteor.call('_isApiEnabled', (e, result) => {
+    this.apiEnabled.set(result);
+  });
 });
 });
 
 
 Template.boardMenuPopup.helpers({
 Template.boardMenuPopup.helpers({
+  withApi() {
+    return Template.instance().apiEnabled.get();
+  },
   exportUrl() {
   exportUrl() {
     const params = {
     const params = {
       boardId: Session.get('currentBoard'),
       boardId: Session.get('currentBoard'),
@@ -271,6 +285,14 @@ Template.membersWidget.helpers({
     const user = Meteor.user();
     const user = Meteor.user();
     return user && user.isInvitedTo(Session.get('currentBoard'));
     return user && user.isInvitedTo(Session.get('currentBoard'));
   },
   },
+  isWorker() {
+    const user = Meteor.user();
+    if (user) {
+      return Meteor.call(Boards.hasWorker(user.memberId));
+    } else {
+      return false;
+    }
+  },
 });
 });
 
 
 Template.membersWidget.events({
 Template.membersWidget.events({
@@ -465,6 +487,10 @@ BlazeComponent.extendComponent({
     return this.currentBoard.allowsSubtasks;
     return this.currentBoard.allowsSubtasks;
   },
   },
 
 
+  allowsReceivedDate() {
+    return this.currentBoard.allowsReceivedDate;
+  },
+
   isBoardSelected() {
   isBoardSelected() {
     return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
     return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
   },
   },
@@ -483,7 +509,7 @@ BlazeComponent.extendComponent({
         'members.userId': Meteor.userId(),
         'members.userId': Meteor.userId(),
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
   },
   },
@@ -578,6 +604,359 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('boardSubtaskSettingsPopup');
 }).register('boardSubtaskSettingsPopup');
 
 
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentBoard = Boards.findOne(Session.get('currentBoard'));
+  },
+
+  allowsReceivedDate() {
+    return this.currentBoard.allowsReceivedDate;
+  },
+
+  allowsStartDate() {
+    return this.currentBoard.allowsStartDate;
+  },
+
+  allowsDueDate() {
+    return this.currentBoard.allowsDueDate;
+  },
+
+  allowsEndDate() {
+    return this.currentBoard.allowsEndDate;
+  },
+
+  allowsSubtasks() {
+    return this.currentBoard.allowsSubtasks;
+  },
+
+  allowsMembers() {
+    return this.currentBoard.allowsMembers;
+  },
+
+  allowsAssignee() {
+    return this.currentBoard.allowsAssignee;
+  },
+
+  allowsAssignedBy() {
+    return this.currentBoard.allowsAssignedBy;
+  },
+
+  allowsRequestedBy() {
+    return this.currentBoard.allowsRequestedBy;
+  },
+
+  allowsLabels() {
+    return this.currentBoard.allowsLabels;
+  },
+
+  allowsChecklists() {
+    return this.currentBoard.allowsChecklists;
+  },
+
+  allowsAttachments() {
+    return this.currentBoard.allowsAttachments;
+  },
+
+  allowsComments() {
+    return this.currentBoard.allowsComments;
+  },
+
+  allowsDescriptionTitle() {
+    return this.currentBoard.allowsDescriptionTitle;
+  },
+
+  allowsDescriptionText() {
+    return this.currentBoard.allowsDescriptionText;
+  },
+
+  isBoardSelected() {
+    return this.currentBoard.dateSettingsDefaultBoardID;
+  },
+
+  isNullBoardSelected() {
+    return (
+      this.currentBoard.dateSettingsDefaultBoardId === null ||
+      this.currentBoard.dateSettingsDefaultBoardId === undefined
+    );
+  },
+
+  boards() {
+    return Boards.find(
+      {
+        archived: false,
+        'members.userId': Meteor.userId(),
+      },
+      {
+        sort: { sort: 1 /* boards default sorting */ },
+      },
+    );
+  },
+
+  lists() {
+    return Lists.find(
+      {
+        boardId: this.currentBoard._id,
+        archived: false,
+      },
+      {
+        sort: ['title'],
+      },
+    );
+  },
+
+  hasLists() {
+    return this.lists().count() > 0;
+  },
+
+  isListSelected() {
+    return (
+      this.currentBoard.dateSettingsDefaultBoardId === this.currentData()._id
+    );
+  },
+
+  events() {
+    return [
+      {
+        'click .js-field-has-receiveddate'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsReceivedDate = !this.currentBoard
+            .allowsReceivedDate;
+          this.currentBoard.setAllowsReceivedDate(
+            this.currentBoard.allowsReceivedDate,
+          );
+          $(`.js-field-has-receiveddate ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsReceivedDate,
+          );
+          $('.js-field-has-receiveddate').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsReceivedDate,
+          );
+        },
+        'click .js-field-has-startdate'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsStartDate = !this.currentBoard
+            .allowsStartDate;
+          this.currentBoard.setAllowsStartDate(
+            this.currentBoard.allowsStartDate,
+          );
+          $(`.js-field-has-startdate ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsStartDate,
+          );
+          $('.js-field-has-startdate').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsStartDate,
+          );
+        },
+        'click .js-field-has-enddate'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsEndDate = !this.currentBoard.allowsEndDate;
+          this.currentBoard.setAllowsEndDate(this.currentBoard.allowsEndDate);
+          $(`.js-field-has-enddate ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsEndDate,
+          );
+          $('.js-field-has-enddate').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsEndDate,
+          );
+        },
+        'click .js-field-has-duedate'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsDueDate = !this.currentBoard.allowsDueDate;
+          this.currentBoard.setAllowsDueDate(this.currentBoard.allowsDueDate);
+          $(`.js-field-has-duedate ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsDueDate,
+          );
+          $('.js-field-has-duedate').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsDueDate,
+          );
+        },
+        'click .js-field-has-subtasks'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
+          this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
+          $(`.js-field-has-subtasks ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsSubtasks,
+          );
+          $('.js-field-has-subtasks').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsSubtasks,
+          );
+        },
+        'click .js-field-has-members'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers;
+          this.currentBoard.setAllowsMembers(this.currentBoard.allowsMembers);
+          $(`.js-field-has-members ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsMembers,
+          );
+          $('.js-field-has-members').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsMembers,
+          );
+        },
+        'click .js-field-has-assignee'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsAssignee = !this.currentBoard.allowsAssignee;
+          this.currentBoard.setAllowsAssignee(this.currentBoard.allowsAssignee);
+          $(`.js-field-has-assignee ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsAssignee,
+          );
+          $('.js-field-has-assignee').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsAssignee,
+          );
+        },
+        'click .js-field-has-assigned-by'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsAssignedBy = !this.currentBoard
+            .allowsAssignedBy;
+          this.currentBoard.setAllowsAssignedBy(
+            this.currentBoard.allowsAssignedBy,
+          );
+          $(`.js-field-has-assigned-by ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsAssignedBy,
+          );
+          $('.js-field-has-assigned-by').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsAssignedBy,
+          );
+        },
+        'click .js-field-has-requested-by'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsRequestedBy = !this.currentBoard
+            .allowsRequestedBy;
+          this.currentBoard.setAllowsRequestedBy(
+            this.currentBoard.allowsRequestedBy,
+          );
+          $(`.js-field-has-requested-by ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsRequestedBy,
+          );
+          $('.js-field-has-requested-by').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsRequestedBy,
+          );
+        },
+        'click .js-field-has-labels'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsLabels = !this.currentBoard.allowsLabels;
+          this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
+          $(`.js-field-has-labels ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsAssignee,
+          );
+          $('.js-field-has-labels').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsLabels,
+          );
+        },
+        'click .js-field-has-description-title'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsDescriptionTitle = !this.currentBoard
+            .allowsDescriptionTitle;
+          this.currentBoard.setAllowsDescriptionTitle(
+            this.currentBoard.allowsDescriptionTitle,
+          );
+          $(`.js-field-has-description-title ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsDescriptionTitle,
+          );
+          $('.js-field-has-description-title').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsDescriptionTitle,
+          );
+        },
+        'click .js-field-has-description-text'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsDescriptionText = !this.currentBoard
+            .allowsDescriptionText;
+          this.currentBoard.setAllowsDescriptionText(
+            this.currentBoard.allowsDescriptionText,
+          );
+          $(`.js-field-has-description-text ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsDescriptionText,
+          );
+          $('.js-field-has-description-text').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsDescriptionText,
+          );
+        },
+        'click .js-field-has-checklists'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsChecklists = !this.currentBoard
+            .allowsChecklists;
+          this.currentBoard.setAllowsChecklists(
+            this.currentBoard.allowsChecklists,
+          );
+          $(`.js-field-has-checklists ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsChecklists,
+          );
+          $('.js-field-has-checklists').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsChecklists,
+          );
+        },
+        'click .js-field-has-attachments'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsAttachments = !this.currentBoard
+            .allowsAttachments;
+          this.currentBoard.setAllowsAttachments(
+            this.currentBoard.allowsAttachments,
+          );
+          $(`.js-field-has-attachments ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsAttachments,
+          );
+          $('.js-field-has-attachments').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsAttachments,
+          );
+        },
+        'click .js-field-has-comments'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsComments = !this.currentBoard.allowsComments;
+          this.currentBoard.setAllowsComments(this.currentBoard.allowsComments);
+          $(`.js-field-has-comments ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsComments,
+          );
+          $('.js-field-has-comments').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsComments,
+          );
+        },
+        'click .js-field-has-activities'(evt) {
+          evt.preventDefault();
+          this.currentBoard.allowsActivities = !this.currentBoard
+            .allowsActivities;
+          this.currentBoard.setAllowsActivities(
+            this.currentBoard.allowsActivities,
+          );
+          $(`.js-field-has-activities ${MCB}`).toggleClass(
+            CKCLS,
+            this.currentBoard.allowsActivities,
+          );
+          $('.js-field-has-activities').toggleClass(
+            CKCLS,
+            this.currentBoard.allowsActivities,
+          );
+        },
+      },
+    ];
+  },
+}).register('boardCardSettingsPopup');
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     this.error = new ReactiveVar('');
     this.error = new ReactiveVar('');
@@ -648,7 +1027,7 @@ BlazeComponent.extendComponent({
 }).register('addMemberPopup');
 }).register('addMemberPopup');
 
 
 Template.changePermissionsPopup.events({
 Template.changePermissionsPopup.events({
-  'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only'(
+  'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only, click .js-set-worker'(
     event,
     event,
   ) {
   ) {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
@@ -658,11 +1037,13 @@ Template.changePermissionsPopup.events({
       'js-set-comment-only',
       'js-set-comment-only',
     );
     );
     const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
     const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
+    const isWorker = $(event.currentTarget).hasClass('js-set-worker');
     currentBoard.setMemberPermission(
     currentBoard.setMemberPermission(
       memberId,
       memberId,
       isAdmin,
       isAdmin,
       isNoComments,
       isNoComments,
       isCommentOnly,
       isCommentOnly,
+      isWorker,
     );
     );
     Popup.back(1);
     Popup.back(1);
   },
   },
@@ -679,7 +1060,8 @@ Template.changePermissionsPopup.helpers({
     return (
     return (
       !currentBoard.hasAdmin(this.userId) &&
       !currentBoard.hasAdmin(this.userId) &&
       !currentBoard.hasNoComments(this.userId) &&
       !currentBoard.hasNoComments(this.userId) &&
-      !currentBoard.hasCommentOnly(this.userId)
+      !currentBoard.hasCommentOnly(this.userId) &&
+      !currentBoard.hasWorker(this.userId)
     );
     );
   },
   },
 
 
@@ -699,6 +1081,13 @@ Template.changePermissionsPopup.helpers({
     );
     );
   },
   },
 
 
+  isWorker() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    return (
+      !currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
+    );
+  },
+
   isLastAdmin() {
   isLastAdmin() {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     return (
     return (

+ 1 - 1
client/components/sidebar/sidebar.styl

@@ -109,7 +109,7 @@
     color: darken(white, 40%)
     color: darken(white, 40%)
 
 
 .board-sidebar
 .board-sidebar
-  width: 248px
+  width: 548px
   right: -@width
   right: -@width
   transition: top .1s, right .1s, width .1s
   transition: top .1s, right .1s, width .1s
 
 

+ 30 - 24
client/components/sidebar/sidebarArchives.jade

@@ -2,54 +2,60 @@ template(name="archivesSidebar")
   if isArchiveReady.get
   if isArchiveReady.get
     +basicTabs(tabs=tabs)
     +basicTabs(tabs=tabs)
       +tabContent(slug="cards")
       +tabContent(slug="cards")
-        p.quiet
-          a.js-restore-all-cards {{_ 'restore-all'}}
-          | -
-          a.js-delete-all-cards {{_ 'delete-all'}}
+        unless isWorker
+          p.quiet
+            a.js-restore-all-cards {{_ 'restore-all'}}
+            | -
+            a.js-delete-all-cards {{_ 'delete-all'}}
         each archivedCards
         each archivedCards
           .minicard-wrapper.js-minicard
           .minicard-wrapper.js-minicard
             +minicard(this)
             +minicard(this)
           if currentUser.isBoardMember
           if currentUser.isBoardMember
-            p.quiet
-              a.js-restore-card {{_ 'restore'}}
-              | -
-              a.js-delete-card {{_ 'delete'}}
+            unless isWorker
+              p.quiet
+                a.js-restore-card {{_ 'restore'}}
+                | -
+                a.js-delete-card {{_ 'delete'}}
             if cardIsInArchivedList
             if cardIsInArchivedList
               p.quiet.small ({{_ 'warn-list-archived'}})
               p.quiet.small ({{_ 'warn-list-archived'}})
         else
         else
           p.no-items-message {{_ 'no-archived-cards'}}
           p.no-items-message {{_ 'no-archived-cards'}}
 
 
       +tabContent(slug="lists")
       +tabContent(slug="lists")
-        p.quiet
-          a.js-restore-all-lists {{_ 'restore-all'}}
-          | -
-          a.js-delete-all-lists {{_ 'delete-all'}}
+        unless isWorker
+          p.quiet
+            a.js-restore-all-lists {{_ 'restore-all'}}
+            | -
+            a.js-delete-all-lists {{_ 'delete-all'}}
         ul.archived-lists
         ul.archived-lists
           each archivedLists
           each archivedLists
             li.archived-lists-item
             li.archived-lists-item
               = title
               = title
               if currentUser.isBoardMember
               if currentUser.isBoardMember
-                p.quiet
-                  a.js-restore-list {{_ 'restore'}}
-                  | -
-                  a.js-delete-list {{_ 'delete'}}
+                unless isWorker
+                  p.quiet
+                    a.js-restore-list {{_ 'restore'}}
+                    | -
+                    a.js-delete-list {{_ 'delete'}}
           else
           else
             li.no-items-message {{_ 'no-archived-lists'}}
             li.no-items-message {{_ 'no-archived-lists'}}
 
 
       +tabContent(slug="swimlanes")
       +tabContent(slug="swimlanes")
-        p.quiet
-          a.js-restore-all-swimlanes {{_ 'restore-all'}}
-          | -
-          a.js-delete-all-swimlanes {{_ 'delete-all'}}
+        unless isWorker
+          p.quiet
+            a.js-restore-all-swimlanes {{_ 'restore-all'}}
+            | -
+            a.js-delete-all-swimlanes {{_ 'delete-all'}}
         ul.archived-lists
         ul.archived-lists
           each archivedSwimlanes
           each archivedSwimlanes
             li.archived-lists-item
             li.archived-lists-item
               = title
               = title
               if currentUser.isBoardMember
               if currentUser.isBoardMember
-                p.quiet
-                  a.js-restore-swimlane {{_ 'restore'}}
-                  | -
-                  a.js-delete-swimlane {{_ 'delete'}}
+                unless isWorker
+                  p.quiet
+                    a.js-restore-swimlane {{_ 'restore'}}
+                    | -
+                    a.js-delete-swimlane {{_ 'delete'}}
           else
           else
             li.no-items-message {{_ 'no-archived-swimlanes'}}
             li.no-items-message {{_ 'no-archived-swimlanes'}}
   else
   else

+ 9 - 0
client/components/sidebar/sidebarArchives.js

@@ -139,3 +139,12 @@ BlazeComponent.extendComponent({
     ];
     ];
   },
   },
 }).register('archivesSidebar');
 }).register('archivesSidebar');
+
+Template.archivesSidebar.helpers({
+  isWorker() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    return (
+      !currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
+    );
+  },
+});

+ 26 - 7
client/components/sidebar/sidebarFilters.jade

@@ -45,6 +45,24 @@ template(name="filterSidebar")
             if Filter.members.isSelected _id
             if Filter.members.isSelected _id
               i.fa.fa-check
               i.fa.fa-check
   hr
   hr
+  ul.sidebar-list
+    li(class="{{#if Filter.assignees.isSelected undefined}}active{{/if}}")
+      a.name.js-toggle-assignee-filter
+        span.sidebar-list-item-description
+          | {{_ 'filter-no-assignee'}}
+        if Filter.assignees.isSelected undefined
+          i.fa.fa-check
+    each currentBoard.activeMembers
+      with getUser userId
+        li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
+          a.name.js-toggle-assignee-filter
+            +userAvatar(userId=this._id)
+            span.sidebar-list-item-description
+              = profile.fullname
+              | (<span class="username">{{ username }}</span>)
+            if Filter.assignees.isSelected _id
+              i.fa.fa-check
+  hr
   ul.sidebar-list
   ul.sidebar-list
     li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
     li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
           a.name.js-toggle-custom-fields-filter
           a.name.js-toggle-custom-fields-filter
@@ -117,13 +135,14 @@ template(name="multiselectionSidebar")
               i.fa.fa-check
               i.fa.fa-check
             else if someSelectedElementHave 'member' _id
             else if someSelectedElementHave 'member' _id
               i.fa.fa-ellipsis-h
               i.fa.fa-ellipsis-h
-  hr
-  a.sidebar-btn.js-move-selection
-    i.fa.fa-share
-    span {{_ 'move-selection'}}
-  a.sidebar-btn.js-archive-selection
-    i.fa.fa-archive
-    span {{_ 'archive-selection'}}
+  unless currentUser.isWorker
+    hr
+    a.sidebar-btn.js-move-selection
+      i.fa.fa-share
+      span {{_ 'move-selection'}}
+    a.sidebar-btn.js-archive-selection
+      i.fa.fa-archive
+      span {{_ 'archive-selection'}}
 
 
 template(name="disambiguateMultiLabelPopup")
 template(name="disambiguateMultiLabelPopup")
   p {{_ 'what-to-do'}}
   p {{_ 'what-to-do'}}

+ 5 - 0
client/components/sidebar/sidebarFilters.js

@@ -18,6 +18,11 @@ BlazeComponent.extendComponent({
           Filter.members.toggle(this.currentData()._id);
           Filter.members.toggle(this.currentData()._id);
           Filter.resetExceptions();
           Filter.resetExceptions();
         },
         },
+        'click .js-toggle-assignee-filter'(evt) {
+          evt.preventDefault();
+          Filter.assignees.toggle(this.currentData()._id);
+          Filter.resetExceptions();
+        },
         'click .js-toggle-archive-filter'(evt) {
         'click .js-toggle-archive-filter'(evt) {
           evt.preventDefault();
           evt.preventDefault();
           Filter.archive.toggle(this.currentData()._id);
           Filter.archive.toggle(this.currentData()._id);

+ 3 - 5
client/components/swimlanes/swimlaneHeader.js

@@ -35,12 +35,10 @@ Template.swimlaneHeader.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
 });
 });

+ 17 - 16
client/components/swimlanes/swimlanes.jade

@@ -43,19 +43,20 @@ template(name="listsGroup")
           +addListForm
           +addListForm
 
 
 template(name="addListForm")
 template(name="addListForm")
-  .list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
-    .list-header-add
-      +inlinedForm(autoclose=false)
-        input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
-          autocomplete="off" autofocus)
-        .edit-controls.clearfix
-          button.primary.confirm(type="submit") {{_ 'save'}}
-          unless currentBoard.isTemplatesBoard
-            unless currentBoard.isTemplateBoard
-              span.quiet
-                | {{_ 'or'}}
-                a.js-list-template {{_ 'template'}}
-      else
-        a.open-list-composer.js-open-inlined-form
-          i.fa.fa-plus
-          | {{_ 'add-list'}}
+  unless currentUser.isWorker
+    .list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
+      .list-header-add
+        +inlinedForm(autoclose=false)
+          input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
+            autocomplete="off" autofocus)
+          .edit-controls.clearfix
+            button.primary.confirm(type="submit") {{_ 'save'}}
+            unless currentBoard.isTemplatesBoard
+              unless currentBoard.isTemplateBoard
+                span.quiet
+                  | {{_ 'or'}}
+                  a.js-list-template {{_ 'template'}}
+        else
+          a.open-list-composer.js-open-inlined-form
+            i.fa.fa-plus
+            | {{_ 'add-list'}}

+ 20 - 28
client/components/swimlanes/swimlanes.js

@@ -1,6 +1,6 @@
 import { Cookies } from 'meteor/ostrio:cookies';
 import { Cookies } from 'meteor/ostrio:cookies';
 const cookies = new Cookies();
 const cookies = new Cookies();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
 
 
 function currentListIsInThisSwimlane(swimlaneId) {
 function currentListIsInThisSwimlane(swimlaneId) {
   const currentList = Lists.findOne(Session.get('currentList'));
   const currentList = Lists.findOne(Session.get('currentList'));
@@ -87,14 +87,12 @@ function initSortable(boardComponent, $listsDom) {
     },
     },
   });
   });
 
 
-  // ugly touch event hotfix
-  enableClickOnTouch('.js-list:not(.js-list-composer)');
-
   function userIsMember() {
   function userIsMember() {
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   }
   }
 
 
@@ -104,31 +102,29 @@ function initSortable(boardComponent, $listsDom) {
     if (currentUser) {
     if (currentUser) {
       showDesktopDragHandles = (currentUser.profile || {})
       showDesktopDragHandles = (currentUser.profile || {})
         .showDesktopDragHandles;
         .showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      showDesktopDragHandles = true;
     } else {
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        showDesktopDragHandles = true;
-      } else {
-        showDesktopDragHandles = false;
-      }
+      showDesktopDragHandles = false;
     }
     }
 
 
-    if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+    if (Utils.isMiniScreen() || showDesktopDragHandles) {
       $listsDom.sortable({
       $listsDom.sortable({
         handle: '.js-list-handle',
         handle: '.js-list-handle',
       });
       });
-    } else {
+    } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
       $listsDom.sortable({
       $listsDom.sortable({
         handle: '.js-list-header',
         handle: '.js-list-header',
       });
       });
     }
     }
 
 
     const $listDom = $listsDom;
     const $listDom = $listsDom;
-    if ($listDom.data('sortable')) {
+    if ($listDom.data('uiSortable') || $listDom.data('sortable')) {
       $listsDom.sortable(
       $listsDom.sortable(
         'option',
         'option',
         'disabled',
         'disabled',
-        // Disable drag-dropping when user is not member
-        !userIsMember(),
+        // Disable drag-dropping when user is not member/is worker
+        !userIsMember() || Meteor.user().isWorker(),
         // Not disable drag-dropping while in multi-selection mode
         // Not disable drag-dropping while in multi-selection mode
         // MultiSelection.isActive() || !userIsMember(),
         // MultiSelection.isActive() || !userIsMember(),
       );
       );
@@ -182,17 +178,14 @@ BlazeComponent.extendComponent({
           if (currentUser) {
           if (currentUser) {
             showDesktopDragHandles = (currentUser.profile || {})
             showDesktopDragHandles = (currentUser.profile || {})
               .showDesktopDragHandles;
               .showDesktopDragHandles;
+          } else if (cookies.has('showDesktopDragHandles')) {
+            showDesktopDragHandles = true;
           } else {
           } else {
-            if (cookies.has('showDesktopDragHandles')) {
-              showDesktopDragHandles = true;
-            } else {
-              showDesktopDragHandles = false;
-            }
+            showDesktopDragHandles = false;
           }
           }
 
 
           const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
           const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
-            Utils.isMiniScreen() ||
-              (!Utils.isMiniScreen() && showDesktopDragHandles)
+            Utils.isMiniScreen() || showDesktopDragHandles
               ? ['.js-list-handle', '.js-swimlane-header-handle']
               ? ['.js-list-handle', '.js-swimlane-header-handle']
               : ['.js-list-header'],
               : ['.js-list-header'],
           );
           );
@@ -276,19 +269,18 @@ Template.swimlane.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
   canSeeAddList() {
   canSeeAddList() {
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
     );
   },
   },
 });
 });

+ 1 - 0
client/components/users/userAvatar.jade

@@ -73,6 +73,7 @@ template(name="cardMemberPopup")
         p.quiet @{{ user.username }}
         p.quiet @{{ user.username }}
     ul.pop-over-list
     ul.pop-over-list
       if currentUser.isNotCommentOnly
       if currentUser.isNotCommentOnly
+        if currentUser.isNotWorker
           li: a.js-remove-member {{_ 'remove-member-from-card'}}
           li: a.js-remove-member {{_ 'remove-member-from-card'}}
 
 
       if $eq currentUser._id user._id
       if $eq currentUser._id user._id

+ 61 - 23
client/components/users/userHeader.jade

@@ -13,21 +13,46 @@ template(name="headerUserBar")
 template(name="memberMenuPopup")
 template(name="memberMenuPopup")
   ul.pop-over-list
   ul.pop-over-list
     with currentUser
     with currentUser
-      li: a.js-edit-profile {{_ 'edit-profile'}}
-      li: a.js-change-settings {{_ 'change-settings'}}
-      li: a.js-change-avatar {{_ 'edit-avatar'}}
+      li
+        a.js-edit-profile
+          i.fa.fa-user
+          | {{_ 'edit-profile'}}
+      li
+        a.js-change-settings
+          i.fa.fa-cog
+          | {{_ 'change-settings'}}
+      li
+        a.js-change-avatar
+          i.fa.fa-picture-o
+          | {{_ 'edit-avatar'}}
       unless isSandstorm
       unless isSandstorm
-        li: a.js-change-password {{_ 'changePasswordPopup-title'}}
-        li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
+        li
+          a.js-change-password
+            i.fa.fa-key
+            | {{_ 'changePasswordPopup-title'}}
+        li
+          a.js-change-language
+            i.fa.fa-flag
+            | {{_ 'changeLanguagePopup-title'}}
     if currentUser.isAdmin
     if currentUser.isAdmin
-      li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}}
-  hr
-  ul.pop-over-list
-    li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}}
+      li
+        a.js-go-setting(href="{{pathFor 'setting'}}")
+          i.fa.fa-lock
+          | {{_ 'admin-panel'}}
+  unless currentUser.isWorker
+    hr
+    ul.pop-over-list
+      li
+        a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
+          i.fa.fa-clone
+          | {{_ 'templates'}}
   unless isSandstorm
   unless isSandstorm
     hr
     hr
     ul.pop-over-list
     ul.pop-over-list
-      li: a.js-logout {{_ 'log-out'}}
+      li
+        a.js-logout
+          i.fa.fa-sign-out
+          | {{_ 'log-out'}}
 
 
 template(name="editProfilePopup")
 template(name="editProfilePopup")
   form
   form
@@ -73,23 +98,36 @@ template(name="changeLanguagePopup")
 
 
 template(name="changeSettingsPopup")
 template(name="changeSettingsPopup")
   ul.pop-over-list
   ul.pop-over-list
-    li
-      a.js-toggle-system-messages
-        | {{_ 'hide-system-messages'}}
-        if hiddenSystemMessages
-          i.fa.fa-check
+    //li
+    //  a.js-toggle-system-messages
+    //    i.fa.fa-comments-o
+    //    | {{_ 'hide-system-messages'}}
+    //    if hiddenSystemMessages
+    //      i.fa.fa-check
     li
     li
       a.js-toggle-desktop-drag-handles
       a.js-toggle-desktop-drag-handles
+        i.fa.fa-arrows
         | {{_ 'show-desktop-drag-handles'}}
         | {{_ 'show-desktop-drag-handles'}}
         if showDesktopDragHandles
         if showDesktopDragHandles
           i.fa.fa-check
           i.fa.fa-check
-    li
-      label.bold
-        | {{_ 'show-cards-minimum-count'}}
-      input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
-      input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
-
+    unless currentUser.isWorker
+      li
+        label.bold.clear
+          i.fa.fa-sort-numeric-asc
+          | {{_ 'show-cards-minimum-count'}}
+        input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
+        label.bold.clear
+          i.fa.fa-calendar
+          | {{_ 'start-day-of-week'}}
+        select#start-day-of-week.inline-input.left
+          each day in weekDays startDayOfWeek
+            if day.isSelected
+              option(selected="true", value="#{day.value}") #{day.name}
+            else
+              option(value="#{day.value}") #{day.name}
+        input.js-apply-user-settings.left(type="submit" value="{{_ 'apply'}}")
 
 
 template(name="userDeletePopup")
 template(name="userDeletePopup")
-  p {{_ 'delete-user-confirm-popup'}}
-  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+  unless currentUser.isWorker
+    p {{_ 'delete-user-confirm-popup'}}
+    button.js-confirm.negate.full(type="submit") {{_ 'delete'}}

+ 58 - 6
client/components/users/userHeader.js

@@ -45,13 +45,31 @@ Template.memberMenuPopup.events({
 
 
 Template.editProfilePopup.helpers({
 Template.editProfilePopup.helpers({
   allowEmailChange() {
   allowEmailChange() {
-    return AccountSettings.findOne('accounts-allowEmailChange').booleanValue;
+    Meteor.call('AccountSettings.allowEmailChange', (_, result) => {
+      if (result) {
+        return true;
+      } else {
+        return false;
+      }
+    });
   },
   },
   allowUserNameChange() {
   allowUserNameChange() {
-    return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue;
+    Meteor.call('AccountSettings.allowUserNameChange', (_, result) => {
+      if (result) {
+        return true;
+      } else {
+        return false;
+      }
+    });
   },
   },
   allowUserDelete() {
   allowUserDelete() {
-    return AccountSettings.findOne('accounts-allowUserDelete').booleanValue;
+    Meteor.call('AccountSettings.allowUserDelete', (_, result) => {
+      if (result) {
+        return true;
+      } else {
+        return false;
+      }
+    });
   },
   },
 });
 });
 
 
@@ -148,6 +166,8 @@ Template.changeLanguagePopup.helpers({
         name = 'Igbo';
         name = 'Igbo';
       } else if (lang.name === 'oc') {
       } else if (lang.name === 'oc') {
         name = 'Occitan';
         name = 'Occitan';
+      } else if (lang.name === '繁体中文(台湾)') {
+        name = '繁體中文(台灣)';
       }
       }
       return { tag, name };
       return { tag, name };
     }).sort(function(a, b) {
     }).sort(function(a, b) {
@@ -204,6 +224,27 @@ Template.changeSettingsPopup.helpers({
       return cookies.get('limitToShowCardsCount');
       return cookies.get('limitToShowCardsCount');
     }
     }
   },
   },
+  weekDays(startDay) {
+    return [
+      TAPi18n.__('sunday'),
+      TAPi18n.__('monday'),
+      TAPi18n.__('tuesday'),
+      TAPi18n.__('wednesday'),
+      TAPi18n.__('thursday'),
+      TAPi18n.__('friday'),
+      TAPi18n.__('saturday'),
+    ].map(function(day, index) {
+      return { name: day, value: index, isSelected: index === startDay };
+    });
+  },
+  startDayOfWeek() {
+    currentUser = Meteor.user();
+    if (currentUser) {
+      return currentUser.getStartDayOfWeek();
+    } else {
+      return cookies.get('startDayOfWeek');
+    }
+  },
 });
 });
 
 
 Template.changeSettingsPopup.events({
 Template.changeSettingsPopup.events({
@@ -227,20 +268,31 @@ Template.changeSettingsPopup.events({
       cookies.set('hasHiddenSystemMessages', 'true');
       cookies.set('hasHiddenSystemMessages', 'true');
     }
     }
   },
   },
-  'click .js-apply-show-cards-at'(event, templateInstance) {
+  'click .js-apply-user-settings'(event, templateInstance) {
     event.preventDefault();
     event.preventDefault();
     const minLimit = parseInt(
     const minLimit = parseInt(
       templateInstance.$('#show-cards-count-at').val(),
       templateInstance.$('#show-cards-count-at').val(),
       10,
       10,
     );
     );
+    const startDay = parseInt(
+      templateInstance.$('#start-day-of-week').val(),
+      10,
+    );
+    const currentUser = Meteor.user();
     if (!isNaN(minLimit)) {
     if (!isNaN(minLimit)) {
-      currentUser = Meteor.user();
       if (currentUser) {
       if (currentUser) {
         Meteor.call('changeLimitToShowCardsCount', minLimit);
         Meteor.call('changeLimitToShowCardsCount', minLimit);
       } else {
       } else {
         cookies.set('limitToShowCardsCount', minLimit);
         cookies.set('limitToShowCardsCount', minLimit);
       }
       }
-      Popup.back();
     }
     }
+    if (!isNaN(startDay)) {
+      if (currentUser) {
+        Meteor.call('changeStartDayOfWeek', startDay);
+      } else {
+        cookies.set('startDayOfWeek', startDay);
+      }
+    }
+    Popup.back();
   },
   },
 });
 });

+ 10 - 0
client/lib/datepicker.js

@@ -10,12 +10,22 @@ DatePicker = BlazeComponent.extendComponent({
     this.defaultTime = defaultTime;
     this.defaultTime = defaultTime;
   },
   },
 
 
+  startDayOfWeek() {
+    const currentUser = Meteor.user();
+    if (currentUser) {
+      return currentUser.getStartDayOfWeek();
+    } else {
+      return 1;
+    }
+  },
+
   onRendered() {
   onRendered() {
     const $picker = this.$('.js-datepicker')
     const $picker = this.$('.js-datepicker')
       .datepicker({
       .datepicker({
         todayHighlight: true,
         todayHighlight: true,
         todayBtn: 'linked',
         todayBtn: 'linked',
         language: TAPi18n.getLanguage(),
         language: TAPi18n.getLanguage(),
+        weekStart: this.startDayOfWeek(),
       })
       })
       .on(
       .on(
         'changeDate',
         'changeDate',

+ 9 - 1
client/lib/filter.js

@@ -459,13 +459,21 @@ Filter = {
   // before changing the schema.
   // before changing the schema.
   labelIds: new SetFilter(),
   labelIds: new SetFilter(),
   members: new SetFilter(),
   members: new SetFilter(),
+  assignees: new SetFilter(),
   archive: new SetFilter(),
   archive: new SetFilter(),
   hideEmpty: new SetFilter(),
   hideEmpty: new SetFilter(),
   customFields: new SetFilter('_id'),
   customFields: new SetFilter('_id'),
   advanced: new AdvancedFilter(),
   advanced: new AdvancedFilter(),
   lists: new AdvancedFilter(), // we need the ability to filter list by name as well
   lists: new AdvancedFilter(), // we need the ability to filter list by name as well
 
 
-  _fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'],
+  _fields: [
+    'labelIds',
+    'members',
+    'assignees',
+    'archive',
+    'hideEmpty',
+    'customFields',
+  ],
 
 
   // We don't filter cards that have been added after the last filter change. To
   // We don't filter cards that have been added after the last filter change. To
   // implement this we keep the id of these cards in this `_exceptions` fields
   // implement this we keep the id of these cards in this `_exceptions` fields

+ 45 - 7
client/lib/keyboard.js

@@ -1,6 +1,16 @@
 // XXX There is no reason to define these shortcuts globally, they should be
 // XXX There is no reason to define these shortcuts globally, they should be
 // attached to a template (most of them will go in the `board` template).
 // attached to a template (most of them will go in the `board` template).
 
 
+function getHoveredCardId() {
+  const card = $('.js-minicard:hover').get(0);
+  if (!card) return null;
+  return Blaze.getData(card)._id;
+}
+
+function getSelectedCardId() {
+  return Session.get('selectedCard') || getHoveredCardId();
+}
+
 Mousetrap.bind('?', () => {
 Mousetrap.bind('?', () => {
   FlowRouter.go('shortcuts');
   FlowRouter.go('shortcuts');
 });
 });
@@ -50,9 +60,9 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
   }
   }
 });
 });
 
 
-// XXX This shortcut should also work when hovering over a card in board view
 Mousetrap.bind('space', evt => {
 Mousetrap.bind('space', evt => {
-  if (!Session.get('currentCard')) {
+  const cardId = getSelectedCardId();
+  if (!cardId) {
     return;
     return;
   }
   }
 
 
@@ -62,7 +72,7 @@ Mousetrap.bind('space', evt => {
   }
   }
 
 
   if (Meteor.user().isBoardMember()) {
   if (Meteor.user().isBoardMember()) {
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Cards.findOne(cardId);
     card.toggleMember(currentUserId);
     card.toggleMember(currentUserId);
     // We should prevent scrolling in card when spacebar is clicked
     // We should prevent scrolling in card when spacebar is clicked
     // This should do it according to Mousetrap docs, but it doesn't
     // This should do it according to Mousetrap docs, but it doesn't
@@ -70,22 +80,46 @@ Mousetrap.bind('space', evt => {
   }
   }
 });
 });
 
 
+Mousetrap.bind('c', evt => {
+  const cardId = getSelectedCardId();
+  if (!cardId) {
+    return;
+  }
+
+  const currentUserId = Meteor.userId();
+  if (currentUserId === null) {
+    return;
+  }
+
+  if (
+    Meteor.user().isBoardMember() &&
+    !Meteor.user().isCommentOnly() &&
+    !Meteor.user().isWorker()
+  ) {
+    const card = Cards.findOne(cardId);
+    card.archive();
+    // We should prevent scrolling in card when spacebar is clicked
+    // This should do it according to Mousetrap docs, but it doesn't
+    evt.preventDefault();
+  }
+});
+
 Template.keyboardShortcuts.helpers({
 Template.keyboardShortcuts.helpers({
   mapping: [
   mapping: [
     {
     {
-      keys: ['W'],
+      keys: ['w'],
       action: 'shortcut-toggle-sidebar',
       action: 'shortcut-toggle-sidebar',
     },
     },
     {
     {
-      keys: ['Q'],
+      keys: ['q'],
       action: 'shortcut-filter-my-cards',
       action: 'shortcut-filter-my-cards',
     },
     },
     {
     {
-      keys: ['F'],
+      keys: ['f'],
       action: 'shortcut-toggle-filterbar',
       action: 'shortcut-toggle-filterbar',
     },
     },
     {
     {
-      keys: ['X'],
+      keys: ['x'],
       action: 'shortcut-clear-filters',
       action: 'shortcut-clear-filters',
     },
     },
     {
     {
@@ -104,5 +138,9 @@ Template.keyboardShortcuts.helpers({
       keys: ['SPACE'],
       keys: ['SPACE'],
       action: 'shortcut-assign-self',
       action: 'shortcut-assign-self',
     },
     },
+    {
+      keys: ['c'],
+      action: 'archive-card',
+    },
   ],
   ],
 });
 });

+ 8 - 3
client/lib/textComplete.js

@@ -48,6 +48,11 @@ $.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
   return this;
   return this;
 };
 };
 
 
-EscapeActions.register('textcomplete', () => {}, () => dropdownMenuIsOpened, {
-  noClickEscapeOn: '.textcomplete-dropdown',
-});
+EscapeActions.register(
+  'textcomplete',
+  () => {},
+  () => dropdownMenuIsOpened,
+  {
+    noClickEscapeOn: '.textcomplete-dropdown',
+  },
+);

+ 48 - 22
client/lib/utils.js

@@ -24,18 +24,14 @@ Utils = {
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).boardView;
       return (currentUser.profile || {}).boardView;
+    } else if (cookies.get('boardView') === 'board-view-lists') {
+      return 'board-view-lists';
+    } else if (cookies.get('boardView') === 'board-view-swimlanes') {
+      return 'board-view-swimlanes';
+    } else if (cookies.get('boardView') === 'board-view-cal') {
+      return 'board-view-cal';
     } else {
     } else {
-      if (cookies.get('boardView') === 'board-view-lists') {
-        return 'board-view-lists';
-      } else if (
-        cookies.get('boardView') === 'board-view-swimlanes'
-      ) {
-        return 'board-view-swimlanes';
-      } else if (cookies.get('boardView') === 'board-view-cal') {
-        return 'board-view-cal';
-      } else {
-        return false;
-      }
+      return false;
     }
     }
   },
   },
 
 
@@ -43,8 +39,8 @@ Utils = {
   goBoardId(_id) {
   goBoardId(_id) {
     const board = Boards.findOne(_id);
     const board = Boards.findOne(_id);
     return (
     return (
-      board
-      && FlowRouter.go('board', {
+      board &&
+      FlowRouter.go('board', {
         id: board._id,
         id: board._id,
         slug: board.slug,
         slug: board.slug,
       })
       })
@@ -55,8 +51,8 @@ Utils = {
     const card = Cards.findOne(_id);
     const card = Cards.findOne(_id);
     const board = Boards.findOne(card.boardId);
     const board = Boards.findOne(card.boardId);
     return (
     return (
-      board
-      && FlowRouter.go('card', {
+      board &&
+      FlowRouter.go('card', {
         cardId: card._id,
         cardId: card._id,
         boardId: board._id,
         boardId: board._id,
         slug: board.slug,
         slug: board.slug,
@@ -151,8 +147,38 @@ Utils = {
   // in a small window (even on desktop), Wekan run in compact mode.
   // in a small window (even on desktop), Wekan run in compact mode.
   // we can easily debug with a small window of desktop browser. :-)
   // we can easily debug with a small window of desktop browser. :-)
   isMiniScreen() {
   isMiniScreen() {
+    // OLD WINDOW WIDTH DETECTION:
     this.windowResizeDep.depend();
     this.windowResizeDep.depend();
     return $(window).width() <= 800;
     return $(window).width() <= 800;
+
+    // NEW TOUCH DEVICE DETECTION:
+    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
+
+    /*
+    var hasTouchScreen = false;
+    if ("maxTouchPoints" in navigator) {
+        hasTouchScreen = navigator.maxTouchPoints > 0;
+    } else if ("msMaxTouchPoints" in navigator) {
+        hasTouchScreen = navigator.msMaxTouchPoints > 0;
+    } else {
+        var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
+        if (mQ && mQ.media === "(pointer:coarse)") {
+            hasTouchScreen = !!mQ.matches;
+        } else if ('orientation' in window) {
+            hasTouchScreen = true; // deprecated, but good fallback
+        } else {
+            // Only as a last resort, fall back to user agent sniffing
+            var UA = navigator.userAgent;
+            hasTouchScreen = (
+                /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
+                /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA)
+            );
+        }
+    }
+    */
+    //if (hasTouchScreen)
+    //    document.getElementById("exampleButton").style.padding="1em";
+    //return false;
   },
   },
 
 
   calculateIndexData(prevData, nextData, nItems = 1) {
   calculateIndexData(prevData, nextData, nItems = 1) {
@@ -227,8 +253,8 @@ Utils = {
       };
       };
 
 
       if (
       if (
-        'ontouchstart' in window
-        || (window.DocumentTouch && document instanceof window.DocumentTouch)
+        'ontouchstart' in window ||
+        (window.DocumentTouch && document instanceof window.DocumentTouch)
       ) {
       ) {
         return true;
         return true;
       }
       }
@@ -249,8 +275,8 @@ Utils = {
 
 
   calculateTouchDistance(touchA, touchB) {
   calculateTouchDistance(touchA, touchB) {
     return Math.sqrt(
     return Math.sqrt(
-      Math.pow(touchA.screenX - touchB.screenX, 2)
-        + Math.pow(touchA.screenY - touchB.screenY, 2),
+      Math.pow(touchA.screenX - touchB.screenX, 2) +
+        Math.pow(touchA.screenY - touchB.screenY, 2),
     );
     );
   },
   },
 
 
@@ -267,9 +293,9 @@ Utils = {
     });
     });
     $(document).on('touchend', selector, function(e) {
     $(document).on('touchend', selector, function(e) {
       if (
       if (
-        touchStart
-        && lastTouch
-        && Utils.calculateTouchDistance(touchStart, lastTouch) <= 20
+        touchStart &&
+        lastTouch &&
+        Utils.calculateTouchDistance(touchStart, lastTouch) <= 20
       ) {
       ) {
         e.preventDefault();
         e.preventDefault();
         const clickEvent = document.createEvent('MouseEvents');
         const clickEvent = document.createEvent('MouseEvents');

+ 7 - 3
config/accounts.js

@@ -38,9 +38,13 @@ AccountsTemplates.configure({
   },
   },
 });
 });
 
 
-['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'].forEach(
-  routeName => AccountsTemplates.configureRoute(routeName),
-);
+[
+  'signIn',
+  'signUp',
+  'resetPwd',
+  'forgotPwd',
+  'enrollAccount',
+].forEach(routeName => AccountsTemplates.configureRoute(routeName));
 
 
 // We display the form to change the password in a popup window that already
 // We display the form to change the password in a popup window that already
 // have a title, so we unset the title automatically displayed by useraccounts.
 // have a title, so we unset the title automatically displayed by useraccounts.

+ 21 - 0
config/router.js

@@ -26,6 +26,27 @@ FlowRouter.route('/', {
   },
   },
 });
 });
 
 
+FlowRouter.route('/public', {
+  name: 'public',
+  triggersEnter: [AccountsTemplates.ensureSignedIn],
+  action() {
+    Session.set('currentBoard', null);
+    Session.set('currentList', null);
+    Session.set('currentCard', null);
+
+    Filter.reset();
+    EscapeActions.executeAll();
+
+    Utils.manageCustomUI();
+    Utils.manageMatomo();
+
+    BlazeLayout.render('defaultLayout', {
+      headerBar: 'boardListHeaderBar',
+      content: 'boardList',
+    });
+  },
+});
+
 FlowRouter.route('/b/:id/:slug', {
 FlowRouter.route('/b/:id/:slug', {
   name: 'board',
   name: 'board',
   action(params) {
   action(params) {

+ 47 - 5
docker-compose.yml

@@ -38,7 +38,7 @@ version: '2'
 #      sudo service docker start
 #      sudo service docker start
 # ----------------------------------------------------------------------------------
 # ----------------------------------------------------------------------------------
 # ==== USAGE OF THIS docker-compose.yml ====
 # ==== USAGE OF THIS docker-compose.yml ====
-# 1) For seeing does Wekan work, try this and check with your webbroser:
+# 1) For seeing does Wekan work, try this and check with your web browser:
 #      docker-compose up
 #      docker-compose up
 # 2) Stop Wekan and start Wekan in background:
 # 2) Stop Wekan and start Wekan in background:
 #     docker-compose stop
 #     docker-compose stop
@@ -93,14 +93,14 @@ services:
     #-------------------------------------------------------------------------------------
     #-------------------------------------------------------------------------------------
     # ==== MONGODB AND METEOR VERSION ====
     # ==== MONGODB AND METEOR VERSION ====
     # a) For Wekan Meteor 1.8.x version at master branch, use mongo 4.x
     # a) For Wekan Meteor 1.8.x version at master branch, use mongo 4.x
-    image: mongo:4.0.12
+    image: mongo:latest
     # b) For Wekan Meteor 1.6.x version at devel branch.
     # b) For Wekan Meteor 1.6.x version at devel branch.
     # Only for Snap and Sandstorm while they are not upgraded yet to Meteor 1.8.x
     # Only for Snap and Sandstorm while they are not upgraded yet to Meteor 1.8.x
     #image: mongo:3.2.21
     #image: mongo:3.2.21
     #-------------------------------------------------------------------------------------
     #-------------------------------------------------------------------------------------
     container_name: wekan-db
     container_name: wekan-db
     restart: always
     restart: always
-    command: mongod --smallfiles --oplogSize 128
+    command: mongod --oplogSize 128
     networks:
     networks:
       - wekan-tier
       - wekan-tier
     expose:
     expose:
@@ -238,7 +238,12 @@ services:
       #---------------------------------------------------------------
       #---------------------------------------------------------------
       # ==== RICH TEXT EDITOR IN CARD COMMENTS ====
       # ==== RICH TEXT EDITOR IN CARD COMMENTS ====
       # https://github.com/wekan/wekan/pull/2560
       # https://github.com/wekan/wekan/pull/2560
-      - RICHER_CARD_COMMENT_EDITOR=true
+      - RICHER_CARD_COMMENT_EDITOR=false
+      #---------------------------------------------------------------
+      # ==== MOUSE SCROLL ====
+      # https://github.com/wekan/wekan/issues/2949
+      - SCROLLINERTIA=0
+      - SCROLLAMOUNT=auto
       #---------------------------------------------------------------
       #---------------------------------------------------------------
       # ==== CARD OPENED, SEND WEBHOOK MESSAGE ====
       # ==== CARD OPENED, SEND WEBHOOK MESSAGE ====
       # https://github.com/wekan/wekan/issues/2518
       # https://github.com/wekan/wekan/issues/2518
@@ -249,6 +254,11 @@ services:
       #-MAX_IMAGE_PIXEL=1024
       #-MAX_IMAGE_PIXEL=1024
       #-IMAGE_COMPRESS_RATIO=80
       #-IMAGE_COMPRESS_RATIO=80
       #---------------------------------------------------------------
       #---------------------------------------------------------------
+      # ==== NOTIFICATION TRAY AFTER READ DAYS BEFORE REMOVE =====
+      # Number of days after a notification is read before we remove it.
+      # Default: 2
+      #- NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE=2
+      #---------------------------------------------------------------
       # ==== BIGEVENTS DUE ETC NOTIFICATIONS =====
       # ==== BIGEVENTS DUE ETC NOTIFICATIONS =====
       # https://github.com/wekan/wekan/pull/2541
       # https://github.com/wekan/wekan/pull/2541
       # Introduced a system env var BIGEVENTS_PATTERN default as "NONE",
       # Introduced a system env var BIGEVENTS_PATTERN default as "NONE",
@@ -342,6 +352,31 @@ services:
       # Tthe claim name you want to map to the email field:
       # Tthe claim name you want to map to the email field:
       #- OAUTH2_EMAIL_MAP=email
       #- OAUTH2_EMAIL_MAP=email
       #-----------------------------------------------------------------
       #-----------------------------------------------------------------
+      # ==== OAUTH2 Nextcloud ====
+      # 1) Register the application with Nextcloud: https://your.nextcloud/settings/admin/security
+      #    Make sure you capture the application ID as well as generate a secret key.
+      # 2) Configure the environment variables. This differs slightly
+      #     by installation type, but make sure you have the following:
+      #- OAUTH2_ENABLED=true
+      # OAuth2 login style: popup or redirect.
+      #- OAUTH2_LOGIN_STYLE=redirect
+      # Application GUID captured during app registration:
+      #- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
+      # Secret key generated during app registration:
+      #- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      #- OAUTH2_SERVER_URL=https://your-nextcloud.tld
+      #- OAUTH2_AUTH_ENDPOINT=/index.php/apps/oauth2/authorize
+      #- OAUTH2_USERINFO_ENDPOINT=/ocs/v2.php/cloud/user?format=json
+      #- OAUTH2_TOKEN_ENDPOINT=/index.php/apps/oauth2/api/v1/token
+      # The claim name you want to map to the unique ID field:
+      #- OAUTH2_ID_MAP=id
+      # The claim name you want to map to the username field:
+      #- OAUTH2_USERNAME_MAP=id
+      # The claim name you want to map to the full name field:
+      #- OAUTH2_FULLNAME_MAP=display-name
+      # Tthe claim name you want to map to the email field:
+      #- OAUTH2_EMAIL_MAP=email
+      #-----------------------------------------------------------------
       # ==== OAUTH2 KEYCLOAK ====
       # ==== OAUTH2 KEYCLOAK ====
       # https://github.com/wekan/wekan/wiki/Keycloak  <== MAPPING INFO, REQUIRED
       # https://github.com/wekan/wekan/wiki/Keycloak  <== MAPPING INFO, REQUIRED
       #- OAUTH2_ENABLED=true
       #- OAUTH2_ENABLED=true
@@ -479,18 +514,22 @@ services:
       # The limit number of entries (0=unlimited)
       # The limit number of entries (0=unlimited)
       #- LDAP_SEARCH_SIZE_LIMIT=0
       #- LDAP_SEARCH_SIZE_LIMIT=0
       #
       #
-      # Enable group filtering
+      # Enable group filtering. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap.
       #- LDAP_GROUP_FILTER_ENABLE=false
       #- LDAP_GROUP_FILTER_ENABLE=false
       #
       #
       # The object class for filtering. Example: group
       # The object class for filtering. Example: group
       #- LDAP_GROUP_FILTER_OBJECTCLASS=
       #- LDAP_GROUP_FILTER_OBJECTCLASS=
       #
       #
+      # The attribute of a group identifying it. Example: cn
       #- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
       #- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
       #
       #
+      # The attribute inside a group object listing its members. Example: member
       #- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
       #- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
       #
       #
+      # The format of the value of LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE. Example: 'dn' if the users dn ist saved as value into the attribute.
       #- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
       #- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
       #
       #
+      # The group name (id) that matches all users.
       #- LDAP_GROUP_FILTER_GROUP_NAME=
       #- LDAP_GROUP_FILTER_GROUP_NAME=
       #
       #
       # LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier). Example: guid
       # LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier). Example: guid
@@ -559,6 +598,9 @@ services:
       # example : LOGOUT_ON_MINUTES=55
       # example : LOGOUT_ON_MINUTES=55
       #- LOGOUT_ON_MINUTES=
       #- LOGOUT_ON_MINUTES=
       #-------------------------------------------------------------------
       #-------------------------------------------------------------------
+      # Hide password login form 
+      # - PASSWORD_LOGIN_ENABLED=true
+      #-------------------------------------------------------------------
     depends_on:
     depends_on:
       - wekandb
       - wekandb
 
 

+ 4 - 4
fix-download-unicode/cfs_access-point.txt

@@ -451,14 +451,14 @@ FS.HTTP.Handlers.Get = function (ref) {
         if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
         if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
             ref.filename =  encodeURIComponent(ref.filename);
             ref.filename =  encodeURIComponent(ref.filename);
         } else if(userAgent.indexOf('firefox') >= 0) {
         } else if(userAgent.indexOf('firefox') >= 0) {
-            ref.filename = new Buffer(ref.filename).toString('binary');
+            ref.filename = Buffer.from(ref.filename).toString('binary');
         } else {
         } else {
             /* safari*/
             /* safari*/
-            ref.filename = new Buffer(ref.filename).toString('binary');
-        }   
+            ref.filename = Buffer.from(ref.filename).toString('binary');
+        }
    } catch (ex){
    } catch (ex){
         ref.filename = 'tempfix';
         ref.filename = 'tempfix';
-   } 
+   }
    return originalHandler.call(this, ref);
    return originalHandler.call(this, ref);
 };
 };
                                                                                                                       // 221
                                                                                                                       // 221

+ 7 - 0
helm/wekan/README.md

@@ -56,3 +56,10 @@ mongodb-replicaset:
 This section controls the scale of the MongoDB redundant Replica Set.
 This section controls the scale of the MongoDB redundant Replica Set.
 
 
 **replicas:** This is the number of MongoDB instances to include in the set. You can set this to 1 for a single server - this will still allow you to scale-up later with a helm upgrade.
 **replicas:** This is the number of MongoDB instances to include in the set. You can set this to 1 for a single server - this will still allow you to scale-up later with a helm upgrade.
+
+### Install OCP route
+If you use this chart to deploy Wekan on an OCP cluster, you can create route instead of ingress with following command:
+
+``` bash
+$ helm template --set route.enabled=true,ingress.enabled=false values.yaml . | oc apply -f-
+```

+ 1 - 1
helm/wekan/requirements.yaml

@@ -1,5 +1,5 @@
 dependencies:
 dependencies:
 - name: mongodb-replicaset
 - name: mongodb-replicaset
-  version: 3.6.x
+  version: 3.11.x
   repository: "https://kubernetes-charts.storage.googleapis.com/"
   repository: "https://kubernetes-charts.storage.googleapis.com/"
   condition: mongodb-replicaset.enabled
   condition: mongodb-replicaset.enabled

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott