Browse Source

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

John Supplee 3 years ago
parent
commit
241c3ed8ae
100 changed files with 3462 additions and 1137 deletions
  1. 19 1
      .devcontainer/Dockerfile
  2. 2 8
      .devcontainer/docker-compose.yml
  3. 2 0
      .eslintrc.json
  4. 1 1
      .future-snap/broken-snapcraft.yaml
  5. 1 1
      .future-snap/snapcraft.yaml
  6. 7 8
      .github/ISSUE_TEMPLATE.md
  7. 63 0
      .github/workflows/docker-publish.yml
  8. 25 0
      .github/workflows/release.yml
  9. 1 0
      .gitignore
  10. 1 2
      .meteor/packages
  11. 1 2
      .meteor/versions
  12. 1 1
      .travis.yml
  13. 1 1
      .tx/config
  14. 715 5
      CHANGELOG.md
  15. 6 4
      Dockerfile
  16. 2 2
      Dockerfile.arm64v8
  17. 25 25
      README.md
  18. 4 4
      SECURITY.md
  19. 1 1
      Stackerfile.yml
  20. 95 18
      api.py
  21. 26 4
      client/components/activities/activities.jade
  22. 73 9
      client/components/activities/activities.js
  23. 54 1
      client/components/activities/activities.styl
  24. 2 2
      client/components/activities/comments.js
  25. 0 1
      client/components/activities/comments.styl
  26. 1 1
      client/components/boards/boardArchive.js
  27. 23 20
      client/components/boards/boardBody.jade
  28. 16 14
      client/components/boards/boardBody.js
  29. 13 6
      client/components/boards/boardColors.styl
  30. 15 8
      client/components/boards/boardHeader.jade
  31. 23 15
      client/components/boards/boardHeader.js
  32. 26 5
      client/components/boards/boardsList.jade
  33. 127 11
      client/components/boards/boardsList.js
  34. 22 0
      client/components/boards/boardsList.styl
  35. 13 0
      client/components/cards/attachments.jade
  36. 7 4
      client/components/cards/attachments.js
  37. 1 1
      client/components/cards/attachments.styl
  38. 16 12
      client/components/cards/cardCustomFields.jade
  39. 9 5
      client/components/cards/cardCustomFields.js
  40. 9 3
      client/components/cards/cardDate.jade
  41. 24 7
      client/components/cards/cardDate.js
  42. 4 1
      client/components/cards/cardDescription.js
  43. 66 26
      client/components/cards/cardDetails.jade
  44. 210 141
      client/components/cards/cardDetails.js
  45. 68 46
      client/components/cards/cardDetails.styl
  46. 8 5
      client/components/cards/cardTime.js
  47. 9 18
      client/components/cards/checklists.jade
  48. 73 56
      client/components/cards/checklists.js
  49. 10 36
      client/components/cards/checklists.styl
  50. 4 2
      client/components/cards/labels.jade
  51. 60 8
      client/components/cards/labels.js
  52. 15 6
      client/components/cards/labels.styl
  53. 20 17
      client/components/cards/minicard.jade
  54. 62 13
      client/components/cards/minicard.js
  55. 4 2
      client/components/cards/minicard.styl
  56. 1 1
      client/components/cards/resultCard.jade
  57. 25 1
      client/components/cards/resultCard.js
  58. 3 0
      client/components/cards/subtasks.js
  59. 1 0
      client/components/forms/forms.styl
  60. 42 27
      client/components/lists/list.js
  61. 29 21
      client/components/lists/list.styl
  62. 12 0
      client/components/lists/listBody.jade
  63. 26 14
      client/components/lists/listBody.js
  64. 14 7
      client/components/lists/listHeader.jade
  65. 24 19
      client/components/lists/listHeader.js
  66. 2 2
      client/components/main/dueCards.js
  67. 2 0
      client/components/main/editor.jade
  68. 277 259
      client/components/main/editor.js
  69. 7 0
      client/components/main/editor.styl
  70. 10 4
      client/components/main/header.jade
  71. 23 0
      client/components/main/header.js
  72. 11 0
      client/components/main/header.styl
  73. 10 2
      client/components/main/layouts.jade
  74. 108 0
      client/components/main/layouts.js
  75. 9 1
      client/components/main/layouts.styl
  76. 1 12
      client/components/main/popup.styl
  77. 1 1
      client/components/rules/actions/cardActions.js
  78. 1 1
      client/components/rules/triggers/boardTriggers.js
  79. 47 1
      client/components/settings/informationBody.jade
  80. 43 2
      client/components/settings/peopleBody.jade
  81. 179 31
      client/components/settings/peopleBody.js
  82. 29 0
      client/components/settings/peopleBody.styl
  83. 30 1
      client/components/settings/settingBody.jade
  84. 62 0
      client/components/settings/settingBody.js
  85. 3 1
      client/components/settings/settingBody.styl
  86. 55 31
      client/components/sidebar/sidebar.jade
  87. 149 20
      client/components/sidebar/sidebar.js
  88. 21 0
      client/components/sidebar/sidebar.styl
  89. 8 8
      client/components/sidebar/sidebarArchives.js
  90. 8 0
      client/components/sidebar/sidebarCustomFields.jade
  91. 12 2
      client/components/sidebar/sidebarCustomFields.js
  92. 8 1
      client/components/sidebar/sidebarFilters.jade
  93. 11 6
      client/components/sidebar/sidebarFilters.js
  94. 5 1
      client/components/sidebar/sidebarSearches.jade
  95. 16 1
      client/components/sidebar/sidebarSearches.js
  96. 1 1
      client/components/swimlanes/swimlaneHeader.jade
  97. 4 17
      client/components/swimlanes/swimlaneHeader.js
  98. 3 1
      client/components/swimlanes/swimlanes.jade
  99. 37 48
      client/components/swimlanes/swimlanes.js
  100. 6 2
      client/components/users/userAvatar.jade

+ 19 - 1
.devcontainer/Dockerfile

@@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive
 
 ENV \
     DEBUG=false \
-    NODE_VERSION=v12.22.4 \
+    NODE_VERSION=v12.22.8 \
     METEOR_RELEASE=1.10.2 \
     USE_EDGE=false \
     METEOR_EDGE=1.5-beta.17 \
@@ -22,6 +22,7 @@ ENV \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
+    ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90 \
     RICHER_CARD_COMMENT_EDITOR=false \
     CARD_OPENED_WEBHOOK_ENABLED=false \
     ATTACHMENTS_STORE_PATH="" \
@@ -195,6 +196,10 @@ COPY \
     settings.json \
     /home/wekan/app/
 
+COPY \
+    tests \
+    /home/wekan/app/tests/
+
 COPY \
     packages \
     /home/wekan/app/packages/
@@ -226,6 +231,19 @@ RUN \
     chmod u+w package.json npm-shrinkwrap.json && \
     npm install
 
+USER root
+# Cleanup
+RUN \
+    set -o xtrace && \
+    apt-get clean -y && \
+    apt-get autoremove -y && \
+    rm -Rf /tmp/* && \
+    rm -Rf /home/wekan/app_build && \
+    rm -Rf /var/cache/apt /var/lib/apt/lists && \
+    rm -Rf /var/lib/apt/lists/*
+
+USER wekan
+
 ENV PORT=3000
 EXPOSE $PORT
 WORKDIR /home/wekan/app

+ 2 - 8
.devcontainer/docker-compose.yml

@@ -12,9 +12,9 @@ services:
     expose:
       - 27017
     volumes:
+      - /etc/localtime:/etc/localtime:ro
       - ./volumes/wekan-db:/data/db
       - ./volumes/wekan-db-dump:/dump
-      - /etc/localtime:/etc/localtime:ro
 
   wekan-dev:
     container_name: wekan-dev-app
@@ -36,19 +36,13 @@ services:
     depends_on:
       - wekandb-dev
     volumes:
+      - /etc/localtime:/etc/localtime:ro
       - ../client:/home/wekan/app/client
       - ../models:/home/wekan/app/models
       - ../config:/home/wekan/app/config
       - ../i18n:/home/wekan/app/i18n
       - ../server:/home/wekan/app/server
       - ../public:/home/wekan/app/public
-      - /etc/localtime:/etc/localtime:ro
-
-volumes:
-  wekan-dev-db:
-    driver: local
-  wekan-dev-db-dump:
-    driver: local
 
 networks:
   wekan-dev-tier:

+ 2 - 0
.eslintrc.json

@@ -122,6 +122,7 @@
     "Activities": true,
     "Attachments": true,
     "Boards": true,
+    "CardCommentReactions": true,
     "CardComments": true,
     "DatePicker": true,
     "Cards": true,
@@ -156,6 +157,7 @@
     "Integrations": true,
     "HTTP": true,
     "AccountSettings": true,
+    "TableVisibilityModeSettings": true,
     "Announcements": true,
     "Swimlanes": true,
     "ChecklistItems": true,

+ 1 - 1
.future-snap/broken-snapcraft.yaml

@@ -81,7 +81,7 @@ parts:
     wekan:
         source: .
         plugin: nodejs
-        node-engine: 12.22.4
+        node-engine: 12.22.8
         node-packages:
             - node-gyp
             - node-pre-gyp

+ 1 - 1
.future-snap/snapcraft.yaml

@@ -83,7 +83,7 @@ parts:
     wekan:
         source: .
         plugin: nodejs
-        node-engine: 12.22.4
+        node-engine: 12.22.8
         node-packages:
             - node-gyp
             - node-pre-gyp

+ 7 - 8
.github/ISSUE_TEMPLATE.md

@@ -1,12 +1,13 @@
 ## Issue
 
-Note: With Docker, please don't use latest tag. Only use release tags.
-See https://github.com/wekan/wekan/issues/3874
+**[PLEASE UPGRADE](https://github.com/wekan/wekan/wiki/Backup)** to newest WeKan ® before adding new issue !!
+- We get too many duplicate reports of already fixed bugs. Newest WeKan ® has newest bugfixes and security fixes.
+- Please search existing Open and Closed issues, most questions have already been answered many times.
 
 If you can not login for any reason:
 - https://github.com/wekan/wekan/wiki/Forgot-Password
 
-Email settings:
+Email settings, only SMTP MAIL_URL and MAIL_FROM are in use:
 - https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
 
 Add these issues to elsewhere:
@@ -20,14 +21,12 @@ Other Wekan issues can be added here.
 * Note: Please anonymize info, and do not add to this public issue any of your Wekan board URLs, passwords, API tokens etc, do you understand?:
 * Did you test in newest Wekan?:
 * For new Wekan install, did you configure root-url correctly so Wekan cards open correctly https://github.com/wekan/wekan/wiki/Settings ?
-* Wekan version:
-* If this is about old version of Wekan, what upgrade problem you have?:
 * Operating System:
-* Deployment Method(snap/docker/sandstorm/mongodb bundle/source):
+* Deployment Method(Snap/Docker/Sandstorm/bundle/source):
 * Http frontend if any (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first):
-* Node Version:
+* Node.js Version:
 * MongoDB Version:
-* Wekan only works on newest desktop Firefox/Chromium/Chrome/Edge/Chromium Edge and mobile Chrome. What webbrowser version are you using?
+* Wekan works on newest desktop and mobile webbrowsers that support Javascript. What webbrowser version are you using?
 
 **Problem description**:
 - *REQUIRED: Add recorded animated gif about how it works currently, and screenshot mockups how it should work. Use peek to record animgif in Linux https://github.com/phw/peek*

+ 63 - 0
.github/workflows/docker-publish.yml

@@ -0,0 +1,63 @@
+name: Docker
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+on:
+  schedule:
+    - cron: '28 23 * * *'
+  push:
+    branches: [ master ]
+    # Publish semver tags as releases.
+    tags: [ 'v*.*.*' ]
+  pull_request:
+    branches: [ master ]
+
+env:
+  # Use docker.io for Docker Hub if empty
+  REGISTRY: ghcr.io
+  # github.repository as <account>/<repo>
+  IMAGE_NAME: ${{ github.repository }}
+
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+
+      # Login against a Docker registry except on PR
+      # https://github.com/docker/login-action
+      - name: Log into registry ${{ env.REGISTRY }}
+        if: github.event_name != 'pull_request'
+        uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      # Extract metadata (tags, labels) for Docker
+      # https://github.com/docker/metadata-action
+      - name: Extract Docker metadata
+        id: meta
+        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+      # Build and push Docker image with Buildx (don't push on PR)
+      # https://github.com/docker/build-push-action
+      - name: Build and push Docker image
+        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
+        with:
+          context: .
+          push: ${{ github.event_name != 'pull_request' }}
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}

+ 25 - 0
.github/workflows/release.yml

@@ -0,0 +1,25 @@
+name: Release Charts
+
+on:
+  push:
+    branches:
+      - master
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+
+      - name: Configure Git
+        run: |
+          git config user.name "$GITHUB_ACTOR"
+          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
+
+      - name: Run chart-releaser
+        uses: helm/chart-releaser-action@v1.1.0
+        env:
+          CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

+ 1 - 0
.gitignore

@@ -38,3 +38,4 @@ ehthumbs.db
 # Helm chart
 # Chart dependencies
 /helm/wekan/**/*.tgz
+/helm/wekan/charts

+ 1 - 2
.meteor/packages

@@ -60,11 +60,10 @@ reactive-var@1.0.11
 fortawesome:fontawesome
 mousetrap:mousetrap
 mquandalle:jquery-textcomplete
-mquandalle:jquery-ui-drag-drop-sort
 mquandalle:mousetrap-bindglobal
 peerlibrary:blaze-components@=0.15.1
 templates:tabs
-verron:autosize
+meteor-autosize
 simple:json-routes
 rajit:bootstrap3-datepicker
 shell-server@0.5.0

+ 1 - 2
.meteor/versions

@@ -76,6 +76,7 @@ matb33:collection-hooks@0.9.1
 matteodem:easy-search@1.6.4
 mdg:validation-error@0.5.1
 meteor@1.9.3
+meteor-autosize@5.0.1
 meteor-base@1.4.0
 meteor-platform@1.2.6
 meteorhacks:aggregate@1.3.0
@@ -106,7 +107,6 @@ mquandalle:collection-mutations@0.1.0
 mquandalle:jade@0.4.9
 mquandalle:jade-compiler@0.4.5
 mquandalle:jquery-textcomplete@0.8.0_1
-mquandalle:jquery-ui-drag-drop-sort@0.2.0
 mquandalle:moment@1.0.1
 mquandalle:mousetrap-bindglobal@0.0.1
 msavin:usercache@1.8.0
@@ -219,7 +219,6 @@ url@1.3.2
 useraccounts:core@1.14.2
 useraccounts:flow-routing@1.14.2
 useraccounts:unstyled@1.14.2
-verron:autosize@3.0.8
 webapp@1.10.1
 webapp-hashing@1.1.0
 wekan-accounts-cas@0.1.0

+ 1 - 1
.travis.yml

@@ -3,7 +3,7 @@ sudo: required
 
 env:
   TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
-  TRAVIS_NODE_VERSION: 12.22.4
+  TRAVIS_NODE_VERSION: 12.22.8
   TRAVIS_NPM_VERSION: latest
 
 before_install:

+ 1 - 1
.tx/config

@@ -39,7 +39,7 @@ host = https://www.transifex.com
 # tap:i18n requires us to use `-` separator in the language identifiers whereas
 # Transifex uses a `_` separator, without an option to customize it on one side
 # or the other, so we need to do a Manual mapping.
-lang_map = ar_EG:ar-EG, bg_BG:bg, de_CH:de-CH, en_IT:en-IT, en_GB:en-GB, es_AR:es-AR, es_CL:es-CL, es_419:es-LA, es_PE:es-PE, es_MX:es-MX, es_TX:es-TX, es_PY:es-PY, el_GR:el, fa_IR:fa-IR, fi_FI:fi, hu_HU:hu, id_ID:id, mn_MN:mn, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, sl_SI:sl, zh_CN:zh-CN, zh_TW:zh-TW, zh_HK:zh-HK
+lang_map = ar_EG:ar-EG, bg_BG:bg, de_AT:de-AT, de_CH:de-CH, en_DE:en-DE, en_IT:en-IT, en_GB:en-GB, es_AR:es-AR, es_CL:es-CL, es_419:es-LA, es_PE:es-PE, es_MX:es-MX, es_TX:es-TX, es_PY:es-PY, el_GR:el-GR, fa_IR:fa-IR, fi_FI:fi, fr_FR:fr-FR, fr_CH:fr-CH, gu_IN:gu-IN, hi_IN:hi-IN, hu_HU:hu, id_ID:id, mn_MN:mn, ms_MY:ms-MY, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, sl_SI:sl, zh_CN:zh-CN, zh_TW:zh-TW, zh_Hans:zh-Hans, zh_HK:zh-HK
 
 [wekan.application]
 file_filter = i18n/<lang>.i18n.json

+ 715 - 5
CHANGELOG.md

@@ -1,7 +1,717 @@
 [Mac ChangeLog](https://github.com/wekan/wekan/wiki/Mac)
 
-Note: With Docker, please don't use latest tag. Only use release tags.
-See https://github.com/wekan/wekan/issues/3874
+# v5.85 2021-12-17 WeKan ® release
+
+This release adds the following updates:
+
+- [Updated to Node.js v12.22.8](https://github.com/wekan/wekan/commit/5ad9ee1de6446e3b2f3e4a5df207d12de76e1b95).
+  Thanks to Node.js developers.
+
+and fixes the following bugs:
+
+- [Fix mobile card details for Modern Dark theme](https://github.com/wekan/wekan/pull/4240).
+  Thanks to jghaanstra.
+- [Fixed undefinded added member to board](https://github.com/wekan/wekan/pull/4245).
+  Thanks to Emile840.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.84 2021-12-15 WeKan ® release
+
+This release adds the following new features:
+
+- [Kubernetes 1.22 support and basic helm test](https://github.com/wekan/wekan/pull/4208).
+  Thanks to varac.
+- [Added Helm Chart usage docs](https://github.com/wekan/wekan/pull/4224).
+  Thanks to varac.
+- [Add full name if exists in `email-invite-subject` for user to invite](https://github.com/wekan/wekan/pull/4226).
+  Thanks to Emile840.
+- [Sort Organizations, Teams and People](https://github.com/wekan/wekan/pull/4232).
+  Thanks to Emile840.
+
+and fixes the following bugs:
+
+- [List title doesn't overlap with hamburger menu anymore](https://github.com/wekan/wekan/pull/4203).
+  Thanks to mfilser.
+- [Fix legal notice traduction bug when refreshing sign in page](https://github.com/wekan/wekan/pull/4217).
+  Thanks to Emile840.
+- [Fix: Clicking to view Lists or Swimlanes Archive adds temporarily many empty Lists to board](https://github.com/wekan/wekan/pull/4221).
+  Thanks to Ben0it-T.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.83 2021-11-30 WeKan ® release
+
+This release adds to following new improvements:
+
+- [Changed delete checklist dialog to a popup](https://github.com/wekan/wekan/pull/4200).
+  Thanks to mfilser.
+- [Dragging minicards scrolls now vertically at the end of the screen](https://github.com/wekan/wekan/pull/4201).
+  Thanks to mfilser.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.82 2021-11-29 WeKan ® release
+
+This release removes the following new features:
+
+- [Revert change from WeKan v5.81: At Sandstorm, every WeKan user is now WeKan Admin and has Admin Panel](https://github.com/wekan/wekan/commit/ebc7741fcb9ad854234921ed0546255411adeec9).
+  Thanks to ocdtrekkie and xet7.
+    
+and adds the following new features:
+
+- [List header contains now a button to add the card to the bottom of the list](https://github.com/wekan/wekan/pull/4195).
+  Thanks to mfilser.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.81 2021-11-29 WeKan ® release
+
+This release adds the following new features:
+
+- [At Sandstorm, every WeKan user is now WeKan Admin and has WeKan Admin Panel. This could help export, board member permissions, etc](https://github.com/wekan/wekan/commit/23a2e90f5f553c2051978a0b4cd5b0d6d4ee03da).
+  Thanks to PizzaProgram and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.80 2021-11-26 WeKan ® release
+
+This release adds the following new features:
+
+- [Show helper at label drag/drop if label popup opened from card details popup](https://github.com/wekan/wekan/pull/4176).
+  Thanks to mfilser.
+- [Show or hide members and assignee(s) on minicard](https://github.com/wekan/wekan/pull/4179).
+  Thanks to Ben0it-T.
+- [List adding has now a cancel button](https://github.com/wekan/wekan/pull/4183).
+  Thanks to mfilser.
+- [CustomFields Currency, autofocus on edit](https://github.com/wekan/wekan/pull/4189).
+  Thanks to mfilser.
+- [Attachments, show file size in KB in card details](https://github.com/wekan/wekan/pull/4191).
+  Thanks to mfilser.
+- [Sidebar Member Settings Popup has now a Popup title](https://github.com/wekan/wekan/pull/4190).
+  Thanks to mfilser.
+- [Add copy text button to most textarea fields](https://github.com/wekan/wekan/pull/4185).
+  Thanks to mfilser.
+- Copy text button at most textarea fields is now translatable.
+  [Part 1](https://github.com/wekan/wekan/commit/5088c122536e13b44cf2fdbcfabeefd00cee332e),
+  [Part 2](https://github.com/wekan/wekan/commit/96465ac664c526d8749dcad158704b512317e256).
+  Thanks to xet7.
+
+and adds the following updates:
+
+- [Docker build script to be executeable](https://github.com/wekan/wekan/commit/8054f2b0025c4cb3f6a3ddf71754ae7c707d6ac0).
+  Thanks to xet7.
+- [Drag drop jquery-ui update + screen and list scroll](https://github.com/wekan/wekan/pull/4181).
+  Thanks to mfilser.
+- [Settings, add some space between radio buttons](https://github.com/wekan/wekan/pull/4186).
+  Thanks to mfilser.
+
+and fixes the following bugs:
+
+- [Default Top Left Corner Logo Image display few seconds before a display of custom Top Left Corner Logo Image](https://github.com/wekan/wekan/issues/4173).
+  Thanks to Emile840.
+- [App reconnect link wasn't clickable](https://github.com/wekan/wekan/pull/4180).
+  Thanks to mfilser.
+- [Copy card URL works now again](https://github.com/wekan/wekan/pull/4184).
+  Thanks to mfilser.
+- [Fix: On mobile infinite scrolling didn't work](https://github.com/wekan/wekan/pull/4187).
+  Thanks to mfilser.
+- [Custom Field StringTemplates didn't save the last input value on touch devices](https://github.com/wekan/wekan/pull/4188).
+  Thanks to mfilser.
+- [Move cards to top/bottom ignores the current filter if active](https://github.com/wekan/wekan/pull/4192).
+  Thanks to mfilser.
+- [Moving many cards with multi selection drag/drop to another list keeps the card order](https://github.com/wekan/wekan/pull/4193).
+  Thanks to mfilser.
+- [Sidebar multi selection actions keep now the card sorting (cards moving, cards to archive etc)](https://github.com/wekan/wekan/pull/4194).
+  Thanks to mfilser.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.79 2021-11-25 WeKan ® release
+
+This release fixes the following bugs:
+
+- [Fix label width oversize bug](https://github.com/wekan/wekan/pull/4157).
+  Thanks to mfilser.
+- [Fixed label popup at desktop view (add and remove labels)](https://github.com/wekan/wekan/pull/4170).
+  Thanks to mfilser.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.78 2021-11-17 WeKan ® release
+
+This release fixes the following bugs:
+
+- [Fix: Sandstorm WeKan Admin Panel version info broken](https://github.com/wekan/wekan/commit/02b6df320fc98e18e5a97105a35196bdffec98bb).
+  Thanks to ocdtrekkie and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.77 2021-11-16 WeKan ® release
+
+This release adds the following updates:
+
+- [Updated Docker Ubuntu base image](https://github.com/wekan/wekan/commit/b1b12b05b571f4eebd38e7486dea28dfd97a885d).
+  Thanks to Ubuntu developers.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.76 2021-11-16 WeKan ® release
+
+This release adds the following new features:
+
+- [Global search load card details](https://github.com/wekan/wekan/pull/4142).
+  Thanks to mfilser.
+- [Layout improvement: Adding organisations to the board](https://github.com/wekan/wekan/pull/4143).
+  Thanks to Ben0it-T.
+- [App reconnect is now possible if the connection was interrupted](https://github.com/wekan/wekan/pull/4147).
+  Thanks to mfilser.
+- [Boards view has now drag handles at desktop view if drag handles are enabled](https://github.com/wekan/wekan/pull/4149).
+  Thanks to mfilser.
+- [Account configuration of option loginExpirationInDays is now possible](https://github.com/wekan/wekan/pull/4150).
+  Thanks to mfilser.
+- [Part 2: Added remaining of Account configuration of option loginExpirationInDays for Snap](https://github.com/wekan/wekan/commit/17d90684bb59fd4159f80b2da224638824151c6f).
+  Thanks to xet7.
+- [Improve multi selection sidebar opening and closing](https://github.com/wekan/wekan/pull/4153).
+  Thanks to marook.
+
+and adds the following updates:
+
+- [Added release scripts for building local Docker images and pushing them to Quay.io and Docker Hub](https://github.com/wekan/wekan/commit/49c4dd8b14d9c13a9ae2aa18b37238a05ed41f92).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Fixed trim whitespace at multiline editor fields](https://github.com/wekan/wekan/pull/4146).
+  Thanks to mfilser.
+- [Fixed placeholder was not visible at list view (mobile view)](https://github.com/wekan/wekan/pull/4148).
+  Thanks to mfilser.
+- [Fix list adding to bottom](https://github.com/wekan/wekan/pull/4152).
+  Thanks to mfilser.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.75 2021-11-12 WeKan ® release
+
+This release adds the following new features:
+
+- [Card popup close color remove move bottom delete](https://github.com/wekan/wekan/pull/4138).
+  Thanks to mfilser.
+- [Comment edit has now a cancel button](https://github.com/wekan/wekan/pull/4139).
+  Thanks to mfilser.
+- [Checklist and items drag drop scrollable mobile view](https://github.com/wekan/wekan/pull/4140).
+  Thanks to mfilser.
+
+and adds the following updates:
+
+- [Updated release scripts](https://github.com/wekan/wekan/commit/936d9fe30697e4651cba04d505393e05f8c902c1).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.74 2021-11-11 WeKan ® release
+
+This release fixes the following bugs:
+
+- [Docker fix failed export and timezone](https://github.com/wekan/wekan/pull/4137).
+  Thanks to mfilser.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.73 2021-11-11 WeKan ® release
+
+This release adds the following new features:
+
+- [Added NodeJS Statistics to Admin Panel/Versio](https://github.com/wekan/wekan/pull/4118).
+  Thanks to Ben0it-T.
+- [Card detail popup loads now comments if opened from board search](https://github.com/wekan/wekan/pull/4128).
+  Thanks to mfilser.
+
+and adds the following updates:
+
+- Updated dependencies
+  [Part 1](https://github.com/wekan/wekan/commit/cf6713a31c9f6ce9d30832ee6bf6c95d35d7044b),
+  [Part 2](https://github.com/wekan/wekan/commit/ac7ef4d4cd7179a140f0c56c7c7d1ffc33e75fbe).
+  Thanks to developers of dependencies.
+
+and fixes the following bugs:
+
+- [Card Details, add missing hr line before Activity title](https://github.com/wekan/wekan/pull/4117).
+  Thanks to Ben0it-T.
+- [Sidebar search only opens the card as popup on mobile view](https://github.com/wekan/wekan/pull/4122).
+  Thanks to mfilser.
+- [Fixed a bug related to the default text of the OIDC button](https://github.com/wekan/wekan/pull/4132).
+  Thanks to Emile840.
+- [Fix: Impossible to export board to excel where title exceeding 31 chars](https://github.com/wekan/wekan/pull/4135).
+  Thanks to Ben0it-T.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.72 2021-10-31 WeKan ® release
+
+This release adds the following new features:
+
+- [Add a possibility for non-admin users (who have an email on a given domain name in Admin Panel) to invite new users for registration](https://github.com/wekan/wekan/pull/4107).
+  Thanks to Emile840.
+
+and fixes the following bugs:
+
+- [Try to fix: Filter List by Title - Hide empty lists in Swimlane view](https://github.com/wekan/wekan/pull/4108).
+  Thanks to Ben0it-T.
+- [Card labels on minicard withouth text are now at the same line again](https://github.com/wekan/wekan/pull/4109).
+  Thanks to mfilser.
+- [Rename "Domaine" to "Domain" that is more like English](https://github.com/wekan/wekan/commit/c136033c1fb25688d310b1b62841003f3901641a).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.71 2021-10-29 WeKan ® release
+
+This release adds the following updates:
+
+- [Updated dependencies](https://github.com/wekan/wekan/commit/df2a2aae1d44ba22563cc28bc8d9baac71b2ced7).
+  Thanks to developers of dependencies.
+
+and fixes the following bugs:
+
+- [Fix: Filter List by Card Title](https://github.com/wekan/wekan/pull/4105).
+  Thanks to Ben0it-T.
+- Add info about upgrades to GitHub issue template.
+  [Part 1](https://github.com/wekan/wekan/commit/46a5eec7d21b66eb1aacac4fec84a0d0a0f4d16b),
+  [Part 2](https://github.com/wekan/wekan/commit/7cc35970a849c19d35b89cf0a5fb91216a66fcb3).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.70 2021-10-28 WeKan ® release
+
+This release fixes the following bugs:
+
+- [Fix bug related to Admin Panel teams management](https://github.com/wekan/wekan/pull/4103).
+  Thanks to Emile840.
+- Docker: Try to fix "Failed export and unexpected container restart". Added timezone and localtime.
+  [Part 1](https://github.com/wekan/wekan/commit/ec33d0b34f3abe5634be0b87f03314c738c771d1),
+  [Part 2](https://github.com/wekan/wekan/commit/e3292dd5627f95d59d130a8c1b9a62df317ae6bd).
+  Thanks to akitzing, mfilser and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.69 2021-10-28 WeKan ® release
+
+This release adds the following updates:
+
+- [Updated Docker base image to Ubuntu 21.10 Impish](https://github.com/wekan/wekan/commit/5411113544f040cab2df86234745e4846029660f).
+  Thanks to Ubuntu developers.
+
+and fixes the following bugs:
+
+- [Fix Docs: Only MAIL_URL and MAIL_FROM for email settings. Not Admin Panel anymore](https://github.com/wekan/wekan/commit/d9adce7b676b705da786eb44cd2c2c4dba120d30).
+  Thanks to niklasdahlheimer.
+- [Popup fixes: Archive cards, upload attachements etc](https://github.com/wekan/wekan/pull/4101).
+  Thanks to mfilser.
+  
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.68 2021-10-27 WeKan ® release
+
+This release adds the following new features:
+
+- [Labels are now drag/drop/sortable](https://github.com/wekan/wekan/pull/4084).
+  Thanks to mfilser.
+
+and fixes the following bugs:
+
+- [Fix labels desktop view add and delete](https://github.com/wekan/wekan/pull/4087).
+  Thanks to mfilser.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.67 2021-10-27 WeKan ® release
+
+This release fixes the following bugs:
+
+- [Fix typo](https://github.com/wekan/wekan/commit/cb9b8d4f2b8e24475a2aafd6f9653f28f305eefb).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.66 2021-10-27 WeKan ® release
+
+This release adds the following new features:
+
+- [api.py: List All Public Boards](https://github.com/wekan/wekan/commit/eac102dbbf302ccc121bbf1e4e8faf115e1f9da8).
+  Thanks to xet7.
+- [api.py: List Custom Fields of Board](https://github.com/wekan/wekan/commit/bcf35731316c327090a8513a4c4094e32e301e3f).
+  Thanks to xet7.
+- [api.py: Info of one Custom Field](https://github.com/wekan/wekan/commit/5c571ca8638c29e558f3a196daf5458274eb715e).
+  Thanks to xet7.
+- [api.py: Add Custom Fields to Board. Does not work yet, error: Settings must be object](https://github.com/wekan/wekan/commit/3921209c9fbf1d908f2ef3e97dade5863a000309).
+  Thanks to xet7.
+- [Add full name if exists in email-invite-subject or when tagging someone with `@` while commenting a card](https://github.com/wekan/wekan/pull/4057).
+  Thanks to Emile840.
+- [Popup sorting number](https://github.com/wekan/wekan/pull/4060).
+  Thanks to mfilser.
+- [At mobile view the card details are opened as Popup](https://github.com/wekan/wekan/pull/4062).
+  Thanks to mfilser.
+- [Add card button has now a cancel button](https://github.com/wekan/wekan/pull/4067).
+  Thanks to mfilser.
+- [Global search checklistitems and custom fields boolean](https://github.com/wekan/wekan/pull/4074).
+  Thanks to mfilser.
+- [Board View, sort cards button also in mobile view](https://github.com/wekan/wekan/pull/4076).
+  Thanks to mfilser.
+- [Minicard label popup](https://github.com/wekan/wekan/pull/4079).
+  Thanks to mfilser.
+- [Re-enables custom schemes auto linking](https://github.com/wekan/wekan/commit/f67a174c4a7706a2d419ba3dd43d696104f90696).
+  Thanks to chrisi51.
+- [Board search remove limit](https://github.com/wekan/wekan/pull/4082).
+  Thanks to mfilser.
+- [Add a possibility of selecting displayed users in Admin Panel](https://github.com/wekan/wekan/pull/4083).
+  Thanks to Emile840.
+
+and adds the following updates:
+
+- Updated dependencies.
+  [Part 1](https://github.com/wekan/wekan/commit/f14e710ac0d5381ec092c9f383b9b68f446cab4d),
+  [Part 2](https://github.com/wekan/wekan/commit/156c0b5d4d91dae2ee9b12ed8c312dc19a3c3075).
+  Thanks to developers of dependencies.
+- [Added npm publish script for releases](https://github.com/wekan/wekan/commit/2666b30ba911da8502153be5827f277b81354f8b).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Fix infinite loading of public boards](https://github.com/wekan/wekan/pull/4053).
+  Thanks to mfilser.
+- [Fix: Setting overtime not working](https://github.com/wekan/wekan/pull/4056).
+  Thanks to Ben0it-T.
+- [Fix main scrollbar](https://github.com/wekan/wekan/pull/4063).
+  Thanks to mfilser.
+- [Try to fix orphanedAttachments](https://github.com/wekan/wekan/commit/6a06522777a0bfa2f758e96c2d25e1237a7b43dc).
+  Thanks to Madko and xet7.
+- [Fix markdown header quick access](https://github.com/wekan/wekan/pull/4065).
+  Thanks to mfilser.
+- [Fix Filter List by Card Title](https://github.com/wekan/wekan/pull/4066).
+  Thanks to Ben0it-T.
+- [Fix long textarea editing](https://github.com/wekan/wekan/pull/4068).
+  Thanks to mfilser.
+- [Boards weren't loaded because of missing filter](https://github.com/wekan/wekan/pull/4069).
+  Thanks to mfilser.
+- [Fix Card details Custom Fields popup empty hr sections and plus icon](https://github.com/wekan/wekan/pull/4070).
+  Thanks to mfilser.
+- [Card popup search and global search improvements](https://github.com/wekan/wekan/pull/4071).
+  Thanks to mfilser.
+- [Comment out showing Search All Boards logs in console](https://github.com/wekan/wekan/commit/a62a177fb1cdf8b823b5c32380a81e803e0049e7).
+  Thanks to mfilser and xet7.
+- [Long labels on card and minicard are wrapped if too long](https://github.com/wekan/wekan/pull/4073).
+  Thanks to mfilser.
+- [Card dates, if deleted rules didn't apply on "unset date fields"](https://github.com/wekan/wekan/pull/4075).
+  Thanks to mfilser.
+- [Comment, added confirm delete popup](https://github.com/wekan/wekan/pull/4077).
+  Thanks to mfilser.
+- [Fix: Filter List by Card Title](https://github.com/wekan/wekan/pull/4078).
+  Thanks to Ben0it-T.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.65 2021-10-12 WeKan ® release
+
+This release adds to following CRITICAL SECURITY UPDATES:
+
+- [Updated to Node.js v12.22.7](https://github.com/wekan/wekan/commit/64fc2e5d8fe50115175d44c01f7fca4e668c7231).
+  Thanks to Node.js developers.
+
+and fixes the following bugs:
+
+- [Excel Export: Export only comments for cards that are not linked](https://github.com/wekan/wekan/pull/4047).
+  Thanks to Ben0it-T.
+- [If OIDC button text was customized, the default text will be added if a user click on `Sign In`](https://github.com/wekan/wekan/pull/4052).
+  Thanks to Emile840.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.64 2021-10-09 WeKan ® release
+
+This release adds the following new features:
+
+- [Excel Export : add board description, add comments worksheet](https://github.com/wekan/wekan/pull/4045).
+  Thanks to Ben0it-T.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.63 2021-10-07 Wekan release
+
+This release adds the following new features:
+
+- [Allow setting custom kubernetes labels when using the helm chart](https://github.com/wekan/wekan/pull/4031).
+  Thanks to ariep.
+
+and fixes the following bugs:
+
+- [Fixed SMTP by reverting MAIL_SERVICE changes](https://github.com/wekan/wekan/commit/9c99c5c3ae8d291df5305b3b6cd1825fc5cc2c21).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.62 2021-10-04 Wekan release
+
+This release adds the following new features:
+
+- [Allow word match for rules -> title filter](https://github.com/wekan/wekan/pull/4025).
+  Thanks to ilvar.
+- [CSV/TSV/Excel Export translatable and fixed, CSV semicolon option added](https://github.com/wekan/wekan/pull/4028).
+  Thanks to Ben0it-T.
+- Added week numbers to dates at card, minicard, Custom Field dates, DatePicker and Calendar.
+  [Part 1](https://github.com/wekan/wekan/commit/d06ac09485dafb0256ae7fbe613ab2dbe00b70f3),
+  [Part 2](https://github.com/wekan/wekan/commit/9e6744d1e33b37e0d23eea5869ccac3ff37f7d53).
+  Thanks to xet7.
+- [Confirm Archive Card](https://github.com/wekan/wekan/commit/6c3fcdcc4c446fd4c8dc4dca1b2846f6e3ea72e4).
+  Thanks to xet7.
+
+and fixes the following bugs:
+
+- [Clean up /tmp after Docker build. This drastically reduces docker image size from ~280 MB to ~180 MB](https://github.com/wekan/wekan/pull/4026).
+  Thanks to ilvar.
+- [Removed extra quotes from Export menu](https://github.com/wekan/wekan/commit/553652556468ac88c0691d4d688d5a922ef6a0c2).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.61 2021-09-25 Wekan release
+
+This release adds the following new features:
+
+- [Search by name or username or emails address when adding a new user to a board](https://github.com/wekan/wekan/pull/4018).
+  Thanks to Emile840.
+
+and fixes the following bugs:
+
+- [Fixed REST API, it shoud work now by Admin user](https://github.com/wekan/wekan/commit/e3a0dea85fa1f8e2f580f419b30cf5f36775d731).
+  Reverted [Allow board members to use more of API of Wekan v5.35](https://github.com/wekan/wekan/commit/a719e8fda1f78bcbf9af6e7b4341f8be1d141e90).
+  Thanks to tomhughes and xet7.
+- [Wekan Gantt GPL: Fix Tasks not displayed in Gantt screen](https://github.com/wekan/wekan-gantt-gpl/commit/72d464f5eb55501f08eb0cfd31fd5340380d7f3b).
+  Thanks to MrLovegreen and khjde1207.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.60 2021-09-22 Wekan release
+
+This release adds the following new features:
+
+- [Toggle opened card Custom Fields layout between Grid and one per row](https://github.com/wekan/wekan/commit/fc2fb9a081021663cc822bf2a687fda74cd0afa6).
+  Thanks to xet7.
+
+and adds the following updates:
+
+- [Updated Docker base image to newer Ubuntu](https://github.com/wekan/wekan/commit/442e6bf983ada47c26a15dbc1982c554118fa84d).
+  Thanks to xet7.
+- [Try to add Docker image to GitHub Docker Image Registry](https://github.com/wekan/wekan/commit/70ba1eca787671879215726c16335a84e2b636c9).
+  Thanks to xet7.
+- [Update build scripts to install npm from NodeSource, and meteor with npm](https://github.com/wekan/wekan/commit/c062621dd5486b60bdd200a9279a38b98fc0d410).
+  Thanks to Meteor developers.
+
+and fixes the following bugs:
+
+- [Try to fix Bug: Card number equal to #0 when creating a sub-task from a card](https://github.com/wekan/wekan/commit/4c659da5334641f558e77285f7ca47e562f7c853).
+  Thanks to marcungeschikts, olivierlambert and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.59 2021-09-17 Wekan release
+
+This release adds the following new features:
+
+- [Admin Panel/People: Possibility of adding a team to all selected users](https://github.com/wekan/wekan/pull/3996).
+  Thanks to Emile840.
+- [Add / remove team members as board members when adding / removing team from board](https://github.com/wekan/wekan/pull/4000).
+  Thanks to Emile840.
+- [Added more translations to: Admin Panel/People: Possibility of adding a team to all selected users](https://github.com/wekan/wekan/commit/3d9b7eb7ab41c6450b473f6f349d894f516c5487).
+  Thanks to xet7.
+- [Enter new password 2 times when registering](https://github.com/wekan/wekan/commit/0da84f8f3eb91c5bf726e058f5ec74a7891d734b).
+  Thanks to sh2515 and xet7.
+- Sum of cards. In Progress, not ready yet.
+  [Part 1: Add Custom Field options for field sum](https://github.com/wekan/wekan/commit/8626b466b830adf6c671211bbd61b53b96ac5a49).
+  [Part 2: Show option for custom field sum only for currency and number custom fields](https://github.com/wekan/wekan/commit/9bee6ae6663a5e1c974de2811f6a5fdd2d66efe5).
+  Thanks to xet7.
+- [Admin Panel/Settings/Layout: Customize OIDC button text](https://github.com/wekan/wekan/pull/4011).
+  Thanks to Emile840.
+- [At card attachments, show play and fullscreen controls for video webm/mp4/ogg, and play controls for audio mp3/ogg](https://github.com/wekan/wekan/commit/bd9fbedbf9fbe0181913876b930b335261cd2a0a).
+  Thanks to luistiktok and xet7.
+
+and fixes the following bugs:
+
+- [Links to devel branch are broken; use master instead](https://github.com/wekan/wekan/pull/3993).
+  Thanks to garrison.
+- [Fix first user creation for via OIDC](https://github.com/wekan/wekan/pull/3994).
+  Thanks to ww-daniel-mora.
+- [When list has just one card, to show 'card' instead of 'cards'](https://github.com/wekan/wekan/pull/3999).
+  Thanks to helioguardabaxo.
+- [Fix: Linked card cannot change date](https://github.com/wekan/wekan/pull/4002).
+  Thanks to Ben0it-T.
+- [Try to fix: Can't delete attachment](https://github.com/wekan/wekan/commit/889ec1339a025a68ec919f059b9d58e8d94a3376).
+  Thanks to luistiktok and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.58 2021-09-01 Wekan release
+
+This release fixes the following bugs:
+
+- [1) Edit profile and modify password menus are not displayed if SSO authentication is used.
+  2) Board filtering will be displayed only if user belongs to atleast one team or
+  organization](https://github.com/wekan/wekan/pull/3983).
+  Thanks to Emile840.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.57 2021-08-31 Wekan release
+
+This release adds the following updates:
+
+- [Updated build scripts](https://github.com/wekan/wekan/commit/52fafe997659e933e403acb0ee0cffc99f74e35f).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.56 2021-08-31 Wekan release
+
+This release adds the following updates:
+
+- [Updated dependencies](https://github.com/wekan/wekan/commit/858967f4200783cadaa62d0e3436f661c772ede7).
+  Thanks to developers of dependencies.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.55 2021-08-31 Wekan release
+
+This release adds to following CRITICAL SECURITY UPDATES:
+
+- [Updated to Node.js v12.22.6](https://github.com/wekan/wekan/commit/48636892489dd01c6f6b930bafb94651c00859d8).
+  Thanks to Node.js developers.
+
+and fixes the following bugs:
+
+- [Fixed bugs](https://github.com/wekan/wekan/pull/3981):
+  1) Public Boards page shows only "Add Board" button, not any Public Boards.
+  2) When at Admin Panel / Boards visibility / Private only, public board still accessible publicly by it's
+  public board URL.
+  Thanks to Emile840.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.54 2021-08-28 Wekan release
+
+This release adds the following new features:
+
+- [Admin panel: Added a parameter to display or not the visibility of a board in private mode only](https://github.com/wekan/wekan/pull/3976).
+  Thanks to Emile840.
+
+and fixes the following bugs:
+
+- [Fix: Incorrect card numbers for sub tasks](https://github.com/wekan/wekan/pull/3977).
+  Thanks to syndimann.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.53 2021-08-27 Wekan release
+
+This release fixes the following bugs:
+
+- [Try to fix MAIL_FROM](https://github.com/wekan/wekan/commit/787df044190915c46e22159f3c40fb611846dc07).
+  Thanks to xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.52 2021-08-26 Wekan release
+
+This release adds the following new features:
+
+- Added MAIL_SERVICE settings for Well Known Email Services
+  [Part 1](https://github.com/wekan/wekan/commit/ab8e56e16a02ef0afb7b4023a43b4adf2558a8ff),
+  [Part 2](https://github.com/wekan/wekan/commit/1fadf204c2d5fa96ea41b9cb39f003cc05e2fe46).
+  https://github.com/wekan/wekan/wiki/Troubleshooting-Mail . Please test.
+  Thanks to xet7.
+- [All Boards page: Possibility of filtering board by team or organization](https://github.com/wekan/wekan/pull/3964).
+  Thanks to Emile840.
+- [Fixed translation of "Clear Filter" for "All boards page: Possibility of filtering board by team or organization"](https://github.com/wekan/wekan/commit/b36a7621e0feca5c22fc4a24eceba1a9fc584ab0).
+  Thanks to xet7.
+
+and adds the following new translations:
+
+- [Added Chinese (Simplified) (zh-Hans or zh-CN)](https://github.com/wekan/wekan/commit/f2c242f49e18e2197f1f90c9b2dac5934a08325d).
+  Thanks to translators.
+
+and fixes the following bugs:
+
+- [Initials not required for new user that is created at Admin Panel](https://github.com/wekan/wekan/commit/9c7c481f48cb66406715f7571439f9d7fa332b87).
+  Thanks to xet7.
+- [Delete user is now possible at Admin Panel](https://github.com/wekan/wekan/commit/7808fdd22f04cc482b7df21187aaf3e9623f19e6).
+  But you should remove user first from all boards, because otherwise there could be
+  bug of empty avatars at boards, that need to be removed manually from database.
+  Thanks to xet7.
+- [Fixed Save button not clickable in maximized card view](https://github.com/wekan/wekan/commit/a59932af00c066871102970d252b78d262d06fa0).
+  Thanks to hatl, urmel1960 and syndimann.
+- [Fixed New wide card edit view is all jumbled on mobile](https://github.com/wekan/wekan/commit/241eb9df0fb446b3775704848281b0cc032c4921).
+  Thanks to jdaviescoates and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.51 2021-08-17 Wekan release
+
+This release fixes the following bugs:
+
+- [Fixed exception in global search](https://github.com/wekan/wekan/pull/3949).
+  Thanks to syndimann.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.50 2021-08-15 Wekan release
+
+This release fixes the following bugs:
+
+- [Fix: Save user initials and fullname when a new user is created](https://github.com/wekan/wekan/pull/3946).
+  Thanks to syndimann.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.49 2021-08-14 Wekan release
+
+This release adds the following new features:
+
+- [Text "Search" now translatable at Card Add Member/Assignee](https://github.com/wekan/wekan/commit/9ce65c601a875a4259fb69fdda45124b8412ae6f).
+  Thanks to xet7.
+- [Add Card Comment Reactions](https://github.com/wekan/wekan/pull/3945).
+  Thanks to syndimann.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
+
+# v5.48 2021-08-11 Wekan release
+
+This release adds the following CRITICAL SECURITY UPDATES:
+
+- [Updated to Node.js v12.22.5](https://github.com/wekan/wekan/commit/91cad7b49e25cecdf417321dadcdd9ea5cd8b020).
+  Thanks to Node.js developers.
+- Also jszip update in some of included update commits.
+
+and adds the following new features:
+
+- [Searchfields for members and assignees card popups](https://github.com/wekan/wekan/pull/3942).
+  Thanks to syndimann.
+
+and adds the following updates:
+
+- [Updated dependencies](https://github.com/wekan/wekan/commit/b3cc01b04167bd67dde02c6c899baf8917ae09c1).
+  Thanks to developers of dependencies.
+
+and adds the following new translations:
+
+- [French (Switzerland) (fr_CH)](https://github.com/wekan/wekan/commit/23c70ac252494b464cd2a268d7e680370775ddc4).
+  Thanks to translators.
+
+and fixes the following bugs:
+
+- [Fixed: Can't save user without Initials](https://github.com/wekan/wekan/commit/9a03654062f9c8ac7aac257f11b386a054cd39e7).
+  Thanks to devagleo and xet7.
+
+Thanks to above GitHub users for their contributions and translators for their translations.
 
 # v5.47 2021-08-05 Wekan release
 
@@ -5578,7 +6288,7 @@ This release adds the following new features:
 and fixes the following bugs:
 
 - Revert [Sandstorm API changes](https://github.com/wekan/wekan/commit/be03a191c4321c2f80116c0ee1ae6c826d882535)
-  that were done at [Wekan v2.05](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md#v205-2019-01-27-wekan-release)
+  that were done at [Wekan v2.05](https://github.com/wekan/wekan/blob/master/CHANGELOG.md#v205-2019-01-27-wekan-release)
   to fix #2143. Thanks to pantraining and xet7.
 
 Thanks to above GitHub users and translators for contributions.
@@ -5725,7 +6435,7 @@ Update translations. Thanks to translators.
 This release adds the following new features:
 
 - [IFTTT Rules improvements](https://github.com/wekan/wekan/pull/2088). Thanks to Angtrim.
-- Add [find.sh](https://github.com/wekan/wekan/blob/devel/find.sh) bash script that ignores
+- Add [find.sh](https://github.com/wekan/wekan/blob/master/find.sh) bash script that ignores
   extra directories when searching. xet7 uses this a lot when developing. Thanks to xet7.
 
 Thanks to above GitHub users for their contributions.
@@ -7236,7 +7946,7 @@ This release adds the following new features:
 
 - [Checklist templates](https://github.com/wekan/wekan/pull/1470);
 - Added [Finnish language changelog](https://github.com/wekan/wekan/tree/devel/meta/t9n-changelog)
-  and [more Finnish traslations](https://github.com/wekan/wekan/blob/devel/sandstorm-pkgdef.capnp)
+  and [more Finnish traslations](https://github.com/wekan/wekan/blob/master/sandstorm-pkgdef.capnp)
   to Sandstorm.
 
 Thanks to GitHub users erikturk and xet7 for their contributions.

+ 6 - 4
Dockerfile

@@ -1,8 +1,8 @@
-FROM quay.io/wekan/ubuntu:groovy-20210115
+FROM quay.io/wekan/ubuntu:impish-20211102
 LABEL maintainer="wekan"
 
-# 2020-12-03:
-# - Above Ubuntu base image copied from Docker Hub ubuntu:groovy-20201125.2
+# 2021-09-18:
+# - Above Ubuntu base image copied from Docker Hub ubuntu:hirsute-20210825
 #   to Quay to avoid Docker Hub rate limits.
 
 # Set the environment variables (defaults where required)
@@ -12,7 +12,7 @@ ARG DEBIAN_FRONTEND=noninteractive
 
 ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
     DEBUG=false \
-    NODE_VERSION=v12.22.4 \
+    NODE_VERSION=v12.22.8 \
     METEOR_RELEASE=1.10.2 \
     USE_EDGE=false \
     METEOR_EDGE=1.5-beta.17 \
@@ -28,6 +28,7 @@ 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_LOCKOUT_PERIOD=60 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
+    ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90 \
     RICHER_CARD_COMMENT_EDITOR=false \
     CARD_OPENED_WEBHOOK_ENABLED=false \
     ATTACHMENTS_STORE_PATH="" \
@@ -309,6 +310,7 @@ RUN \
     apt-get remove --purge -y ${BUILD_DEPS} && \
     apt-get autoremove -y && \
     npm uninstall -g api2html &&\
+    rm -R /tmp/* && \
     rm -R /var/lib/apt/lists/* && \
     rm -R /home/wekan/.meteor && \
     rm -R /home/wekan/app && \

+ 2 - 2
Dockerfile.arm64v8

@@ -4,7 +4,7 @@ FROM amd64/alpine:3.7 AS builder
 ENV QEMU_VERSION=v4.2.0-6 \
     QEMU_ARCHITECTURE=aarch64 \
     NODE_ARCHITECTURE=linux-arm64 \
-    NODE_VERSION=v12.22.4 \
+    NODE_VERSION=v12.22.8 \
     WEKAN_VERSION=latest  \
     WEKAN_ARCHITECTURE=arm64
 
@@ -40,7 +40,7 @@ LABEL maintainer="wekan"
 # Set the environment variables (defaults where required)
 ENV QEMU_ARCHITECTURE=aarch64 \
     NODE_ARCHITECTURE=linux-arm64 \
-    NODE_VERSION=v12.22.4 \
+    NODE_VERSION=v12.22.8 \
     NODE_ENV=production \
     NPM_VERSION=latest \
     WITH_API=true \

+ 25 - 25
README.md

@@ -1,6 +1,6 @@
 [![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)
 [![Docker Repository on Quay](https://quay.io/repository/wekan/wekan/status "Docker Repository on Quay")](https://quay.io/repository/wekan/wekan)
@@ -14,19 +14,19 @@
 [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fwekan%2Fwekan.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_shield)
 [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4619/badge)](https://bestpractices.coreinfrastructure.org/projects/4619)
 
-## [Translate Wekan at Transifex](https://transifex.com/wekan/wekan)
+## [Translate WeKan ® at Transifex](https://transifex.com/wekan/wekan)
 
 Translations to non-English languages are accepted only at [Transifex](https://transifex.com/wekan/wekan) using webbrowser.
 New English strings of new features can be added as PRs to edge branch file wekan/i18n/en.i18n.json .
 
 ## [Wekan feature requests and bugs](https://github.com/wekan/wekan/issues)
 
-Please add most of your questions as GitHub issue: [Wekan Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
+Please add most of your questions as GitHub issue: [WeKan ® Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
 It's better than at chat where details get lost when chat scrolls up.
 
 ## Chat
 
-[Discussions][discussions] - Wekan Community GitHub Discussions, that are not [Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
+[Discussions][discussions] - WeKan Community GitHub Discussions, that are not [Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
 
 [Wekan IRC FAQ](https://github.com/wekan/wekan/wiki/IRC-FAQ)
 
@@ -41,9 +41,9 @@ See https://github.com/wekan/wekan/issues/3874
 - Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
 - 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 ®
 
-Wekan is an completely [Open Source][open_source] and [Free software][free_software]
+WeKan ® is an completely [Open Source][open_source] and [Free software][free_software]
 collaborative kanban board application with MIT license.
 
 Whether you’re maintaining a personal todo list, planning your holidays with some friends,
@@ -51,58 +51,58 @@ or working in a team on your next revolutionary idea, Kanban boards are an unbea
 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.
 
-Since Wekan is a free software, you don’t have to trust us with your data and can
+Since WeKan ® is a free software, you don’t have to trust us with your data and can
 install Wekan on your own computer or server. In fact we encourage you to do
 that by providing one-click installation on various platforms.
 
-- Wekan is used in [most countries of the world](https://snapcraft.io/wekan).
+- WeKan ® is used in [most countries of the world](https://snapcraft.io/wekan).
 - Wekan largest user has 13k users using Wekan in their company.
-- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 63 languages.
-- [Features][features]: Wekan has real-time user interface.
-- [Platforms][platforms]: Wekan supports many platforms.
-  Wekan is critical part of new platforms Wekan is currently being integrated to.
+- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 70 languages.
+- [Features][features]: WeKan ® has real-time user interface.
+- [Platforms][platforms]: WeKan ® supports many platforms.
+  WeKan ® is critical part of new platforms Wekan is currently being integrated to.
 
 ## Requirements
 
 - 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.io) /
   [Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows).
   [More Platforms](https://github.com/wekan/wekan/wiki/Platforms), 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/master/docker-compose.yml): 3 frontend servers,
   each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
 - Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
-- SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
-  Old versions have security issues because of old versions Node.js etc. Only newest Wekan is supported.
-  Wekan on Sandstorm is not usually affected by any Standalone Wekan (Snap/Docker/Source) security issues.
+- SECURITY: Updating to newest WeKan ® version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
+  Old versions have security issues because of old versions Node.js etc. Only newest WeKan ® is supported.
+  WeKan ® on Sandstorm is not usually affected by any Standalone WeKan ® (Snap/Docker/Source) security issues.
 - [Reporting all new bugs immediately](https://github.com/wekan/wekan/issues).
-  New features and fixes are added to Wekan [many times a day](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md).
-- [Backups](https://github.com/wekan/wekan/wiki/Backup) of Wekan database once a day miminum.
+  New features and fixes are added to WeKan ® [many times a day](https://github.com/wekan/wekan/blob/master/CHANGELOG.md).
+- [Backups](https://github.com/wekan/wekan/wiki/Backup) of WeKan ® database once a day miminum.
   Bugs, updates, users deleting list or card, harddrive full, harddrive crash etc can eat your data. There is no undo yet.
-  Some bug can cause Wekan board to not load at all, requiring manual fixing of database content.
+  Some bug can cause WeKan ® board to not load at all, requiring manual fixing of database content.
 
 ## Roadmap and Demo
 
-[Roadmap][roadmap_wekan] - Public read-only board at Wekan demo.
+[Roadmap][roadmap_wekan] - Public read-only board at WeKan ® demo.
 
 [Developer Documentation][dev_docs]
 
-- There is many companies and individuals contributing code to Wekan, to add features and bugfixes
-  [many times a day](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md).
+- There is many companies and individuals contributing code to WeKan ®, to add features and bugfixes
+  [many times a day](https://github.com/wekan/wekan/blob/master/CHANGELOG.md).
 - [Please add Add new Feature Requests and Bug Reports immediately](https://github.com/wekan/wekan/issues).
 - [Commercial Support](https://wekan.team/commercial-support/).
 
 We also welcome sponsors for features and bugfixes.
-By working directly with Wekan you get the benefit of active maintenance and new features added by growing Wekan developer community.
+By working directly with WeKan ® you get the benefit of active maintenance and new features added by growing WeKan ® developer community.
 
 ## Screenshot
 
 [More screenshots at Features page](https://github.com/wekan/wekan/wiki/Features)
 
-[![Screenshot of Wekan][screenshot_wekan]][roadmap_wekan]
+[![Screenshot of WeKan ®][screenshot_wekan]][roadmap_wekan]
 
 ## License
 
-Wekan is released under the very permissive [MIT license](LICENSE), and made
+WeKan ® is released under the very permissive [MIT license](LICENSE), and made
 with [Meteor](https://www.meteor.com).
 
 [platforms]: https://github.com/wekan/wekan/wiki/Platforms

+ 4 - 4
SECURITY.md

@@ -51,8 +51,8 @@ This also means all Standalone Wekan functionality works in offline local networ
 Wekan is used by companies that have [thousands of users](https://github.com/wekan/wekan/wiki/AWS) and at healthcare.
 
 Wekan uses xss package for input fields like cards, as you can see from
-[package.json](https://github.com/wekan/wekan/blob/devel/package.json). Other used versions can be seen from
-[Meteor versions file](https://github.com/wekan/wekan/blob/devel/.meteor/versions).
+[package.json](https://github.com/wekan/wekan/blob/master/package.json). Other used versions can be seen from
+[Meteor versions file](https://github.com/wekan/wekan/blob/master/.meteor/versions).
 Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
 It's possible to add attachments to cards, and markdown/html links to files.
 
@@ -69,7 +69,7 @@ access to outside of Wekan grain.
 Standalone Wekan only has password auth currently, there is work in progress to add
 [oauth2](https://github.com/wekan/wekan/pull/1578), [Openid](https://github.com/wekan/wekan/issues/538),
 [LDAP](https://github.com/wekan/wekan/issues/119) etc. If you need more login security for Standalone Wekan now,
-it's possible add additional [Google Auth proxybouncer](https://github.com/wekan/wekan/wiki/Let's-Encrypt-and-Google-Auth) in front of password auth, and then use Google Authenticator for Google Auth. Standalone Wekan does have [brute force protection with eluck:accounts-lockout and browser-policy clickjacking protection](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md#v080-2018-04-04-wekan-release). You can also optionally use some [WAF](https://en.wikipedia.org/wiki/Web_application_firewall)
+it's possible add additional [Google Auth proxybouncer](https://github.com/wekan/wekan/wiki/Let's-Encrypt-and-Google-Auth) in front of password auth, and then use Google Authenticator for Google Auth. Standalone Wekan does have [brute force protection with eluck:accounts-lockout and browser-policy clickjacking protection](https://github.com/wekan/wekan/blob/master/CHANGELOG.md#v080-2018-04-04-wekan-release). You can also optionally use some [WAF](https://en.wikipedia.org/wiki/Web_application_firewall)
 like for example [AWS WAF](https://aws.amazon.com/waf/).
 
 [All Wekan Platforms](https://github.com/wekan/wekan/wiki/Platforms)
@@ -106,7 +106,7 @@ a security issue, we'd like to know about it, and also how to fix it:
 Typical already known or "no impact" bugs such as:
 
 - Brute force password guessign. Currently there is
-  [brute force protection with eluck:accounts-lockout](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md#v080-2018-04-04-wekan-release).
+  [brute force protection with eluck:accounts-lockout](https://github.com/wekan/wekan/blob/master/CHANGELOG.md#v080-2018-04-04-wekan-release).
 - Security issues related to that Wekan uses Meteor 1.6.0.1 related packages, and upgrading to newer
   Meteor 1.6.1 is complicated process that requires lots of changes to many dependency packages.
   Upgrading [has been tried many times, spending a lot of time](https://github.com/meteor/meteor/issues/9609)

+ 1 - 1
Stackerfile.yml

@@ -1,5 +1,5 @@
 appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
-appVersion: "v5.47.0"
+appVersion: "v5.85.0"
 files:
   userUploads:
     - README.md

+ 95 - 18
api.py

@@ -5,6 +5,9 @@
 # Wekan API Python CLI, originally from here, where is more details:
 # https://github.com/wekan/wekan/wiki/New-card-with-Python3-and-REST-API
 
+# TODO:
+#   addcustomfieldtoboard: There is error: Settings must be object. So adding does not work yet.
+
 try:
     # python 3
     from urllib.parse import urlencode
@@ -23,12 +26,16 @@ if arguments == 0:
     print("AUTHORID is USERID that writes card.")
     print("If *nix:  chmod +x api.py => ./api.py users")
     print("Syntax:")
-    print("  python3 api.py users              # All users")
-    print("  python3 api.py boards USERID      # Boards of USERID")
-    print("  python3 api.py board BOARDID      # Info of BOARDID")
-    print("  python3 api.py swimlanes BOARDID  # Swimlanes of BOARDID")
-    print("  python3 api.py lists BOARDID      # Lists of BOARDID")
-    print("  python3 api.py list BOARDID LISTID # Info of LISTID")
+    print("  python3 api.py users                # All users")
+    print("  python3 api.py boards               # All Public Boards")
+    print("  python3 api.py boards USERID        # Boards of USERID")
+    print("  python3 api.py board BOARDID        # Info of BOARDID")
+    print("  python3 api.py customfields BOARDID # Custom Fields of BOARDID")
+    print("  python3 api.py customfield BOARDID CUSTOMFIELDID # Info of CUSTOMFIELDID")
+    print("  python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board")
+    print("  python3 api.py swimlanes BOARDID    # Swimlanes of BOARDID")
+    print("  python3 api.py lists BOARDID        # Lists of BOARDID")
+    print("  python3 api.py list BOARDID LISTID  # Info of LISTID")
     print("  python3 api.py createlist BOARDID LISTTITLE # Create list")
     print("  python3 api.py addcard AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION")
     print("  python3 api.py editcard BOARDID LISTID CARDID NEWCARDTITLE NEWCARDDESCRIPTION")
@@ -65,12 +72,15 @@ chmod +x api.py
 === Wekan API Python CLI: Shows IDs for addcard ===
 AUTHORID is USERID that writes card.
 Syntax:
-  python3 api.py users               # All users
-  python3 api.py boards USERID       # Boards of USERID
-  python3 api.py board BOARDID       # Info of BOARDID
-  python3 api.py swimlanes BOARDID   # Swimlanes of BOARDID
-  python3 api.py lists BOARDID       # Lists of BOARDID
-  python3 api.py list BOARDID LISTID # Info of LISTID
+  python3 api.py users                # All users
+  python3 api.py boards USERID        # Boards of USERID
+  python3 api.py board BOARDID        # Info of BOARDID
+  python3 api.py customfields BOARDID # Custom Fields of BOARDID
+  python3 api.py customfield BOARDID CUSTOMFIELDID # Info of CUSTOMFIELDID
+  python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
+  python3 api.py swimlanes BOARDID    # Swimlanes of BOARDID
+  python3 api.py lists BOARDID        # Lists of BOARDID
+  python3 api.py list BOARDID LISTID  # Info of LISTID
   python3 api.py createlist BOARDID LISTTITLE # Create list
   python3 api.py addcard AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION
   python3 api.py editcard BOARDID LISTID CARDID NEWCARDTITLE NEWCARDDESCRIPTION
@@ -78,6 +88,13 @@ Syntax:
   python3 api.py attachmentjson BOARDID ATTACHMENTID # One attachment as JSON base64
   python3 api.py attachmentbinary BOARDID ATTACHMENTID # One attachment as binary file
 
+=== ADD CUSTOM FIELD TO BOARD ===
+
+Type: text, number, date, dropdown, checkbox, currency, stringtemplate.
+
+python3 api.py addcustomfieldtoboard cmx3gmHLKwAXLqjxz LcDW4QdooAx8hsZh8 "SomeField" "date" "" true true true true 
+
+
 === USERS ===
 
 python3 api.py users
@@ -133,6 +150,7 @@ l = 'lists'
 sw = 'swimlane'
 sws = 'swimlanes'
 cs = 'cards'
+cf = 'custom-fields'
 bs = 'boards'
 atl = 'attachmentslist'
 at = 'attachment'
@@ -150,10 +168,34 @@ apikey = d['token']
 
 # ------- LOGIN TOKEN END -----------
 
+if arguments == 10:
+
+    if sys.argv[1] == 'addcustomfieldtoboard':
+        # ------- ADD CUSTOM FIELD TO BOARD START -----------
+        authorid = sys.argv[2]
+        boardid = sys.argv[3]
+        name = sys.argv[4]
+        type1 = sys.argv[5]
+        settings = str(json.loads(sys.argv[6]))
+        #  There is error: Settings must be object. So this does not work yet.
+        #settings = {'currencyCode': 'EUR'}
+        print(type(settings))
+        showoncard = sys.argv[7]
+        automaticallyoncard = sys.argv[8]
+        showlabelonminicard = sys.argv[9]
+        showsumattopoflist = sys.argv[10]
+        customfieldtoboard = wekanurl + apiboards + boardid + s + cf
+        # Add Custom Field to Board
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        post_data = {'authorId': '{}'.format(authorid), 'name': '{}'.format(name), 'type': '{}'.format(type1), 'settings': '{}'.format(settings), 'showoncard': '{}'.format(showoncard), 'automaticallyoncard': '{}'.format(automaticallyoncard), 'showlabelonminicard': '{}'.format(showlabelonminicard), 'showsumattopoflist': '{}'.format(showsumattopoflist)}
+        body = requests.post(customfieldtoboard, data=post_data, headers=headers)
+        print(body.text)
+        # ------- ADD CUSTOM FIELD TO BOARD END -----------
+
 if arguments == 7:
 
     if sys.argv[1] == 'addcard':
-        # ------- WRITE TO CARD START -----------
+        # ------- ADD CARD START -----------
         authorid = sys.argv[2]
         boardid = sys.argv[3]
         swimlaneid = sys.argv[4]
@@ -161,18 +203,18 @@ if arguments == 7:
         cardtitle = sys.argv[6]
         carddescription = sys.argv[7]
         cardtolist = wekanurl + apiboards + boardid + s + l + s + listid + s + cs
-        # Write to card
+        # Add card
         headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
         post_data = {'authorId': '{}'.format(authorid), 'title': '{}'.format(cardtitle), 'description': '{}'.format(carddescription), 'swimlaneId': '{}'.format(swimlaneid)}
         body = requests.post(cardtolist, data=post_data, headers=headers)
         print(body.text)
-        # ------- WRITE TO CARD END -----------
+        # ------- ADD CARD END -----------
 
 if arguments == 6:
 
     if sys.argv[1] == 'editcard':
 
-        # ------- LIST OF BOARD START -----------
+        # ------- EDIT CARD START -----------
         boardid = sys.argv[2]
         listid = sys.argv[3]
         cardid = sys.argv[4]
@@ -187,7 +229,7 @@ if arguments == 6:
         body = requests.get(edcard, headers=headers)
         data2 = body.text.replace('}',"}\n")
         print(data2)
-        # ------- LISTS OF BOARD END -----------
+        # ------- EDIT CARD END -----------
 
 if arguments == 3:
 
@@ -217,6 +259,19 @@ if arguments == 3:
         print(data2)
         # ------- LISTS OF BOARD END -----------
 
+    if sys.argv[1] == 'customfield':
+
+        # ------- INFO OF CUSTOM FIELD START -----------
+        boardid = sys.argv[2]
+        customfieldid = sys.argv[3]
+        customfieldone = wekanurl + apiboards + boardid + s + cf + s + customfieldid
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        print("=== INFO OF ONE CUSTOM FIELD ===\n")
+        body = requests.get(customfieldone, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- INFO OF CUSTOM FIELD END -----------
+
 if arguments == 2:
 
     # ------- BOARDS LIST START -----------
@@ -230,8 +285,8 @@ if arguments == 2:
         data2 = body.text.replace('}',"}\n")
         print(data2)
     # ------- BOARDS LIST END -----------
-    if sys.argv[1] == 'board':
 
+    if sys.argv[1] == 'board':
         # ------- BOARD INFO START -----------
         boardid = sys.argv[2]
         board = wekanurl + apiboards + boardid
@@ -242,6 +297,17 @@ if arguments == 2:
         print(data2)
         # ------- BOARD INFO END -----------
 
+    if sys.argv[1] == 'customfields':
+        # ------- CUSTOM FIELDS OF BOARD START -----------
+        boardid = sys.argv[2]
+        boardcustomfields = wekanurl + apiboards + boardid + s + cf
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        body = requests.get(boardcustomfields, headers=headers)
+        print("=== CUSTOM FIELDS OF BOARD ===\n")
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- CUSTOM FIELDS OF BOARD END -----------
+
     if sys.argv[1] == 'swimlanes':
         boardid = sys.argv[2]
         swimlanes = wekanurl + apiboards + boardid + s + sws
@@ -289,3 +355,14 @@ if arguments == 1:
         data2 = body.text.replace('}',"}\n")
         print(data2)
         # ------- LIST OF USERS END -----------
+
+    if sys.argv[1] == 'boards':
+
+        # ------- LIST OF PUBLIC BOARDS START -----------
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        print("=== PUBLIC BOARDS ===\n")
+        listpublicboards = wekanurl + apiboards
+        body = requests.get(listpublicboards, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- LIST OF PUBLIC BOARDS END -----------

+ 26 - 4
client/components/activities/activities.jade

@@ -12,7 +12,7 @@ template(name="boardActivities")
     +activity(activity=activityData card=card mode=mode)
 
 template(name="cardActivities")
-  each activityData in currentCard.activities
+  each activityData in activities
     +activity(activity=activityData card=card mode=mode)
 
 template(name="editOrDeleteComment")
@@ -21,6 +21,26 @@ template(name="editOrDeleteComment")
   = ' - '
   a.js-delete-comment {{_ "delete"}}
 
+template(name="deleteCommentPopup")
+  p {{_ "comment-delete"}}
+  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="commentReactions")
+  .reactions
+    each reaction in reactions
+      span.reaction(class="{{#if isSelected reaction.userIds}}selected{{/if}}" data-codepoint="#{reaction.reactionCodepoint}" title="{{userNames reaction.userIds}}")
+        span.reaction-codepoint !{reaction.reactionCodepoint}
+        span.reaction-count #{reaction.userIds.length}
+    if (currentUser.isBoardMember)
+      a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
+        i.fa.fa-smile-o
+        i.fa.fa-plus
+
+template(name="addReactionPopup")
+  .reactions-popup
+    each codepoint in codepoints
+      span.add-comment-reaction(data-codepoint="#{codepoint}") !{codepoint}
+
 template(name="activity")
   .activity
     +userAvatar(userId=activity.user._id)
@@ -120,10 +140,12 @@ template(name="activity")
               = activity.comment.text
             .edit-controls
               button.primary(type="submit") {{_ 'edit'}}
+              .fa.fa-times-thin.js-close-inlined-form
           else
             .activity-comment
               +viewer
                 = activity.comment.text
+            +commentReactions(reactions=activity.comment.reactions commentId=activity.comment._id)
             span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
               if($eq currentUser._id activity.comment.userId)
                 +editOrDeleteComment
@@ -150,20 +172,20 @@ template(name="activity")
 
         if($eq activity.activityType 'a-startAt')
           | {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
-        
+
         if($eq activity.activityType 'a-dueAt')
           | {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
 
         if($eq activity.activityType 'a-endAt')
           | {{{_ 'activity-endDate' (sanitize endDate) cardLink}}}.
-      
+
       if($eq mode 'board')
         if($eq activity.activityType 'a-receivedAt')
           | {{{_ 'activity-receivedDate' (sanitize receivedDate) cardLink}}}.
 
         if($eq activity.activityType 'a-startAt')
           | {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
-        
+
         if($eq activity.activityType 'a-dueAt')
           | {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
 

+ 73 - 9
client/components/activities/activities.js

@@ -13,14 +13,14 @@ BlazeComponent.extendComponent({
     this.autorun(() => {
       let mode = this.data().mode;
       const capitalizedMode = Utils.capitalize(mode);
-      let thisId, searchId;
+      let searchId;
       if (mode === 'linkedcard' || mode === 'linkedboard') {
-        thisId = Session.get('currentCard');
-        searchId = Cards.findOne({ _id: thisId }).linkedId;
+        searchId = Utils.getCurrentCard().linkedId;
         mode = mode.replace('linked', '');
+      } else if (mode === 'card') {
+        searchId = Utils.getCurrentCardId();
       } else {
-        thisId = Session.get(`current${capitalizedMode}`);
-        searchId = thisId;
+        searchId = Session.get(`current${capitalizedMode}`);
       }
       const limit = this.page.get() * activitiesPerPage;
       const user = Meteor.user();
@@ -54,6 +54,13 @@ BlazeComponent.extendComponent({
   },
 }).register('activities');
 
+Template.activities.helpers({
+  activities() {
+    const ret = this.card.activities();
+    return ret;
+  },
+});
+
 BlazeComponent.extendComponent({
   checkItem() {
     const checkItemId = this.currentData().activity.checklistItemId;
@@ -113,8 +120,10 @@ BlazeComponent.extendComponent({
     ).getLabelById(lastLabelId);
     if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
       return lastLabel.color;
-    } else {
+    } else if (lastLabel.name !== undefined && lastLabel.name !== '') {
       return lastLabel.name;
+    } else {
+      return null;
     }
   },
 
@@ -211,10 +220,11 @@ BlazeComponent.extendComponent({
     return [
       {
         // XXX We should use Popup.afterConfirmation here
-        'click .js-delete-comment'() {
-          const commentId = this.currentData().activity.commentId;
+        'click .js-delete-comment': Popup.afterConfirm('deleteComment', () => {
+          const commentId = this.data().activity.commentId;
           CardComments.remove(commentId);
-        },
+          Popup.back();
+        }),
         'submit .js-edit-comment'(evt) {
           evt.preventDefault();
           const commentText = this.currentComponent()
@@ -240,6 +250,60 @@ Template.activity.helpers({
   },
 });
 
+Template.commentReactions.events({
+  'click .reaction'(event) {
+    if (Meteor.user().isBoardMember()) {
+      const codepoint = event.currentTarget.dataset['codepoint'];
+      const commentId = Template.instance().data.commentId;
+      const cardComment = CardComments.findOne({_id: commentId});
+      cardComment.toggleReaction(codepoint);
+    }
+  },
+  'click .open-comment-reaction-popup': Popup.open('addReaction'),
+})
+
+Template.addReactionPopup.events({
+  'click .add-comment-reaction'(event) {
+    if (Meteor.user().isBoardMember()) {
+      const codepoint = event.currentTarget.dataset['codepoint'];
+      const commentId = Template.instance().data.commentId;
+      const cardComment = CardComments.findOne({_id: commentId});
+      cardComment.toggleReaction(codepoint);
+    }
+    Popup.back();
+  },
+})
+
+Template.addReactionPopup.helpers({
+  codepoints() {
+    // Starting set of unicode codepoints as comment reactions
+    return [
+      '&#128077;',
+      '&#128078;',
+      '&#128064;',
+      '&#9989;',
+      '&#10060;',
+      '&#128591;',
+      '&#128079;',
+      '&#127881;',
+      '&#128640;',
+      '&#128522;',
+      '&#129300;',
+      '&#128532;'];
+  }
+})
+
+Template.commentReactions.helpers({
+  isSelected(userIds) {
+    return userIds.includes(Meteor.user()._id);
+  },
+  userNames(userIds) {
+    return Users.find({_id: {$in: userIds}})
+                .map(user => user.profile.fullname)
+                .join(', ');
+  }
+})
+
 function createCardLink(card) {
   if (!card) return '';
   return (

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

@@ -5,6 +5,20 @@
   display: flex
   justify-content:space-between
 
+.reactions-popup
+  .add-comment-reaction
+    display: inline-block
+    cursor: pointer
+    border-radius: 5px
+    font-size: 22px
+    text-align: center
+    line-height: 30px
+    width: 40px
+
+    &:hover {
+      background-color: #b0c4de
+    }
+
 .activities
   clear: both
 
@@ -18,7 +32,7 @@
       height: @width
 
     .activity-member
-      font-weight: 700      
+      font-weight: 700
 
     .activity-desc
       word-wrap: break-word
@@ -39,6 +53,45 @@
         margin-top: 5px
         padding: 5px
 
+      .reactions
+        display: flex
+        margin-top: 5px
+        gap: 5px
+
+        .open-comment-reaction-popup
+          display: flex
+          align-items: center
+          text-decoration: none
+          height: 24px;
+
+          i.fa.fa-smile-o
+            font-size: 17px
+            font-weight: 500
+            margin-left: 2px
+
+          i.fa.fa-plus
+            font-size: 8px;
+            margin-top: -7px;
+            margin-left: 1px;
+
+        .reaction
+          cursor: pointer
+          border: 1px solid grey
+          border-radius: 15px
+          display: flex
+          padding: 2px 5px
+
+          &.selected {
+            background-color: #b0c4de
+          }
+
+          &:hover {
+            background-color: #b0c4de
+          }
+
+          .reaction-count
+            font-size: 12px
+
       .activity-checklist
         display: block
         border-radius: 3px

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

@@ -64,7 +64,7 @@ function resetCommentInput(input) {
 // Tracker.autorun to register the component dependencies, and re-run when these
 // dependencies are invalidated. A better component API would remove this hack.
 Tracker.autorun(() => {
-  Session.get('currentCard');
+  Utils.getCurrentCardId();
   Tracker.afterFlush(() => {
     autosize.update($('.js-new-comment-input'));
   });
@@ -75,7 +75,7 @@ EscapeActions.register(
   () => {
     const draftKey = {
       fieldName: 'cardComment',
-      docId: Session.get('currentCard'),
+      docId: Utils.getCurrentCardId(),
     };
     const commentInput = $('.js-new-comment-input');
     const draft = commentInput.val().trim();

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

@@ -31,7 +31,6 @@
     background-color: #fff
     border: 0
     box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
-    color: #8c8c8c
     height: 36px
     margin: 4px 4px 6px 0
     padding: 9px 11px

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

@@ -34,7 +34,7 @@ BlazeComponent.extendComponent({
           Utils.goBoardId(board._id);
         },
         'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
-          Popup.close();
+          Popup.back();
           const isSandstorm =
             Meteor.settings &&
             Meteor.settings.public &&

+ 23 - 20
client/components/boards/boardBody.jade

@@ -13,26 +13,29 @@ template(name="board")
     +spinner
 
 template(name="boardBody")
-  .board-wrapper(class=currentBoard.colorClass)
-    +sidebar
-    .board-canvas.js-swimlanes(
-      class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
-      class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
-      class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
-      if showOverlay.get
-        .board-overlay
-      if currentBoard.isTemplatesBoard
-        each currentBoard.swimlanes
-          +swimlane(this)
-      else if isViewSwimlanes
-        each currentBoard.swimlanes
-          +swimlane(this)
-      else if isViewLists
-        +listsGroup(currentBoard)
-      else if isViewCalendar
-        +calendarView
-      else
-        +listsGroup(currentBoard)
+  if notDisplayThisBoard
+   | {{_ 'tableVisibilityMode-allowPrivateOnly'}}
+  else
+    .board-wrapper(class=currentBoard.colorClass)
+      +sidebar
+      .board-canvas.js-swimlanes(
+        class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
+        class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
+        class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
+        if showOverlay.get
+          .board-overlay
+        if currentBoard.isTemplatesBoard
+          each currentBoard.swimlanes
+            +swimlane(this)
+        else if isViewSwimlanes
+          each currentBoard.swimlanes
+            +swimlane(this)
+        else if isViewLists
+          +listsGroup(currentBoard)
+        else if isViewCalendar
+          +calendarView
+        else
+          +listsGroup(currentBoard)
 
 template(name="calendarView")
   if isViewCalendar

+ 16 - 14
client/components/boards/boardBody.js

@@ -23,7 +23,7 @@ BlazeComponent.extendComponent({
   },
 
   onlyShowCurrentCard() {
-    return Utils.isMiniScreen() && Session.get('currentCard');
+    return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
   },
 
   goHome() {
@@ -33,6 +33,7 @@ BlazeComponent.extendComponent({
 
 BlazeComponent.extendComponent({
   onCreated() {
+    Meteor.subscribe('tableVisibilityModeSettings');
     this.showOverlay = new ReactiveVar(false);
     this.draggingActive = new ReactiveVar(false);
     this._isDragging = false;
@@ -190,21 +191,11 @@ BlazeComponent.extendComponent({
     });
 
     this.autorun(() => {
-      let showDesktopDragHandles = false;
-      currentUser = Meteor.user();
-      if (currentUser) {
-        showDesktopDragHandles = (currentUser.profile || {})
-          .showDesktopDragHandles;
-      } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-        showDesktopDragHandles = true;
-      } else {
-        showDesktopDragHandles = false;
-      }
-      if (Utils.isMiniScreen() || showDesktopDragHandles) {
+      if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
         $swimlanesDom.sortable({
           handle: '.js-swimlane-header-handle',
         });
-      } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
+      } else {
         $swimlanesDom.sortable({
           handle: '.swimlane-header',
         });
@@ -215,7 +206,7 @@ BlazeComponent.extendComponent({
       $swimlanesDom.sortable(
         'option',
         'disabled',
-        !Meteor.user().isBoardAdmin(),
+        !Meteor.user() || !Meteor.user().isBoardAdmin(),
       );
     });
 
@@ -235,6 +226,16 @@ BlazeComponent.extendComponent({
     }
   },
 
+  notDisplayThisBoard(){
+    let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
+    let currentBoard = Boards.findOne(Session.get('currentBoard'));
+    if(allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public'){
+      return true;
+    }
+
+    return false;
+  },
+
   isViewSwimlanes() {
     currentUser = Meteor.user();
     if (currentUser) {
@@ -325,6 +326,7 @@ BlazeComponent.extendComponent({
       defaultView: 'agendaDay',
       editable: true,
       timezone: 'local',
+      weekNumbers: true,
       header: {
         left: 'title   today prev,next',
         center:

+ 13 - 6
client/components/boards/boardColors.styl

@@ -876,7 +876,7 @@ setBoardClear(color1,color2)
     padding: 10px
     top: 0
 
-  .list-header .list-header-plus-icon
+  .list-header .list-header-plus-top
     color: #a6a6a6
 
   .list-body
@@ -956,17 +956,24 @@ setBoardClear(color1,color2)
 
   /* Card Details */
   .card-details
-    position: absolute
-    top: 30px
-    left: calc(50% - 384px)
-    width: 768px
-    max-height: calc(100% - 60px)
     background-color: #454545
     color: #cccccc
     box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)
     border: 1px solid #111111
     z-index: 100 !important
 
+  @media screen and (max-width: 800px)
+    .card-details
+      width: 98%
+
+  @media screen and (min-width: 801px)
+    .card-details
+      position: absolute
+      top: 30px
+      left: calc(50% - 384px)
+      width: 768px
+      max-height: calc(100% - 60px)
+
   .card-details
     scrollbar-width: thin
     scrollbar-color: #343434 #999999

+ 15 - 8
client/components/boards/boardHeader.jade

@@ -80,6 +80,12 @@ template(name="boardHeaderBar")
             if $eq watchLevel "muted"
               i.fa.fa-bell-slash
             span {{_ watchLevel}}
+          a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
+            i.fa.fa-sort
+            span {{#if isSortActive }}{{_ 'Sort is on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
+            if isSortActive
+              a.board-header-btn-close.js-sort-reset(title="Remove Sort")
+                i.fa.fa-times-thin
 
         else
           a.board-header-btn.js-log-in(
@@ -147,14 +153,15 @@ template(name="boardVisibilityList")
           if visibilityCheck
             i.fa.fa-check
           span.sub-name {{_ 'private-desc'}}
-    li
-      with "public"
-        a.js-select-visibility
-          i.fa.fa-globe.colorful
-          | {{_ 'public'}}
-          if visibilityCheck
-            i.fa.fa-check
-          span.sub-name {{_ 'public-desc'}}
+    if notAllowPrivateVisibilityOnly
+      li
+        with "public"
+          a.js-select-visibility
+            i.fa.fa-globe.colorful
+            | {{_ 'public'}}
+            if visibilityCheck
+              i.fa.fa-check
+            span.sub-name {{_ 'public-desc'}}
 
 template(name="boardChangeVisibilityPopup")
   +boardVisibilityList

+ 23 - 15
client/components/boards/boardHeader.js

@@ -7,11 +7,11 @@ Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
   'click .js-custom-fields'() {
     Sidebar.setView('customFields');
-    Popup.close();
+    Popup.back();
   },
   'click .js-open-archives'() {
     Sidebar.setView('archives');
-    Popup.close();
+    Popup.back();
   },
   'click .js-change-board-color': Popup.open('boardChangeColor'),
   'click .js-change-language': Popup.open('changeLanguage'),
@@ -24,7 +24,7 @@ Template.boardMenuPopup.events({
   }),
   'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
-    Popup.close();
+    Popup.back();
     Boards.remove(currentBoard._id);
     FlowRouter.go('home');
   }),
@@ -47,7 +47,7 @@ Template.boardChangeTitlePopup.events({
     if (newTitle) {
       this.rename(newTitle);
       this.setDescription(newDesc);
-      Popup.close();
+      Popup.back();
     }
     event.preventDefault();
   },
@@ -136,7 +136,7 @@ BlazeComponent.extendComponent({
           Sidebar.setView('search');
         },
         'click .js-multiselection-activate'() {
-          const currentCard = Session.get('currentCard');
+          const currentCard = Utils.getCurrentCardId();
           MultiSelection.activate();
           if (currentCard) {
             MultiSelection.add(currentCard);
@@ -173,15 +173,15 @@ Template.boardHeaderBar.helpers({
 Template.boardChangeViewPopup.events({
   'click .js-open-lists-view'() {
     Utils.setBoardView('board-view-lists');
-    Popup.close();
+    Popup.back();
   },
   'click .js-open-swimlanes-view'() {
     Utils.setBoardView('board-view-swimlanes');
-    Popup.close();
+    Popup.back();
   },
   'click .js-open-cal-view'() {
     Utils.setBoardView('board-view-cal');
-    Popup.close();
+    Popup.back();
   },
 });
 
@@ -194,6 +194,11 @@ const CreateBoard = BlazeComponent.extendComponent({
     this.visibilityMenuIsOpen = new ReactiveVar(false);
     this.visibility = new ReactiveVar('private');
     this.boardId = new ReactiveVar('');
+    Meteor.subscribe('tableVisibilityModeSettings');
+  },
+
+  notAllowPrivateVisibilityOnly(){
+    return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
   },
 
   visibilityCheck() {
@@ -310,6 +315,9 @@ const CreateBoard = BlazeComponent.extendComponent({
 }.register('headerBarCreateBoardPopup'));
 
 BlazeComponent.extendComponent({
+  notAllowPrivateVisibilityOnly(){
+    return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
+  },
   visibilityCheck() {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     return this.currentData() === currentBoard.permission;
@@ -319,7 +327,7 @@ BlazeComponent.extendComponent({
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     const visibility = this.currentData();
     currentBoard.setVisibility(visibility);
-    Popup.close();
+    Popup.back();
   },
 
   events() {
@@ -352,7 +360,7 @@ BlazeComponent.extendComponent({
             Session.get('currentBoard'),
             level,
             (err, ret) => {
-              if (!err && ret) Popup.close();
+              if (!err && ret) Popup.back();
             },
           );
         },
@@ -424,7 +432,7 @@ BlazeComponent.extendComponent({
           const direction = down ? -1 : 1;
           this.setSortBy([sortby, direction]);
           if (Utils.isMiniScreen) {
-            Popup.close();
+            Popup.back();
           }
         },
       },
@@ -443,7 +451,7 @@ BlazeComponent.extendComponent({
           };
           Session.set('sortBy', sortBy);
           sortCardsBy.set(TAPi18n.__('due-date'));
-          Popup.close();
+          Popup.back();
         },
         'click .js-sort-title'() {
           const sortBy = {
@@ -451,7 +459,7 @@ BlazeComponent.extendComponent({
           };
           Session.set('sortBy', sortBy);
           sortCardsBy.set(TAPi18n.__('title'));
-          Popup.close();
+          Popup.back();
         },
         'click .js-sort-created-asc'() {
           const sortBy = {
@@ -459,7 +467,7 @@ BlazeComponent.extendComponent({
           };
           Session.set('sortBy', sortBy);
           sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
-          Popup.close();
+          Popup.back();
         },
         'click .js-sort-created-desc'() {
           const sortBy = {
@@ -467,7 +475,7 @@ BlazeComponent.extendComponent({
           };
           Session.set('sortBy', sortBy);
           sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
-          Popup.close();
+          Popup.back();
         },
       },
     ];

+ 26 - 5
client/components/boards/boardsList.jade

@@ -1,11 +1,32 @@
 template(name="boardList")
   .wrapper
+    ul.AllBoardTeamsOrgs
+      li.AllBoardTeams
+        if userHasTeams
+          select.js-AllBoardTeams#jsAllBoardTeams("multiple")
+            option(value="-1") {{_ 'teams'}} :
+            each teamsDatas
+              option(value="{{teamId}}") {{_ teamDisplayName}}
+
+      li.AllBoardOrgs
+        if userHasOrgs
+          select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
+            option(value="-1") {{_ 'organizations'}} :
+            each orgsDatas
+              option(value="{{orgId}}") {{_ orgDisplayName}}
+      li.AllBoardBtns
+        div.AllBoardButtonsContainer
+          if userHasOrgsOrTeams
+            i.fa.fa-filter
+            input#filterBtn(type="button" value="{{_ 'filter'}}")
+            input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
+
     ul.board-list.clearfix.js-boards
       li.js-add-board
         a.board-list-item.label(title="{{_ 'add-board'}}")
           | {{_ 'add-board'}}
       each boards
-        li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
+        li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
           if isInvited
             .board-list-item
               span.details
@@ -33,11 +54,11 @@ template(name="boardList")
                     i.fa.js-has-spenttime-cards(
                       class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
                       title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
-                  if isMiniScreen
+                  if isMiniScreenOrShowDesktopDragHandles
                     i.fa.board-handle(
                         class="fa-arrows"
                         title="{{_ 'Drag board'}}")
-                  unless isMiniScreen
+                  else
                     if isSandstorm
                       i.fa.js-clone-board(
                           class="fa-clone"
@@ -75,11 +96,11 @@ template(name="boardList")
                     i.fa.js-has-spenttime-cards(
                       class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
                       title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
-                  if isMiniScreen
+                  if isMiniScreenOrShowDesktopDragHandles
                     i.fa.board-handle(
                         class="fa-arrows"
                         title="{{_ 'Drag board'}}")
-                  unless isMiniScreen
+                  else
                     if isSandstorm
                       i.fa.js-clone-board(
                           class="fa-clone"

+ 127 - 11
client/components/boards/boardsList.js

@@ -1,5 +1,4 @@
 const subManager = new SubsManager();
-const { calculateIndex, enableClickOnTouch } = Utils;
 
 Template.boardListHeaderBar.events({
   'click .js-open-archived-board'() {
@@ -22,6 +21,7 @@ Template.boardListHeaderBar.helpers({
 BlazeComponent.extendComponent({
   onCreated() {
     Meteor.subscribe('setting');
+    Meteor.subscribe('tableVisibilityModeSettings');
     let currUser = Meteor.user();
     let userLanguage;
     if(currUser && currUser.profile){
@@ -55,7 +55,7 @@ BlazeComponent.extendComponent({
         // 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 sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
 
         const boardDomElement = ui.item.get(0);
         const board = Blaze.getData(boardDomElement);
@@ -72,21 +72,56 @@ BlazeComponent.extendComponent({
       },
     });
 
-    // ugly touch event hotfix
-    enableClickOnTouch(itemsSelector);
-
     // Disable drag-dropping if the current user is not a board member or is comment only
     this.autorun(() => {
-      if (Utils.isMiniScreen()) {
+      if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
         $boards.sortable({
           handle: '.board-handle',
         });
       }
     });
   },
+  userHasTeams(){
+    if(Meteor.user() != null && Meteor.user().teams && Meteor.user().teams.length > 0)
+      return true;
+    else
+      return false;
+  },
+  teamsDatas() {
+    if(Meteor.user().teams)
+      return Meteor.user().teams.sort((a, b) => a.teamDisplayName.localeCompare(b.teamDisplayName));
+    else
+      return [];
+  },
+  userHasOrgs(){
+    if(Meteor.user() != null && Meteor.user().orgs && Meteor.user().orgs.length > 0)
+      return true;
+    else
+      return false;
+  },
+  orgsDatas() {
+    if(Meteor.user().orgs)
+      return Meteor.user().orgs.sort((a, b) => a.orgDisplayName.localeCompare(b.orgDisplayName));
+    else
+      return [];
+  },
+  userHasOrgsOrTeams(){
+    let boolUserHasOrgs;
+    if(Meteor.user() != null && Meteor.user().orgs && Meteor.user().orgs.length > 0)
+      boolUserHasOrgs = true;
+    else
+      boolUserHasOrgs = false;
+
+    let boolUserHasTeams;
+    if(Meteor.user() != null && Meteor.user().teams && Meteor.user().teams.length > 0)
+      boolUserHasTeams = true;
+    else
+      boolUserHasTeams = false;
 
+    return (boolUserHasOrgs || boolUserHasTeams);
+  },
   boards() {
-    const query = {
+    let query = {
       //archived: false,
       ////type: { $in: ['board','template-container'] },
       //type: 'board',
@@ -96,9 +131,15 @@ BlazeComponent.extendComponent({
         { $or:[] }
       ]
     };
+
+    let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
+
     if (FlowRouter.getRouteName() === 'home'){
       query.$and[2].$or.push({'members.userId': Meteor.userId()});
 
+      if(allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue){
+        query.$and.push({'permission': 'private'});
+      }
       const currUser = Users.findOne(Meteor.userId());
 
       // const currUser = Users.findOne(Meteor.userId(), {
@@ -108,7 +149,7 @@ BlazeComponent.extendComponent({
       //   },
       // });
 
-      let orgIdsUserBelongs = currUser.teams !== 'undefined' ? currUser.orgIdsUserBelongs() : '';
+      let orgIdsUserBelongs = currUser !== undefined && currUser.teams !== 'undefined' ? currUser.orgIdsUserBelongs() : '';
       if(orgIdsUserBelongs && orgIdsUserBelongs != ''){
         let orgsIds = orgIdsUserBelongs.split(',');
         // for(let i = 0; i < orgsIds.length; i++){
@@ -119,7 +160,7 @@ BlazeComponent.extendComponent({
         query.$and[2].$or.push({'orgs.orgId': {$in : orgsIds}});
       }
 
-      let teamIdsUserBelongs = currUser.teams !== 'undefined' ? currUser.teamIdsUserBelongs() : '';
+      let teamIdsUserBelongs = currUser !== undefined && currUser.teams !== 'undefined' ? currUser.teamIdsUserBelongs() : '';
       if(teamIdsUserBelongs && teamIdsUserBelongs != ''){
         let teamsIds = teamIdsUserBelongs.split(',');
         // for(let i = 0; i < teamsIds.length; i++){
@@ -129,10 +170,17 @@ BlazeComponent.extendComponent({
         query.$and[2].$or.push({'teams.teamId': {$in : teamsIds}});
       }
     }
-    else query.permission = 'public';
+    else if(allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue){
+      query = {
+        archived: false,
+        //type: { $in: ['board','template-container'] },
+        type: 'board',
+        permission: 'public',
+      };
+    }
 
     return Boards.find(query, {
-      //sort: { sort: 1 /* boards default sorting */ },
+      sort: { sort: 1 /* boards default sorting */ },
     });
   },
   isStarred() {
@@ -206,6 +254,74 @@ BlazeComponent.extendComponent({
             }
           });
         },
+        'click #resetBtn'(event){
+          let allBoards = document.getElementsByClassName("js-board");
+          let currBoard;
+          for(let i=0; i < allBoards.length; i++){
+            currBoard = allBoards[i];
+            currBoard.style.display = "block";
+          }
+        },
+        'click #filterBtn'(event) {
+          event.preventDefault();
+          let selectedTeams = document.querySelectorAll('#jsAllBoardTeams option:checked');
+          let selectedTeamsValues = Array.from(selectedTeams).map(function(elt){return elt.value});
+          let index = selectedTeamsValues.indexOf("-1");
+          if (index > -1) {
+            selectedTeamsValues.splice(index, 1);
+          }
+
+          let selectedOrgs = document.querySelectorAll('#jsAllBoardOrgs option:checked');
+          let selectedOrgsValues = Array.from(selectedOrgs).map(function(elt){return elt.value});
+          index = selectedOrgsValues.indexOf("-1");
+          if (index > -1) {
+            selectedOrgsValues.splice(index, 1);
+          }
+
+          if(selectedTeamsValues.length > 0 || selectedOrgsValues.length > 0){
+            const query = {
+              $and: [
+                { archived: false },
+                { type: 'board' },
+                { $or:[] }
+              ]
+            };
+            if(selectedTeamsValues.length > 0)
+            {
+              query.$and[2].$or.push({'teams.teamId': {$in : selectedTeamsValues}});
+            }
+            if(selectedOrgsValues.length > 0)
+            {
+              query.$and[2].$or.push({'orgs.orgId': {$in : selectedOrgsValues}});
+            }
+
+            let filteredBoards = Boards.find(query, {}).fetch();
+            let allBoards = document.getElementsByClassName("js-board");
+            let currBoard;
+            if(filteredBoards.length > 0){
+              let currBoardId;
+              let found;
+              for(let i=0; i < allBoards.length; i++){
+                currBoard = allBoards[i];
+                currBoardId = currBoard.classList[0];
+                found = filteredBoards.find(function(board){
+                  return board._id == currBoardId;
+                });
+
+                if(found !== undefined)
+                  currBoard.style.display = "block";
+                else
+                  currBoard.style.display = "none";
+              }
+            }
+            else{
+              for(let i=0; i < allBoards.length; i++){
+                currBoard = allBoards[i];
+                currBoard.style.display = "none";
+              }
+            }
+          }
+        },
       },
     ];
   },

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

@@ -229,3 +229,25 @@ $spaceBetweenTiles = 16px
       transform: translateY(-50%)
       right: 10px
       font-size: 24px
+
+.AllBoardTeamsOrgs
+  list-style-type: none;
+  overflow: hidden;
+
+.AllBoardTeams,.AllBoardOrgs,.AllBoardBtns
+  float: left;
+
+.js-AllBoardOrgs
+  margin-left: 16px;
+
+.AllBoardTeams
+  margin-left : 16px;
+
+.AllBoardButtonsContainer
+  margin: 16px;
+
+#filterBtn,#resetBtn
+  display: inline;
+
+.js-board
+  display: block;

+ 13 - 0
client/components/cards/attachments.jade

@@ -26,12 +26,25 @@ template(name="attachmentsGalery")
           if isUploaded
             if isImage
               img.attachment-thumbnail-img(src="{{url}}")
+            else if($eq extension 'mp3')
+                video(width="100%" height="100%" controls="true")
+                  source(src="{{url}}" type="audio/mpeg")
+            else if($eq extension 'ogg')
+                video(width="100%" height="100%" controls="true")
+                  source(src="{{url}}" type="video/ogg")
+            else if($eq extension 'webm')
+                video(width="100%" height="100%" controls="true")
+                  source(src="{{url}}" type="video/webm")
+            else if($eq extension 'mp4')
+                video(width="100%" height="100%" controls="true")
+                  source(src="{{url}}" type="video/mp4")
             else
               span.attachment-thumbnail-ext= extension
           else
             +spinner
         p.attachment-details
           = name
+          span.file-size ({{fileSize size}} KB)
           span.attachment-details-actions
             a.js-download(href="{{url download=true}}")
               i.fa.fa-download

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

@@ -4,7 +4,7 @@ Template.attachmentsGalery.events({
     'attachmentDelete',
     function() {
       Attachments.remove(this._id);
-      Popup.close();
+      Popup.back();
     },
   ),
   // If we let this event bubble, FlowRouter will handle it and empty the page
@@ -49,11 +49,14 @@ Template.attachmentsGalery.helpers({
   isBoardAdmin() {
     return Meteor.user().isBoardAdmin();
   },
+  fileSize(size) {
+    return Math.round(size / 1024);
+  },
 });
 
 Template.previewAttachedImagePopup.events({
   'click .js-large-image-clicked'() {
-    Popup.close();
+    Popup.back();
   },
 });
 
@@ -65,7 +68,7 @@ Template.cardAttachmentsPopup.events({
         if (attachment && attachment._id && attachment.isImage()) {
           card.setCover(attachment._id);
         }
-        Popup.close();
+        Popup.back();
       });
     };
 
@@ -174,7 +177,7 @@ Template.previewClipboardImagePopup.events({
 
       pastedResults = null;
       $(document.body).pasteImageReader(() => {});
-      Popup.close();
+      Popup.back();
     }
   },
 });

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

@@ -9,7 +9,7 @@
     margin: 10px 1% 0
     text-align: center
     border-radius: 3px
-    overflow: hidden
+    overflow: auto
     background: darken(white, 7%)
     min-height: 120px
 

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

@@ -63,7 +63,7 @@ template(name="cardCustomField-checkbox")
 template(name="cardCustomField-currency")
     if canModifyCard
         +inlinedForm(classNames="js-card-customfield-currency")
-            input(type="text" value=data.value)
+            input(type="text" value=data.value autofocus)
             .edit-controls.clearfix
                 button.primary(type="submit") {{_ 'save'}}
                 a.fa.fa-times-thin.js-close-inlined-form
@@ -79,18 +79,22 @@ template(name="cardCustomField-currency")
 
 template(name="cardCustomField-date")
     if canModifyCard
-        a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
-            if value
-                div.card-date
-                    time(datetime="{{showISODate}}")
-                        | {{showDate}}
-            else
-                | {{_ 'edit'}}
-    else
+      a.js-edit-date(title="{{showTitle}} {{_ 'predicate-week'}} {{showWeek}}" class="{{classes}}")
         if value
-            div.card-date
-                time(datetime="{{showISODate}}")
-                    | {{showDate}}
+          div.card-date
+            time(datetime="{{showISODate}}")
+              | {{showDate}}
+              b
+                | {{showWeek}}
+        else
+          | {{_ 'edit'}}
+    else
+      if value
+        div.card-date
+          time(datetime="{{showISODate}}")
+            | {{showDate}}
+            b
+              | {{showWeek}}
 
 template(name="cardCustomField-dropdown")
     if canModifyCard

+ 9 - 5
client/components/cards/cardCustomFields.js

@@ -3,7 +3,7 @@ import Cards from '/models/cards';
 
 Template.cardCustomFieldsPopup.helpers({
   hasCustomField() {
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Utils.getCurrentCard();
     const customFieldId = this._id;
     return card.customFieldIndex(customFieldId) > -1;
   },
@@ -11,7 +11,7 @@ Template.cardCustomFieldsPopup.helpers({
 
 Template.cardCustomFieldsPopup.events({
   'click .js-select-field'(event) {
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Utils.getCurrentCard();
     const customFieldId = this._id;
     card.toggleCustomField(customFieldId);
     event.preventDefault();
@@ -31,7 +31,7 @@ const CardCustomField = BlazeComponent.extendComponent({
 
   onCreated() {
     const self = this;
-    self.card = Cards.findOne(Session.get('currentCard'));
+    self.card = Utils.getCurrentCard();
     self.customFieldId = this.data()._id;
   },
 
@@ -149,6 +149,10 @@ CardCustomField.register('cardCustomField');
     });
   }
 
+  showWeek() {
+    return this.date.get().week().toString();
+  }
+
   showDate() {
     // this will start working once mquandalle:moment
     // is updated to at least moment.js 2.10.5
@@ -190,7 +194,7 @@ CardCustomField.register('cardCustomField');
   onCreated() {
     super.onCreated();
     const self = this;
-    self.card = Cards.findOne(Session.get('currentCard'));
+    self.card = Utils.getCurrentCard();
     self.customFieldId = this.data()._id;
     this.data().value && this.date.set(moment(this.data().value));
   }
@@ -267,7 +271,7 @@ CardCustomField.register('cardCustomField');
       {
         'submit .js-card-customfield-stringtemplate'(event) {
           event.preventDefault();
-          const items = this.getItems();
+          const items = this.stringtemplateItems.get();
           this.card.setCustomField(this.customFieldId, items);
         },
 

+ 9 - 3
client/components/cards/cardDate.jade

@@ -1,14 +1,20 @@
 template(name="dateBadge")
   if canModifyCard
-    a.js-edit-date.card-date(title="{{showTitle}}" class="{{classes}}")
+    a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{showWeek}}" class="{{classes}}")
       time(datetime="{{showISODate}}")
         | {{showDate}}
+        b
+          | {{showWeek}}
   else
-    a.card-date(title="{{showTitle}}" class="{{classes}}")
+    a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{showWeek}}" class="{{classes}}")
       time(datetime="{{showISODate}}")
         | {{showDate}}
+        b
+          | {{showWeek}}
 
 template(name="dateCustomField")
-  a(title="{{showTitle}}" class="{{classes}}")
+  a(title="{{showTitle}} {{_ 'predicate-week'}} {{showWeek}}" class="{{classes}}")
     time(datetime="{{showISODate}}")
       | {{showDate}}
+      b
+        | {{showWeek}}

+ 24 - 7
client/components/cards/cardDate.js

@@ -24,7 +24,7 @@ Template.dateBadge.helpers({
   }
 
   _deleteDate() {
-    this.card.setReceived(null);
+    this.card.unsetReceived();
   }
 }.register('editCardReceivedDatePopup'));
 
@@ -50,7 +50,7 @@ Template.dateBadge.helpers({
   }
 
   _deleteDate() {
-    this.card.setStart(null);
+    this.card.unsetStart();
   }
 }.register('editCardStartDatePopup'));
 
@@ -73,7 +73,7 @@ Template.dateBadge.helpers({
   }
 
   _deleteDate() {
-    this.card.setDue(null);
+    this.card.unsetDue();
   }
 }.register('editCardDueDatePopup'));
 
@@ -96,7 +96,7 @@ Template.dateBadge.helpers({
   }
 
   _deleteDate() {
-    this.card.setEnd(null);
+    this.card.unsetEnd();
   }
 }.register('editCardEndDatePopup'));
 
@@ -115,6 +115,10 @@ const CardDate = BlazeComponent.extendComponent({
     }, 60000);
   },
 
+  showWeek() {
+    return this.date.get().week().toString();
+  },
+
   showDate() {
     // this will start working once mquandalle:moment
     // is updated to at least moment.js 2.10.5
@@ -284,12 +288,25 @@ class CardCustomFieldDate extends CardDate {
     });
   }
 
-  classes() {
-    return 'customfield-date';
+  showWeek() {
+    return this.date.get().week().toString();
+  }
+
+  showDate() {
+    // this will start working once mquandalle:moment
+    // is updated to at least moment.js 2.10.5
+    // until then, the date is displayed in the "L" format
+    return this.date.get().calendar(null, {
+      sameElse: 'llll',
+    });
   }
 
   showTitle() {
-    return '';
+    return `${this.date.get().format('LLLL')}`;
+  }
+
+  classes() {
+    return 'customfield-date';
   }
 
   events() {

+ 4 - 1
client/components/cards/cardDescription.js

@@ -25,7 +25,10 @@ BlazeComponent.extendComponent({
         // Pressing Ctrl+Enter should submit the form
         'keydown form textarea'(evt) {
           if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
-            this.find('button[type=submit]').click();
+            const submitButton = this.find('button[type=submit]');
+            if (submitButton) {
+              submitButton.click();
+            }
           }
         },
       },

+ 66 - 26
client/components/cards/cardDetails.jade

@@ -1,27 +1,40 @@
+template(name="cardDetailsPopup")
+  +cardDetails(popupCard)
+
 template(name="cardDetails")
-  section.card-details.js-card-details(class='{{#if cardMaximized}}card-details-maximized{{/if}}'): .card-details-canvas
+  section.card-details.js-card-details(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}'): .card-details-canvas
     .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
       +inlinedForm(classNames="js-card-details-title")
         +editCardTitleForm
       else
         unless isMiniScreen
-          a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
-          unless cardMaximized
-            a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
-          if cardMaximized
-            a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
+          unless isPopup
+            a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
+            if cardMaximized
+              a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
+            else
+              a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
           if currentUser.isBoardMember
             a.fa.fa-navicon.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
-            input.inline-input(type="text" id="cardURL_copy" value="{{ originRelativeUrl }}")
             a.fa.fa-link.card-copy-button.js-copy-link(
+              id="cardURL_copy"
               class="fa-link"
               title="{{_ 'copy-card-link-to-clipboard'}}"
+              href="{{ originRelativeUrl }}"
             )
-        if isMiniScreen
-          a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details(title="{{_ 'close-card'}}")
+            span.copied-tooltip {{_ 'copied'}}
+        else
+          unless isPopup
+            a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
           if currentUser.isBoardMember
             a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
-            a.fa.fa-link.card-copy-mobile-button
+            a.fa.fa-link.card-copy-mobile-button.js-copy-link(
+              id="cardURL_copy"
+              class="fa-link"
+              title="{{_ 'copy-card-link-to-clipboard'}}"
+              href="{{ originRelativeUrl }}"
+            )
+            span.copied-tooltip {{_ 'copied'}}
         h2.card-details-title.js-card-title(
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
             +viewer
@@ -66,8 +79,10 @@ template(name="cardDetails")
                 a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
                   i.fa.fa-plus
 
-        if currentBoard.allowsReceivedDate
+        if currentBoard.hasAnyAllowsDate
           hr
+
+        if currentBoard.allowsReceivedDate
           .card-details-item.card-details-item-received
             h3.card-details-item-title
               i.fa.fa-sign-out
@@ -119,7 +134,9 @@ template(name="cardDetails")
                   a.card-label.add-label.js-end-date
                     i.fa.fa-plus
 
-        hr
+        if currentBoard.hasAnyAllowsUser
+          hr
+
         if currentBoard.allowsCreator
           .card-details-item.card-details-item-creator
             h3.card-details-item-title
@@ -160,17 +177,6 @@ template(name="cardDetails")
                 a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
                   i.fa.fa-plus
 
-        //.card-details-items
-        if getSpentTime
-          .card-details-item.card-details-item-spent
-            if getIsOvertime
-              h3.card-details-item-title
-                | {{_ 'overtime-hours'}}
-            else
-              h3.card-details-item-title
-                | {{_ 'spent-time-hours'}}
-            +cardSpentTime
-
         //.card-details-items
         if currentBoard.allowsRequestedBy
           .card-details-item.card-details-item-name
@@ -212,6 +218,9 @@ template(name="cardDetails")
               +viewer
                 = getAssignedBy
 
+        if $or currentBoard.allowsCardSortingByNumber getSpentTime
+          hr
+
         if currentBoard.allowsCardSortingByNumber
           .card-details-item.card-details-sort-order
             h3.card-details-item-title
@@ -225,15 +234,36 @@ template(name="cardDetails")
                   +viewer
                     = sort
 
+        //.card-details-items
+        if getSpentTime
+          .card-details-item.card-details-item-spent
+            if getIsOvertime
+              h3.card-details-item-title
+                | {{_ 'overtime-hours'}}
+            else
+              h3.card-details-item-title
+                | {{_ 'spent-time-hours'}}
+            +cardSpentTime
+
         //.card-details-items
         if customFieldsWD
-          hr
+          unless customFieldsGrid
+            hr
           each customFieldsWD
+            if customFieldsGrid
+              hr
             .card-details-item.card-details-item-customfield
               h3.card-details-item-title
                 i.fa.fa-list-alt
                 = definition.name
               +cardCustomField
+          .material-toggle-switch(title="{{_ 'change'}} {{_ 'custom-fields'}} {{_ 'layout'}}")
+            if customFieldsGrid
+              input.toggle-switch(type="checkbox" id="toggleCustomFieldsGridButton" checked="checked")
+            else
+              input.toggle-switch(type="checkbox" id="toggleCustomFieldsGridButton")
+            label.toggle-label(for="toggleCustomFieldsGridButton")
+          a.fa.fa-plus.js-custom-fields.card-details-item.custom-fields(title="{{_ 'custom-fields'}}")
 
       if getVoteQuestion
         hr
@@ -519,6 +549,7 @@ template(name="cardDetails")
     .card-details-right
 
       unless currentUser.isNoComments
+        hr
         .activity-title
           h3.card-details-item-title
             i.fa.fa-history
@@ -708,8 +739,9 @@ template(name="boardsAndLists")
     button.primary.confirm.js-done {{_ 'done'}}
 
 template(name="cardMembersPopup")
+  input.card-members-filter(type="text" placeholder="{{_ 'search'}}")
   ul.pop-over-list.js-card-member-list
-    each board.activeMembers
+    each members
       li.item(class="{{#if isCardMember}}active{{/if}}")
         a.name.js-select-member(href="#")
           +userAvatar(userId=user._id)
@@ -720,9 +752,10 @@ template(name="cardMembersPopup")
             i.fa.fa-check
 
 template(name="cardAssigneesPopup")
+  input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
   unless currentUser.isWorker
     ul.pop-over-list.js-card-assignee-list
-      each board.activeMembers
+      each members
         li.item(class="{{#if isCardAssignee}}active{{/if}}")
           a.name.js-select-assignee(href="#")
             +userAvatar(userId=user._id)
@@ -767,6 +800,7 @@ template(name="cardMorePopup")
       i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
       input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
       button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
+      .copied-tooltip {{_ 'copied'}}
     span.clearfix
     br
     h2 {{_ 'change-card-parent'}}
@@ -815,6 +849,12 @@ template(name="cardDeletePopup")
     p {{_ "card-delete-suggest-archive"}}
   button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
 
+template(name="cardArchivePopup")
+  p {{_ "card-archive-pop"}}
+  unless archived
+    p {{_ "card-archive-suggest-cancel"}}
+  button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
+
 template(name="deleteVotePopup")
   p {{_ "vote-delete-pop"}}
   button.js-confirm.negate.full(type="submit") {{_ 'delete'}}

+ 210 - 141
client/components/cards/cardDetails.js

@@ -34,15 +34,25 @@ BlazeComponent.extendComponent({
   onCreated() {
     this.currentBoard = Boards.findOne(Session.get('currentBoard'));
     this.isLoaded = new ReactiveVar(false);
-    const boardBody = this.parentComponent().parentComponent();
-    //in Miniview parent is Board, not BoardBody.
-    if (boardBody !== null) {
-      boardBody.showOverlay.set(true);
-      boardBody.mouseHasEnterCardDetails = false;
+
+    if (this.parentComponent() && this.parentComponent().parentComponent()) {
+      const boardBody = this.parentComponent().parentComponent();
+      //in Miniview parent is Board, not BoardBody.
+      if (boardBody !== null) {
+        boardBody.showOverlay.set(true);
+        boardBody.mouseHasEnterCardDetails = false;
+      }
     }
     this.calculateNextPeak();
 
     Meteor.subscribe('unsaved-edits');
+
+    // this.findUsersOptions = new ReactiveVar({});
+    // this.page = new ReactiveVar(1);
+    // this.autorun(() => {
+    //   const limitUsers = this.page.get() * Number.MAX_SAFE_INTEGER;
+    //   this.subscribe('people', this.findUsersOptions.get(), limitUsers, () => {});
+    // });
   },
 
   isWatching() {
@@ -54,6 +64,11 @@ BlazeComponent.extendComponent({
     return Meteor.user().hasHiddenSystemMessages();
   },
 
+  customFieldsGrid() {
+    return Meteor.user().hasCustomFieldsGrid();
+  },
+
+
   cardMaximized() {
     return Meteor.user().hasCardMaximized();
   },
@@ -180,7 +195,7 @@ BlazeComponent.extendComponent({
             integration,
             'CardSelected',
             params,
-            () => {},
+            () => { },
           );
         });
       }
@@ -203,7 +218,7 @@ BlazeComponent.extendComponent({
       distance: 7,
       start(evt, ui) {
         ui.placeholder.height(ui.helper.height());
-        EscapeActions.executeUpTo('popup-close');
+        EscapeActions.clickExecute(evt.target, 'inlinedForm');
       },
       stop(evt, ui) {
         let prevChecklist = ui.item.prev('.js-checklist').get(0);
@@ -285,6 +300,7 @@ BlazeComponent.extendComponent({
   },
 
   onDestroyed() {
+    if (this.parentComponent() === null) return;
     const parentComponent = this.parentComponent().parentComponent();
     //on mobile view parent is Board, not board body.
     if (parentComponent === null) return;
@@ -307,30 +323,12 @@ BlazeComponent.extendComponent({
         'click .js-close-card-details'() {
           Utils.goBoardId(this.data().boardId);
         },
-        'click .js-copy-link'() {
-          const StringToCopyElement = document.getElementById('cardURL_copy');
-          StringToCopyElement.value =
-            window.location.origin + window.location.pathname;
-          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-copy-link'(event) {
+          event.preventDefault();
+          const promise = Utils.copyTextToClipboard(event.target.href);
+
+          const $tooltip = this.$('.card-details-header .copied-tooltip');
+          Utils.showCopied(promise, $tooltip);
         },
         'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
         'submit .js-card-description'(event) {
@@ -365,6 +363,12 @@ BlazeComponent.extendComponent({
             this.data().setRequestedBy('');
           }
         },
+        'keydown input.js-edit-card-sort'(evt) {
+          // enter = save
+          if (evt.keyCode === 13) {
+            this.find('button[type=submit]').click();
+          }
+        },
         'submit .js-card-details-sort'(event) {
           event.preventDefault();
           const sort = parseFloat(this.currentComponent()
@@ -389,7 +393,9 @@ BlazeComponent.extendComponent({
         'click .js-end-date': Popup.open('editCardEndDate'),
         'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
         'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
+        'click .js-custom-fields': Popup.open('cardCustomFields'),
         'mouseenter .js-card-details'() {
+          if (this.parentComponent() === null) return;
           const parentComponent = this.parentComponent().parentComponent();
           //on mobile view parent is Board, not BoardBody.
           if (parentComponent === null) return;
@@ -412,6 +418,9 @@ BlazeComponent.extendComponent({
         'click #toggleButton'() {
           Meteor.call('toggleSystemMessages');
         },
+        'click #toggleCustomFieldsGridButton'() {
+          Meteor.call('toggleCustomFieldsGrid');
+        },
         'click .js-maximize-card-details'() {
           Meteor.call('toggleCardMaximized');
           autosize($('.card-details'));
@@ -511,6 +520,23 @@ BlazeComponent.extendComponent({
   },
 }).register('cardDetails');
 
+Template.cardDetails.helpers({
+  isPopup() {
+    let ret = !!Utils.getPopupCardId();
+    return ret;
+  }
+});
+Template.cardDetailsPopup.onDestroyed(() => {
+  Session.delete('popupCardId');
+  Session.delete('popupCardBoardId');
+});
+Template.cardDetailsPopup.helpers({
+  popupCard() {
+    const ret = Utils.getPopupCard();
+    return ret;
+  },
+});
+
 BlazeComponent.extendComponent({
   template() {
     return 'exportCard';
@@ -541,8 +567,8 @@ BlazeComponent.extendComponent({
 }).register('exportCardPopup');
 
 // only allow number input
-Template.editCardSortOrderForm.onRendered(function() {
-  this.$('input').on("keypress paste", function(event) {
+Template.editCardSortOrderForm.onRendered(function () {
+  this.$('input').on("keypress paste", function (event) {
     let keyCode = event.keyCode;
     let charCode = String.fromCharCode(keyCode);
     let regex = new RegExp('[-0-9.]');
@@ -561,16 +587,15 @@ Template.editCardSortOrderForm.onRendered(function() {
       // XXX Recovering the currentCard identifier form a session variable is
       // fragile because this variable may change for instance if the route
       // change. We should use some component props instead.
-      docId: Session.get('currentCard'),
+      docId: Utils.getCurrentCardId(),
     };
   }
 
   close(isReset = false) {
     if (this.isOpen.get() && !isReset) {
       const draft = this.getValue().trim();
-      if (
-        draft !== Cards.findOne(Session.get('currentCard')).getDescription()
-      ) {
+      let card = Utils.getCurrentCard();
+      if (card && draft !== card.getDescription()) {
         UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
       }
     }
@@ -615,7 +640,6 @@ Template.cardDetailsActionsPopup.events({
   'click .js-export-card': Popup.open('exportCard'),
   'click .js-members': Popup.open('cardMembers'),
   'click .js-assignees': Popup.open('cardAssignees'),
-  'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
   'click .js-start-voting': Popup.open('cardStartVoting'),
   'click .js-start-planning-poker': Popup.open('cardStartPlanningPoker'),
@@ -634,25 +658,27 @@ Template.cardDetailsActionsPopup.events({
     event.preventDefault();
     const minOrder = _.min(
       this.list()
-        .cards(this.swimlaneId)
+        .cardsUnfiltered(this.swimlaneId)
         .map((c) => c.sort),
     );
     this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
+    Popup.back();
   },
   'click .js-move-card-to-bottom'(event) {
     event.preventDefault();
     const maxOrder = _.max(
       this.list()
-        .cards(this.swimlaneId)
+        .cardsUnfiltered(this.swimlaneId)
         .map((c) => c.sort),
     );
     this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
+    Popup.back();
   },
-  'click .js-archive'(event) {
-    event.preventDefault();
-    this.archive();
+  'click .js-archive': Popup.afterConfirm('cardArchive', function () {
     Popup.close();
-  },
+    this.archive();
+    Utils.goBoardId(this.boardId);
+  }),
   'click .js-more': Popup.open('cardMore'),
   'click .js-toggle-watch-card'() {
     const currentCard = this;
@@ -667,6 +693,64 @@ Template.editCardTitleForm.onRendered(function () {
   autosize(this.$('.js-edit-card-title'));
 });
 
+Template.cardMembersPopup.onCreated(function () {
+  let currBoard = Boards.findOne(Session.get('currentBoard'));
+  let members = currBoard.activeMembers();
+
+  // let query = {
+  //   "teams.teamId": { $in: currBoard.teams.map(t => t.teamId) },
+  // };
+
+  // let boardTeamUsers = Users.find(query, {
+  //   sort: { sort: 1 },
+  // });
+
+  // members = currBoard.activeMembers2(members, boardTeamUsers);
+
+  this.members = new ReactiveVar(members);
+});
+
+Template.cardMembersPopup.events({
+  'keyup .card-members-filter'(event) {
+    const members = filterMembers(event.target.value);
+    Template.instance().members.set(members);
+  }
+});
+
+Template.cardMembersPopup.helpers({
+  members() {
+    return Template.instance().members.get();
+  },
+});
+
+const filterMembers = (filterTerm) => {
+  let currBoard = Boards.findOne(Session.get('currentBoard'));
+  let members = currBoard.activeMembers();
+
+  // let query = {
+  //   "teams.teamId": { $in: currBoard.teams.map(t => t.teamId) },
+  // };
+
+  // let boardTeamUsers = Users.find(query, {
+  //   sort: { sort: 1 },
+  // });
+
+  // members = currBoard.activeMembers2(members, boardTeamUsers);
+
+  if (filterTerm) {
+    members = members
+      .map(member => ({
+        member,
+        user: Users.findOne(member.userId)
+      }))
+      .filter(({ user }) =>
+      (user.profile.fullname !== undefined && user.profile.fullname.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
+      || user.profile.fullname === undefined && user.profile.username !== undefined && user.profile.username.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
+      .map(({ member }) => member);
+  }
+  return members;
+}
+
 Template.editCardTitleForm.events({
   'keydown .js-edit-card-title'(event) {
     // If enter key was pressed, submit the data
@@ -707,7 +791,7 @@ Template.moveCardPopup.events({
   'click .js-done'() {
     // XXX We should *not* get the currentCard from the global state, but
     // instead from a “component” state.
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Utils.getCurrentCard();
     const bSelect = $('.js-select-boards')[0];
     let boardId;
     // if we are a worker, we won't have a board select so we just use the
@@ -719,7 +803,13 @@ Template.moveCardPopup.events({
     const slSelect = $('.js-select-swimlanes')[0];
     const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
     card.move(boardId, swimlaneId, listId, 0);
-    Popup.close();
+
+    // set new id's to card object in case the card is moved to top by the comment "moveCard" after this command (.js-move-card)
+    this.boardId = boardId;
+    this.swimlaneId = swimlaneId;
+    this.listId = listId;
+
+    Popup.back();
   },
 });
 BlazeComponent.extendComponent({
@@ -765,7 +855,7 @@ BlazeComponent.extendComponent({
 
 Template.copyCardPopup.events({
   'click .js-done'() {
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Utils.getCurrentCard();
     const lSelect = $('.js-select-lists')[0];
     const listId = lSelect.options[lSelect.selectedIndex].value;
     const slSelect = $('.js-select-swimlanes')[0];
@@ -787,14 +877,14 @@ Template.copyCardPopup.events({
       // See https://github.com/wekan/wekan/issues/80
       Filter.addException(_id);
 
-      Popup.close();
+      Popup.back();
     }
   },
 });
 
 Template.convertChecklistItemToCardPopup.events({
   'click .js-done'() {
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Utils.getCurrentCard();
     const lSelect = $('.js-select-lists')[0];
     const listId = lSelect.options[lSelect.selectedIndex].value;
     const slSelect = $('.js-select-swimlanes')[0];
@@ -814,7 +904,7 @@ Template.convertChecklistItemToCardPopup.events({
       });
       Filter.addException(_id);
 
-      Popup.close();
+      Popup.back();
 
     }
   },
@@ -822,7 +912,7 @@ Template.convertChecklistItemToCardPopup.events({
 
 Template.copyChecklistToManyCardsPopup.events({
   'click .js-done'() {
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Utils.getCurrentCard();
     const oldId = card._id;
     card._id = null;
     const lSelect = $('.js-select-lists')[0];
@@ -870,7 +960,7 @@ Template.copyChecklistToManyCardsPopup.events({
           cmt.copy(_id);
         });
       }
-      Popup.close();
+      Popup.back();
     }
   },
 });
@@ -941,7 +1031,7 @@ BlazeComponent.extendComponent({
   },
 
   cards() {
-    const currentId = Session.get('currentCard');
+    const currentId = Utils.getCurrentCardId();
     if (this.parentBoard.get()) {
       return Cards.find({
         boardId: this.parentBoard.get(),
@@ -980,30 +1070,11 @@ BlazeComponent.extendComponent({
   events() {
     return [
       {
-        'click .js-copy-card-link-to-clipboard'() {
-          // Clipboard code from:
-          // https://stackoverflow.com/questions/6300213/copy-selected-text-to-the-clipboard-without-using-flash-must-be-cross-browser
-          const StringToCopyElement = document.getElementById('cardURL');
-          StringToCopyElement.select();
-          if (document.execCommand('copy')) {
-            StringToCopyElement.blur();
-          } else {
-            document.getElementById('cardURL').selectionStart = 0;
-            document.getElementById('cardURL').selectionEnd = 999;
-            document.execCommand('copy');
-            if (window.getSelection) {
-              if (window.getSelection().empty) {
-                // Chrome
-                window.getSelection().empty();
-              } else if (window.getSelection().removeAllRanges) {
-                // Firefox
-                window.getSelection().removeAllRanges();
-              }
-            } else if (document.selection) {
-              // IE?
-              document.selection.empty();
-            }
-          }
+        'click .js-copy-card-link-to-clipboard'(event) {
+          const promise = Utils.copyTextToClipboard(location.origin + document.getElementById('cardURL').value);
+
+          const $tooltip = this.$('.copied-tooltip');
+          Utils.showCopied(promise, $tooltip);
         },
         'click .js-delete': Popup.afterConfirm('cardDelete', function () {
           Popup.close();
@@ -1019,9 +1090,8 @@ BlazeComponent.extendComponent({
             //   https://github.com/wekan/wekan/issues/2785
             const message = `${TAPi18n.__(
               'delete-linked-card-before-this-card',
-            )} linkedId: ${
-              this._id
-            } at client/components/cards/cardDetails.js and https://github.com/wekan/wekan/issues/2785`;
+            )} linkedId: ${this._id
+              } at client/components/cards/cardDetails.js and https://github.com/wekan/wekan/issues/2785`;
             alert(message);
           }
           Utils.goBoardId(this.boardId);
@@ -1074,12 +1144,12 @@ BlazeComponent.extendComponent({
           if (endString) {
             this.currentCard.setVoteEnd(endString);
           }
-          Popup.close();
+          Popup.back();
         },
         'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
           event.preventDefault();
           this.currentCard.unsetVote();
-          Popup.close();
+          Popup.back();
         }),
         'click a.js-toggle-vote-public'(event) {
           event.preventDefault();
@@ -1119,7 +1189,7 @@ BlazeComponent.extendComponent({
             // if active vote -  store it
             if (this.currentData().getVoteQuestion()) {
               this._storeDate(newDate.toDate());
-              Popup.close();
+              Popup.back();
             } else {
               this.currentData().vote = { end: newDate.toDate() }; // set vote end temp
               Popup.back();
@@ -1153,86 +1223,77 @@ BlazeComponent.extendComponent({
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(usaDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: usaDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (euroAmDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(euroAmDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: euroAmDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (euro24hDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(euro24hDate.toDate());
               this.card.setPokerEnd(euro24hDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: euro24hDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (eurodotDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(eurodotDate.toDate());
               this.card.setPokerEnd(eurodotDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: eurodotDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (minusDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(minusDate.toDate());
               this.card.setPokerEnd(minusDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: minusDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (slashDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(slashDate.toDate());
               this.card.setPokerEnd(slashDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: slashDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (dotDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(dotDate.toDate());
               this.card.setPokerEnd(dotDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: dotDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (brezhonegDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(brezhonegDate.toDate());
               this.card.setPokerEnd(brezhonegDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: brezhonegDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (hrvatskiDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(hrvatskiDate.toDate());
               this.card.setPokerEnd(hrvatskiDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: hrvatskiDate.toDate() }; // set poker end temp
               Popup.back();
@@ -1242,41 +1303,37 @@ BlazeComponent.extendComponent({
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(latviaDate.toDate());
               this.card.setPokerEnd(latviaDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: latviaDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (nederlandsDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(nederlandsDate.toDate());
               this.card.setPokerEnd(nederlandsDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: nederlandsDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (greekDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(greekDate.toDate());
               this.card.setPokerEnd(greekDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: greekDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (macedonianDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(macedonianDate.toDate());
               this.card.setPokerEnd(macedonianDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: macedonianDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else {
             this.error.set('invalid-date');
             evt.target.date.focus();
@@ -1285,7 +1342,7 @@ BlazeComponent.extendComponent({
         'click .js-delete-date'(evt) {
           evt.preventDefault();
           this._deleteDate();
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -1323,11 +1380,11 @@ BlazeComponent.extendComponent({
           if (endString) {
             this.currentCard.setPokerEnd(endString);
           }
-          Popup.close();
+          Popup.back();
         },
         'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
           this.currentCard.unsetPoker();
-          Popup.close();
+          Popup.back();
         }),
         'click a.js-toggle-poker-allow-non-members'(event) {
           event.preventDefault();
@@ -1390,7 +1447,7 @@ BlazeComponent.extendComponent({
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(newDate.toDate());
-              Popup.close();
+              Popup.back();
             } else {
               this.currentData().poker = { end: newDate.toDate() }; // set poker end temp
               Popup.back();
@@ -1422,130 +1479,117 @@ BlazeComponent.extendComponent({
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(usaDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: usaDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (euroAmDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(euroAmDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: euroAmDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (euro24hDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(euro24hDate.toDate());
               this.card.setPokerEnd(euro24hDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: euro24hDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (eurodotDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(eurodotDate.toDate());
               this.card.setPokerEnd(eurodotDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: eurodotDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (minusDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(minusDate.toDate());
               this.card.setPokerEnd(minusDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: minusDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (slashDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(slashDate.toDate());
               this.card.setPokerEnd(slashDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: slashDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (dotDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(dotDate.toDate());
               this.card.setPokerEnd(dotDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: dotDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (brezhonegDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(brezhonegDate.toDate());
               this.card.setPokerEnd(brezhonegDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: brezhonegDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (hrvatskiDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(hrvatskiDate.toDate());
               this.card.setPokerEnd(hrvatskiDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: hrvatskiDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (latviaDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(latviaDate.toDate());
               this.card.setPokerEnd(latviaDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: latviaDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (nederlandsDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(nederlandsDate.toDate());
               this.card.setPokerEnd(nederlandsDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: nederlandsDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (greekDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(greekDate.toDate());
               this.card.setPokerEnd(greekDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: greekDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else if (macedonianDate.isValid()) {
             // if active poker -  store it
             if (this.currentData().getPokerQuestion()) {
               this._storeDate(macedonianDate.toDate());
               this.card.setPokerEnd(macedonianDate.toDate());
-              Popup.close();
             } else {
               this.currentData().poker = { end: macedonianDate.toDate() }; // set poker end temp
-              Popup.back();
             }
+            Popup.back();
           } else {
             // this.error.set('invalid-date);
             this.error.set('invalid-date' + ' ' + dateString);
@@ -1555,7 +1599,7 @@ BlazeComponent.extendComponent({
         'click .js-delete-date'(evt) {
           evt.preventDefault();
           this._deleteDate();
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -1589,13 +1633,34 @@ EscapeActions.register(
   },
 );
 
+Template.cardAssigneesPopup.onCreated(function () {
+  let currBoard = Boards.findOne(Session.get('currentBoard'));
+  let members = currBoard.activeMembers();
+
+  // let query = {
+  //   "teams.teamId": { $in: currBoard.teams.map(t => t.teamId) },
+  // };
+
+  // let boardTeamUsers = Users.find(query, {
+  //   sort: { sort: 1 },
+  // });
+
+  // members = currBoard.activeMembers2(members, boardTeamUsers);
+
+  this.members = new ReactiveVar(members);
+});
+
 Template.cardAssigneesPopup.events({
   'click .js-select-assignee'(event) {
-    const card = Cards.findOne(Session.get('currentCard'));
+    const card = Utils.getCurrentCard();
     const assigneeId = this.userId;
     card.toggleAssignee(assigneeId);
     event.preventDefault();
   },
+  'keyup .card-assignees-filter'(event) {
+    const members = filterMembers(event.target.value);
+    Template.instance().members.set(members);
+  },
 });
 
 Template.cardAssigneesPopup.helpers({
@@ -1606,6 +1671,10 @@ Template.cardAssigneesPopup.helpers({
     return _.contains(cardAssignees, this.userId);
   },
 
+  members() {
+    return Template.instance().members.get();
+  },
+
   user() {
     return Users.findOne(this.userId);
   },
@@ -1657,7 +1726,7 @@ Template.cardAssigneePopup.helpers({
 Template.cardAssigneePopup.events({
   'click .js-remove-assignee'() {
     Cards.findOne(this.cardId).unassignAssignee(this.userId);
-    Popup.close();
+    Popup.back();
   },
   'click .js-edit-profile': Popup.open('editProfile'),
 });

+ 68 - 46
client/components/cards/cardDetails.styl

@@ -4,15 +4,6 @@
 
 avatar-radius = 50%
 
-#cardURL_copy
-  // Have clipboard text not visible by moving it to far left
-  position: absolute
-  left: -2000px
-  top: 0px
-
-#clipboard
-  white-space: normal
-
 .assignee
   border-radius: 3px
   display: block
@@ -85,6 +76,12 @@ avatar-radius = 50%
       box-shadow: 0 0 0 2px darken(white, 60%) inset
 
 // Other card details
+.copied-tooltip
+  display: none
+  padding: 0px 10px;
+  background-color: #000000df;
+  color: #fff;
+  border-radius: 5px;
 
 .card-details
   padding: 0
@@ -127,7 +124,8 @@ avatar-radius = 50%
     .card-copy-button,
     .card-copy-mobile-button,
     .close-card-details-mobile-web,
-    .card-details-menu-mobile-web
+    .card-details-menu-mobile-web,
+    .copied-tooltip
       float: right
 
     .close-card-details,
@@ -196,6 +194,14 @@ avatar-radius = 50%
           border-radius: 3px
           padding: 0px 5px
 
+    .copied-tooltip
+      display: none
+      margin-right: 10px
+      padding: 10px;
+      background-color: #000000df;
+      color: #fff;
+      border-radius: 5px;
+
   .card-description textarea
     min-height: 100px
 
@@ -230,55 +236,54 @@ avatar-radius = 50%
         word-wrap: break-word
         max-width: 28%
         flex-grow: 1
+      &.custom-fields
+        padding-left: 10px
+
 
   .card-details-item-title
     font-size: 16px
     font-weight: bold
     color: #4d4d4d
 
-  .card-label
-    padding-top: 5px
-    padding-bottom: 5px
-
   .activities
     padding-top: 10px
 
-.card-details-maximized
-  padding: 0
-  flex-shrink: 0
-  flex-basis: calc(100% - 20px)
-  will-change: flex-basis
-  overflow-y: scroll
-  overflow-x: scroll
-  background: darken(white, 3%)
-  border-radius: bottom 3px
-  z-index: 1000 !important
-  animation: flexGrowIn 0.1s
-  box-shadow: 0 0 7px 0 darken(white, 30%)
-  transition: flex-basis 0.1s
-  box-sizing: border-box
-  position: absolute
-  top: 0
-  left: 0
-  height: calc(100% - 20px)
-  width: calc(100% - 20px)
-  float: left
-
-  .card-details-left
+@media screen and (min-width: 801px)
+  .card-details-maximized
+    padding: 0
+    flex-shrink: 0
+    flex-basis: calc(100% - 20px)
+    will-change: flex-basis
+    overflow-y: scroll
+    overflow-x: scroll
+    background: darken(white, 3%)
+    border-radius: bottom 3px
+    z-index: 1000 !important
+    animation: flexGrowIn 0.1s
+    box-shadow: 0 0 7px 0 darken(white, 30%)
+    transition: flex-basis 0.1s
+    box-sizing: border-box
     position: absolute
+    top: 0
+    left: 0
+    height: calc(100% - 20px)
+    width: calc(100% - 20px)
     float: left
-    top: 60px
-    left: 20px
-    width: 47%
 
-  .card-details-right
-    position: absolute
-    float: right
-    top: 20px
-    left: 50%
+    .card-details-left
+      float: left
+      top: 60px
+      left: 20px
+      width: 47%
 
-  .card-details-header
-    width: 47%
+    .card-details-right
+      position: absolute
+      float: right
+      top: 20px
+      left: 50%
+
+    .card-details-header
+      width: 47%
 
 input[type="text"].attachment-add-link-input
   float: left
@@ -297,6 +302,8 @@ input[type="submit"].attachment-add-link-submit
     padding: 0px 20px 0px 20px
     margin: 0px
     transition: none
+    overflow-y: revert
+    overflow-x: revert
 
     .card-details-canvas
       width: 100%
@@ -315,6 +322,21 @@ input[type="submit"].attachment-add-link-submit
       .minimize-card-details
         margin-right: 40px
 
+  .card-details-popup
+    padding: 0px 10px
+
+  .pop-over > .content-wrapper > .popup-container-depth-0
+    width: 100%
+
+    & > .content
+      width: calc(100% - 10px)
+
+    & > .content > .card-details-popup hr
+        margin: 15px 0px
+
+      .card-details-header
+        margin: 0
+
 card-details-color(background, color...)
   background: background !important
   if color

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

@@ -9,7 +9,6 @@ BlazeComponent.extendComponent({
   toggleOvertime() {
     this.card.setIsOvertime(!this.card.getIsOvertime());
     $('#overtime .materialCheckBox').toggleClass('is-checked');
-
     $('#overtime').toggleClass('is-checked');
   },
   storeTime(spentTime, isOvertime) {
@@ -18,6 +17,7 @@ BlazeComponent.extendComponent({
   },
   deleteTime() {
     this.card.setSpentTime(null);
+    this.card.setIsOvertime(false);
   },
   events() {
     return [
@@ -27,11 +27,14 @@ BlazeComponent.extendComponent({
           evt.preventDefault();
 
           const spentTime = parseFloat(evt.target.time.value);
-          const isOvertime = this.card.getIsOvertime();
-
+          //const isOvertime = this.card.getIsOvertime();
+          let isOvertime = false;
+          if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
+            isOvertime = true;
+          }
           if (spentTime >= 0) {
             this.storeTime(spentTime, isOvertime);
-            Popup.close();
+            Popup.back();
           } else {
             this.error.set('invalid-time');
             evt.target.time.focus();
@@ -40,7 +43,7 @@ BlazeComponent.extendComponent({
         'click .js-delete-time'(evt) {
           evt.preventDefault();
           this.deleteTime();
-          Popup.close();
+          Popup.back();
         },
         'click a.js-toggle-overtime': this.toggleOvertime,
       },

+ 9 - 18
client/components/cards/checklists.jade

@@ -12,20 +12,15 @@ template(name="checklists")
           input.toggle-switch(type="checkbox" id="toggleHideCheckedItemsButton")
         label.toggle-label(for="toggleHideCheckedItemsButton")
 
-  if toggleDeleteDialog.get
-    .board-overlay#card-details-overlay
-    +checklistDeleteDialog(checklist = checklistToDelete)
-
-
   .card-checklist-items
-    each checklist in currentCard.checklists
+    each checklist in checklists
       +checklistDetail(checklist = checklist)
 
   if canModifyCard
     +inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
       +addChecklistItemForm
     else
-      a.js-open-inlined-form(title="{{_ 'add-checklist'}}")
+      a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}")
         i.fa.fa-plus
 
 template(name="checklistDetail")
@@ -50,25 +45,21 @@ template(name="checklistDetail")
                 = checklist.title
     +checklistItems(checklist = checklist)
 
-template(name="checklistDeleteDialog")
-  .js-confirm-checklist-delete
-    p
-      i(class="fa fa-exclamation-triangle" aria-hidden="true")
-    p
-      | {{_ 'confirm-checklist-delete-dialog'}}
-      span {{checklist.title}}
-      | ?
-    .js-checklist-delete-buttons
-      button.confirm-checklist-delete(type="button") {{_ 'delete'}}
-      button.toggle-delete-checklist-dialog(type="button") {{_ 'cancel'}}
+template(name="checklistDeletePopup")
+  p {{_ 'confirm-checklist-delete-popup'}}
+  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
 
 template(name="addChecklistItemForm")
+  a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
+  span.copied-tooltip {{_ 'copied'}}
   textarea.js-add-checklist-item(rows='1' autofocus)
   .edit-controls.clearfix
     button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
     a.fa.fa-times-thin.js-close-inlined-form
 
 template(name="editChecklistItemForm")
+  a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
+  span.copied-tooltip {{_ 'copied'}}
   textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
     if $eq type 'item'
       = item.title

+ 73 - 56
client/components/cards/checklists.js

@@ -13,10 +13,10 @@ function initSorting(items) {
     appendTo: 'parent',
     distance: 7,
     placeholder: 'checklist-item placeholder',
-    scroll: false,
+    scroll: true,
     start(evt, ui) {
       ui.placeholder.height(ui.helper.height());
-      EscapeActions.executeUpTo('popup-close');
+      EscapeActions.clickExecute(evt.target, 'inlinedForm');
     },
     stop(evt, ui) {
       const parent = ui.item.parents('.js-checklist-items');
@@ -55,7 +55,7 @@ BlazeComponent.extendComponent({
       return Meteor.user() && Meteor.user().isBoardMember();
     }
 
-    // Disable sorting if the current user is not a board member or is a miniscreen
+    // Disable sorting if the current user is not a board member
     self.autorun(() => {
       const $itemsDom = $(self.itemsDom);
       if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
@@ -94,16 +94,14 @@ BlazeComponent.extendComponent({
         title,
         sort: card.checklists().count(),
       });
+      this.closeAllInlinedForms();
       setTimeout(() => {
         this.$('.add-checklist-item')
           .last()
           .click();
       }, 100);
     }
-    textarea.value = '';
-    textarea.focus();
   },
-
   addChecklistItem(event) {
     event.preventDefault();
     const textarea = this.find('textarea.js-add-checklist-item');
@@ -132,14 +130,6 @@ BlazeComponent.extendComponent({
     );
   },
 
-  deleteChecklist() {
-    const checklist = this.currentData().checklist;
-    if (checklist && checklist._id) {
-      Checklists.remove(checklist._id);
-      this.toggleDeleteDialog.set(false);
-    }
-  },
-
   deleteItem() {
     const checklist = this.currentData().checklist;
     const item = this.currentData().item;
@@ -165,11 +155,6 @@ BlazeComponent.extendComponent({
     item.setTitle(title);
   },
 
-  onCreated() {
-    this.toggleDeleteDialog = new ReactiveVar(false);
-    this.checklistToDelete = null; //Store data context to pass to checklistDeleteDialog template
-  },
-
   pressKey(event) {
     //If user press enter key inside a form, submit it
     //Unless the user is also holding down the 'shift' key
@@ -190,14 +175,13 @@ BlazeComponent.extendComponent({
     }
   },
 
+  /** closes all inlined forms (checklist and checklist-item input fields) */
+  closeAllInlinedForms() {
+    this.$('.js-close-inlined-form').click();
+  },
+
   events() {
     const events = {
-      'click .toggle-delete-checklist-dialog'(event) {
-        if ($(event.target).hasClass('js-delete-checklist')) {
-          this.checklistToDelete = this.currentData().checklist; //Store data context
-        }
-        this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
-      },
       'click #toggleHideCheckedItemsButton'() {
         Meteor.call('toggleHideCheckedItems');
       },
@@ -206,14 +190,22 @@ BlazeComponent.extendComponent({
     return [
       {
         ...events,
+        'click .toggle-delete-checklist-dialog' : Popup.afterConfirm('checklistDelete', function () {
+          Popup.close();
+          const checklist = this.checklist;
+          if (checklist && checklist._id) {
+            Checklists.remove(checklist._id);
+          }
+        }),
         'submit .js-add-checklist': this.addChecklist,
         'submit .js-edit-checklist-title': this.editChecklist,
         'submit .js-add-checklist-item': this.addChecklistItem,
         'submit .js-edit-checklist-item': this.editChecklistItem,
         'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
         'click .js-delete-checklist-item': this.deleteItem,
-        'click .confirm-checklist-delete': this.deleteChecklist,
         'focus .js-add-checklist-item': this.focusChecklistItem,
+        // add and delete checklist / checklist-item
+        'click .js-open-inlined-form': this.closeAllInlinedForms,
         keydown: this.pressKey,
       },
     ];
@@ -262,6 +254,11 @@ BlazeComponent.extendComponent({
 }).register('boardsSwimlanesAndLists');
 
 Template.checklists.helpers({
+  checklists() {
+    const card = Cards.findOne(this.cardId);
+    const ret = card.checklists();
+    return ret;
+  },
   hideCheckedItems() {
     const currentUser = Meteor.user();
     if (currentUser) return currentUser.hasHideCheckedItems();
@@ -269,39 +266,59 @@ Template.checklists.helpers({
   },
 });
 
-Template.addChecklistItemForm.onRendered(() => {
-  autosize($('textarea.js-add-checklist-item'));
-});
-
-Template.editChecklistItemForm.onRendered(() => {
-  autosize($('textarea.js-edit-checklist-item'));
-});
+BlazeComponent.extendComponent({
+  onRendered() {
+    autosize(this.$('textarea.js-add-checklist-item'));
+  },
+  canModifyCard() {
+    return (
+      Meteor.user() &&
+      Meteor.user().isBoardMember() &&
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
+    );
+  },
+  events() {
+    return [
+      {
+        'click a.fa.fa-copy'(event) {
+          const $editor = this.$('textarea');
+          const promise = Utils.copyTextToClipboard($editor[0].value);
 
-Template.checklistDeleteDialog.onCreated(() => {
-  const $cardDetails = this.$('.card-details');
-  this.scrollState = {
-    position: $cardDetails.scrollTop(), //save current scroll position
-    top: false, //required for smooth scroll animation
-  };
-  //Callback's purpose is to only prevent scrolling after animation is complete
-  $cardDetails.animate({ scrollTop: 0 }, 500, () => {
-    this.scrollState.top = true;
-  });
+          const $tooltip = this.$('.copied-tooltip');
+          Utils.showCopied(promise, $tooltip);
+        },
+      }
+    ];
+  }
+}).register('addChecklistItemForm');
 
-  //Prevent scrolling while dialog is open
-  $cardDetails.on('scroll', () => {
-    if (this.scrollState.top) {
-      //If it's already in position, keep it there. Otherwise let animation scroll
-      $cardDetails.scrollTop(0);
-    }
-  });
-});
+BlazeComponent.extendComponent({
+  onRendered() {
+    autosize(this.$('textarea.js-edit-checklist-item'));
+  },
+  canModifyCard() {
+    return (
+      Meteor.user() &&
+      Meteor.user().isBoardMember() &&
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
+    );
+  },
+  events() {
+    return [
+      {
+        'click a.fa.fa-copy'(event) {
+          const $editor = this.$('textarea');
+          const promise = Utils.copyTextToClipboard($editor[0].value);
 
-Template.checklistDeleteDialog.onDestroyed(() => {
-  const $cardDetails = this.$('.card-details');
-  $cardDetails.off('scroll'); //Reactivate scrolling
-  $cardDetails.animate({ scrollTop: this.scrollState.position });
-});
+          const $tooltip = this.$('.copied-tooltip');
+          Utils.showCopied(promise, $tooltip);
+        },
+      }
+    ];
+  }
+}).register('editChecklistItemForm');
 
 Template.checklistItemDetail.helpers({
   canModifyCard() {

+ 10 - 36
client/components/cards/checklists.styl

@@ -47,41 +47,6 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
     padding-top: 3px
     float: left
 
-
-.js-confirm-checklist-delete
-  background-color: darken(white, 3%)
-  position: absolute
-  float: left;
-  width: 60%
-  margin-top: 0
-  margin-left: 13%
-  padding-bottom: 2%
-  padding-left: 3%
-  padding-right: 3%
-  z-index: 17
-  border-radius: 3px
-
-  p
-    position: relative
-    margin-top: 3%
-    width: 100%
-    text-align: center
-    span
-      font-weight: bold
-
-    i
-      font-size: 2em
-
-  .js-checklist-delete-buttons
-    position: relative
-    padding: left 2% right 2%
-    .confirm-checklist-delete
-      margin-left: 12%
-      float: left
-    .toggle-delete-checklist-dialog
-      margin-right: 12%
-      float: right
-
 #card-details-overlay
   top: 0
   bottom: -600px
@@ -167,4 +132,13 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
 
 .add-checklist-item
   margin: 0.2em 0 0.5em 1.33em
-  display: inline-block
+
+.add-checklist-item,.add-checklist
+  &.js-open-inlined-form
+    display: block
+    width: 50%
+
+    &:hover
+      background: #dbdbdb
+      color: #222
+      box-shadow: 0 1px 2px rgba(0,0,0,.2)

+ 4 - 2
client/components/cards/labels.jade

@@ -27,9 +27,11 @@ template(name="deleteLabelPopup")
 template(name="cardLabelsPopup")
   ul.edit-labels-pop-over
     each board.labels
-      li
+      li.js-card-label-item
         a.card-label-edit-button.fa.fa-pencil.js-edit-label
-        span.card-label.card-label-selectable.js-select-label(class="card-label-{{color}}"
+        if isMiniScreenOrShowDesktopDragHandles
+          span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
+        span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
           class="{{# if isLabelSelected ../_id }}active{{/if}}")
             +viewer
               = name

+ 60 - 8
client/components/cards/labels.js

@@ -39,15 +39,67 @@ Template.createLabelPopup.helpers({
   },
 });
 
-Template.cardLabelsPopup.events({
-  'click .js-select-label'(event) {
-    const card = Cards.findOne(Session.get('currentCard'));
-    const labelId = this._id;
-    card.toggleLabel(labelId);
-    event.preventDefault();
+BlazeComponent.extendComponent({
+  onRendered() {
+    const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
+    const $labels = this.$('.edit-labels-pop-over');
+
+    $labels.sortable({
+      connectWith: '.edit-labels-pop-over',
+      tolerance: 'pointer',
+      appendTo: '.edit-labels-pop-over',
+      helper(element, currentItem) {
+        let ret = currentItem.clone();
+        if (currentItem.closest('.popup-container-depth-0').size() == 0)
+        { // only set css transform at every sub-popup, not at the main popup
+          const content = currentItem.closest('.content')[0]
+          const offsetLeft = content.offsetLeft;
+          const offsetTop = $('.pop-over > .header').height() * -1;
+          ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
+        }
+        return ret;
+      },
+      distance: 7,
+      items: itemsSelector,
+      placeholder: 'card-label-wrapper placeholder',
+      start(evt, ui) {
+        ui.helper.css('z-index', 1000);
+        ui.placeholder.height(ui.helper.height());
+        EscapeActions.clickExecute(evt.target, 'inlinedForm');
+      },
+      stop(evt, ui) {
+        const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
+        const card = Blaze.getData(this);
+        card.board().setNewLabelOrder(newLabelOrderOnlyIds);
+      },
+    });
+
+    // Disable drag-dropping if the current user is not a board member or is comment only
+    this.autorun(() => {
+      if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
+        $labels.sortable({
+          handle: '.label-handle',
+        });
+      }
+    });
   },
-  'click .js-edit-label': Popup.open('editLabel'),
-  'click .js-add-label': Popup.open('createLabel'),
+  events() {
+    return [
+      {
+        'click .js-select-label'(event) {
+          const card = this.data();
+          const labelId = this.currentData()._id;
+          card.toggleLabel(labelId);
+          event.preventDefault();
+        },
+        'click .js-edit-label': Popup.open('editLabel'),
+        'click .js-add-label': Popup.open('createLabel'),
+      }
+    ];
+  }
+}).register('cardLabelsPopup');
+
+Template.cardLabelsPopup.events({
 });
 
 Template.formLabel.events({

+ 15 - 6
client/components/cards/labels.styl

@@ -2,6 +2,7 @@
 
 // XXX Use .board-widget-labels as a flexbox container
 .card-label
+  border: 1px solid #000000
   border-radius: 4px
   color: white  //Default white text, in select cases,  changed to black to improve contrast between label colour and text
   display: inline-block
@@ -12,10 +13,11 @@
   padding: 3px 8px
   max-width: 210px
   min-width: 8px
-  overflow: ellipsis
   word-wrap: break-word
-  height: 18px
-  vertical-align: bottom
+  min-height: 18px
+  vertical-align: middle
+  white-space: initial
+  overflow: initial
 
   &:hover
     color: white
@@ -27,12 +29,13 @@
 
   &.add-label
     box-shadow: 0 0 0 2px darken(white, 25%) inset
+    border: initial
 
     &:hover, &.is-active
       box-shadow: 0 0 0 2px darken(white, 60%) inset
 
-    i.fa-plus
-      margin-top: 3px
+  p
+    margin: 0px
 
 .palette-colors
   display: flex
@@ -47,7 +50,6 @@
 .card-label-white
   background-color: #ffffff
   color: #000000 //Black text for better visibility
-  border: 1px solid #c0c0c0
 
 .card-label-white:hover
   color: #aaaaaa //grey text for better visibility
@@ -144,6 +146,7 @@
     height: 25px
     margin: 0px 3% 7px 0px
     width: 10.5%
+    max-width: 10.5%
     cursor: pointer
 
 .edit-labels
@@ -220,3 +223,9 @@
   &:hover
     background: #dbdbdb
 
+ul.edit-labels-pop-over
+  span.fa.label-handle
+    padding-right: 10px;
+
+  span.fa.label-handle + .card-label
+    max-width: 180px

+ 20 - 17
client/components/cards/minicard.jade

@@ -2,21 +2,17 @@ template(name="minicard")
   .minicard(
     class="{{#if isLinkedCard}}linked-card{{/if}}"
     class="{{#if isLinkedBoard}}linked-board{{/if}}"
-    class="minicard-{{colorClass}}")
-    if isMiniScreen
+    class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
+    if isMiniScreenOrShowDesktopDragHandles
       .handle
         .fa.fa-arrows
-    unless isMiniScreen
-      if showDesktopDragHandles
-        .handle
-          .fa.fa-arrows
     if cover
       .minicard-cover(style="background-image: url('{{cover.url}}');")
     if labels
-      .minicard-labels
+      .minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
         each labels
           unless hiddenMinicardLabelText
-            span.card-label(class="card-label-{{color}}" title=name)
+            span.js-card-label.card-label(class="card-label-{{color}}" title=name)
               +viewer
                 = name
           if hiddenMinicardLabelText
@@ -92,15 +88,17 @@ template(name="minicard")
                   +viewer
                     = trueValue
 
-    if getAssignees
-      .minicard-assignees.js-minicard-assignees
-        each getAssignees
-          +userAvatar(userId=this)
+    if showAssignee
+      if getAssignees
+        .minicard-assignees.js-minicard-assignees
+          each getAssignees
+            +userAvatar(userId=this)
 
-    if getMembers
-      .minicard-members.js-minicard-members
-        each getMembers
-          +userAvatar(userId=this)
+    if showMembers
+      if getMembers
+        .minicard-members.js-minicard-members
+          each getMembers
+            +userAvatar(userId=this)
 
     if showCreator
       .minicard-creator
@@ -145,4 +143,9 @@ template(name="minicard")
       if currentBoard.allowsCardSortingByNumber
         .badge
           span.badge-icon.fa.fa-sort
-          span.badge-text {{ sort }}
+          span.badge-text.check-list-sort {{ sort }}
+
+template(name="editCardSortOrderPopup")
+  input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
+  .edit-controls.clearfix
+    button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}

+ 62 - 13
client/components/cards/minicard.js

@@ -49,6 +49,38 @@ BlazeComponent.extendComponent({
     return false;
   },
 
+  showMembers() {
+    if (this.data().board()) {
+      return (
+        this.data().board.allowsMembers === null ||
+        this.data().board().allowsMembers === undefined ||
+        this.data().board().allowsMembers
+      );
+    }
+    return false;
+  },
+
+  showAssignee() {
+    if (this.data().board()) {
+      return (
+        this.data().board.allowsAssignee === null ||
+        this.data().board().allowsAssignee === undefined ||
+        this.data().board().allowsAssignee
+      );
+    }
+    return false;
+  },
+
+  /** opens the card label popup only if clicked onto a label
+   * <li> this is necessary to have the data context of the minicard.
+   *      if .js-card-label is used at click event, then only the data context of the label itself is available at this.currentData()
+   */
+  cardLabelsPopup(event) {
+    if (this.find('.js-card-label:hover')) {
+      Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
+    }
+  },
+
   events() {
     return [
       {
@@ -57,8 +89,6 @@ BlazeComponent.extendComponent({
           else if (this.data().isLinkedBoard())
             Utils.goBoardId(this.data().linkedId);
         },
-      },
-      {
         'click .js-toggle-minicard-label-text'() {
           if (window.localStorage.getItem('hiddenMinicardLabelText')) {
             window.localStorage.removeItem('hiddenMinicardLabelText'); //true
@@ -66,22 +96,14 @@ BlazeComponent.extendComponent({
             window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
           }
         },
-      },
+        'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
+        'click .minicard-labels' : this.cardLabelsPopup,
+      }
     ];
   },
 }).register('minicard');
 
 Template.minicard.helpers({
-  showDesktopDragHandles() {
-    currentUser = Meteor.user();
-    if (currentUser) {
-      return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-      return true;
-    } else {
-      return false;
-    }
-  },
   hiddenMinicardLabelText() {
     currentUser = Meteor.user();
     if (currentUser) {
@@ -93,3 +115,30 @@ Template.minicard.helpers({
     }
   },
 });
+
+BlazeComponent.extendComponent({
+  events() {
+    return [
+      {
+        'keydown input.js-edit-card-sort-popup'(evt) {
+          // enter = save
+          if (evt.keyCode === 13) {
+            this.find('button[type=submit]').click();
+          }
+        },
+        'click button.js-submit-edit-card-sort-popup'(event) {
+          // save button pressed
+          event.preventDefault();
+          const sort = this.$('.js-edit-card-sort-popup')[0]
+            .value
+            .trim();
+          if (!Number.isNaN(sort)) {
+            let card = this.data();
+            card.move(card.boardId, card.swimlaneId, card.listId, sort);
+            Popup.back();
+          }
+        },
+      }
+    ]
+  }
+}).register('editCardSortOrderPopup');

+ 4 - 2
client/components/cards/minicard.styl

@@ -80,8 +80,6 @@
 
   .minicard-labels
     float: none
-    display: flex
-    flex-wrap: wrap
 
     .minicard-label
       width: 11px
@@ -90,6 +88,10 @@
       margin-right: 3px
       margin-bottom: 3px
 
+  .minicard-labels-no-text
+    display: flex
+    flex-wrap: wrap
+
   .minicard-custom-fields
     display:block;
   .minicard-custom-field

+ 1 - 1
client/components/cards/resultCard.jade

@@ -1,6 +1,6 @@
 template(name="resultCard")
   .result-card-wrapper
-    a.minicard-wrapper.card-title(href=originRelativeUrl)
+    a.minicard-wrapper.js-minicard.card-title(href=originRelativeUrl)
       +minicard(this)
       //= card.title
     ul.result-card-context-list

+ 25 - 1
client/components/cards/resultCard.js

@@ -5,7 +5,31 @@ Template.resultCard.helpers({
 });
 
 BlazeComponent.extendComponent({
+  clickOnMiniCard(evt) {
+    evt.preventDefault();
+    const this_ = this;
+    const cardId = this.currentData()._id;
+    const boardId = this.currentData().boardId;
+    Meteor.subscribe('popupCardData', cardId, {
+      onReady() {
+        Session.set('popupCardId', cardId);
+        Session.set('popupCardBoardId', boardId);
+        this_.cardDetailsPopup(evt);
+      },
+    });
+  },
+
+  cardDetailsPopup(event) {
+    if (!Popup.isOpen()) {
+      Popup.open("cardDetails")(event);
+    }
+  },
+
   events() {
-    return [{}];
+    return [
+      {
+        'click .js-minicard': this.clickOnMiniCard,
+      },
+    ];
   },
 }).register('resultCard');

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

@@ -37,6 +37,8 @@ BlazeComponent.extendComponent({
         ? targetBoard.getDefaultSwimline()._id
         : targetSwimlane._id;
 
+    const nextCardNumber = targetBoard.getNextCardNumber();
+
     if (title) {
       const _id = Cards.insert({
         title,
@@ -49,6 +51,7 @@ BlazeComponent.extendComponent({
         sort: sortIndex,
         swimlaneId,
         type: 'cardType-card',
+        cardNumber: nextCardNumber
       });
 
       // In case the filter is active we need to add the newly inserted card in

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

@@ -247,6 +247,7 @@ textarea
     position: absolute
     left: -9999px
     visibility: hidden
+    display: none
 
 .materialCheckBox
   position: relative

+ 42 - 27
client/components/lists/list.js

@@ -1,3 +1,5 @@
+require('/client/lib/jquery-ui.js')
+
 const { calculateIndex } = Utils;
 
 BlazeComponent.extendComponent({
@@ -93,7 +95,7 @@ BlazeComponent.extendComponent({
         $cards.sortable('cancel');
 
         if (MultiSelection.isActive()) {
-          Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
+          Cards.find(MultiSelection.getMongoSelector(), {sort: ['sort']}).forEach((card, i) => {
             const newSwimlaneId = targetSwimlaneId
               ? targetSwimlaneId
               : card.swimlaneId || defaultSwimlaneId;
@@ -114,25 +116,51 @@ BlazeComponent.extendComponent({
         }
         boardComponent.setIsDragging(false);
       },
+      sort(event, ui) {
+        const $boardCanvas = $('.board-canvas');
+        const  boardCanvas = $boardCanvas[0];
+
+        if (event.pageX < 10)
+        { // scroll to the left
+          boardCanvas.scrollLeft -= 15;
+          ui.helper[0].offsetLeft -= 15;
+        }
+        if (
+          event.pageX > boardCanvas.offsetWidth - 10 &&
+          boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
+        )
+        { // scroll to the right
+          boardCanvas.scrollLeft += 15;
+        }
+        if (
+          event.pageY > boardCanvas.offsetHeight - 10 &&
+          event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
+        )
+        { // scroll to the bottom
+          boardCanvas.scrollTop += 15;
+        }
+        if (event.pageY < 10)
+        { // scroll to the top
+          boardCanvas.scrollTop -= 15;
+        }
+      },
+      activate(event, ui) {
+        const $boardCanvas = $('.board-canvas');
+        const  boardCanvas = $boardCanvas[0];
+        // scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
+        // https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
+        $boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
+        // https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
+        $boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
+      },
     });
 
     this.autorun(() => {
-      let showDesktopDragHandles = false;
-      currentUser = Meteor.user();
-      if (currentUser) {
-        showDesktopDragHandles = (currentUser.profile || {})
-          .showDesktopDragHandles;
-      } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-        showDesktopDragHandles = true;
-      } else {
-        showDesktopDragHandles = false;
-      }
-
-      if (Utils.isMiniScreen() || showDesktopDragHandles) {
+      if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
         $cards.sortable({
           handle: '.handle',
         });
-      } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
+      } else {
         $cards.sortable({
           handle: '.minicard',
         });
@@ -178,19 +206,6 @@ BlazeComponent.extendComponent({
   },
 }).register('list');
 
-Template.list.helpers({
-  showDesktopDragHandles() {
-    currentUser = Meteor.user();
-    if (currentUser) {
-      return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-      return true;
-    } else {
-      return false;
-    }
-  },
-});
-
 Template.miniList.events({
   'click .js-select-list'() {
     const listId = this._id;

+ 29 - 21
client/components/lists/list.styl

@@ -85,13 +85,9 @@
     color: #a6a6a6
 
   .list-header-menu
-    position: absolute
-    padding: 27px 19px
-    margin-top: 1px
-    top: -7px
-    right: 3px
+    float: right
 
-  .list-header-plus-icon
+  .list-header-plus-top
     color: #a6a6a6
     margin-right: 15px
 
@@ -100,9 +96,10 @@
 
   .cardCount
     color: #8c8c8c
-    font-size: 0.8em
+    font-size: 12px
+    font-weight: bold
 
-.list-header .list-header-plus-icon, .js-open-list-menu, .list-header-menu a
+.list-header .list-header-plus-top, .js-open-list-menu, .list-header-menu a
     color #4d4d4d
     padding-left 4px
 
@@ -160,18 +157,6 @@
     float: left
 
 @media screen and (max-width: 800px)
-  .list-header-menu
-    position: absolute
-    padding: 27px 19px
-    margin-top: 1px
-    top: -7px
-    margin-right: 7px
-    right: -3px
-
-  .list-header
-    .list-header-name
-      margin-left: 1.4rem
-
   .mini-list
     flex: 0 0 60px
     height: auto
@@ -215,7 +200,6 @@
     display: flex
     align-items: center
     .list-header-left-icon
-      display: inline
       padding: 7px
       padding-right: 27px
       margin-top: 1px
@@ -238,6 +222,30 @@
       right: 10px
       font-size: 24px
 
+  .list-header
+    display: grid
+    grid-template-columns: 30px 5fr 1fr
+    .list-header-left-icon
+      display: grid
+      grid-row: 1/3
+      grid-column: 1
+    .list-header-name
+      grid-row: 1
+      grid-column: 2
+      align-self: end
+    .cardCount
+      grid-row: 2
+      grid-column: 2
+      align-self: start
+    .list-header-menu
+      grid-row: 1/3
+      grid-column: 3
+    .inlined-form
+      grid-row: 1/3
+      grid-column: 1/4
+    .edit-controls
+      align-items: initial
+
 .link-board-wrapper
   display: flex
   align-items: baseline

+ 12 - 0
client/components/lists/listBody.jade

@@ -4,6 +4,17 @@ template(name="listBody")
       if cards.count
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
+      ul.sidebar-list
+        each customFieldsSum
+          li
+            +viewer
+              = name
+            if $eq customFieldsSum.type "number"
+              +viewer
+                = value
+            if $eq customFieldsSum.type "currency"
+              +viewer
+                = formattedCurrencyCustomFieldValue(value)
       each (cardsWithLimit (idOrNull ../../_id))
         a.minicard-wrapper.js-minicard(href=originRelativeUrl
           class="{{#if cardIsSelected}}is-selected{{/if}}"
@@ -42,6 +53,7 @@ template(name="addCardForm")
 
   .add-controls.clearfix
     button.primary.confirm(type="submit") {{_ 'add'}}
+    a.fa.fa-times-thin.js-close-inlined-form
     unless currentBoard.isTemplatesBoard
       unless currentBoard.isTemplateBoard
         span.quiet

+ 26 - 14
client/components/lists/listBody.js

@@ -13,6 +13,13 @@ BlazeComponent.extendComponent({
     return [];
   },
 
+  customFieldsSum() {
+    return CustomFields.find({
+      boardIds: { $in: [Session.get('currentBoard')] },
+      showSumAtTopOfList: true,
+    });
+  },
+
   openForm(options) {
     options = options || {};
     options.position = options.position || 'top';
@@ -141,6 +148,10 @@ BlazeComponent.extendComponent({
       // If the card is already selected, we want to de-select it.
       // XXX We should probably modify the minicard href attribute instead of
       // overwriting the event in case the card is already selected.
+    } else if (Utils.isMiniScreen()) {
+      evt.preventDefault();
+      Session.set('popupCardId', this.currentData()._id);
+      this.cardDetailsPopup(evt);
     } else if (Session.equals('currentCard', this.currentData()._id)) {
       evt.stopImmediatePropagation();
       evt.preventDefault();
@@ -209,6 +220,12 @@ BlazeComponent.extendComponent({
     );
   },
 
+  cardDetailsPopup(event) {
+    if (!Popup.isOpen()) {
+      Popup.open("cardDetails")(event);
+    }
+  },
+
   events() {
     return [
       {
@@ -479,7 +496,7 @@ BlazeComponent.extendComponent({
           evt.preventDefault();
           const linkedId = $('.js-select-cards option:selected').val();
           if (!linkedId) {
-            Popup.close();
+            Popup.back();
             return;
           }
           const _id = Cards.insert({
@@ -494,7 +511,7 @@ BlazeComponent.extendComponent({
             linkedId,
           });
           Filter.addException(_id);
-          Popup.close();
+          Popup.back();
         },
         'click .js-link-board'(evt) {
           //LINK BOARD
@@ -505,7 +522,7 @@ BlazeComponent.extendComponent({
             !impBoardId ||
             Cards.findOne({ linkedId: impBoardId, archived: false })
           ) {
-            Popup.close();
+            Popup.back();
             return;
           }
           const _id = Cards.insert({
@@ -520,7 +537,7 @@ BlazeComponent.extendComponent({
             linkedId: impBoardId,
           });
           Filter.addException(_id);
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -567,7 +584,7 @@ BlazeComponent.extendComponent({
       });
     }
     if (!board) {
-      Popup.close();
+      Popup.back();
       return;
     }
     const boardId = board._id;
@@ -694,7 +711,7 @@ BlazeComponent.extendComponent({
               },
             );
           }
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -782,17 +799,12 @@ BlazeComponent.extendComponent({
       return false;
     }
 
+    const spinnerViewPosition = this.spinner.offsetTop - this.container.offsetTop + this.spinner.clientHeight;
+
     const parentViewHeight = this.container.clientHeight;
     const bottomViewPosition = this.container.scrollTop + parentViewHeight;
 
-    let spinnerOffsetTop = this.spinner.offsetTop;
-
-    const addCard = $(this.container).find("a.open-minicard-composer").first()[0];
-    if (addCard !== undefined) {
-      spinnerOffsetTop -= addCard.clientHeight;
-    }
-
-    return bottomViewPosition > spinnerOffsetTop;
+    return bottomViewPosition > spinnerViewPosition;
   }
 
   getSkSpinnerName() {

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

@@ -18,9 +18,9 @@ template(name="listHeader")
          span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.count}}
          |/#{wipLimit.value})
 
-        if showCardsCountForList cards.count
-          |&nbsp;
-          span(class="cardCount") {{cardsCount}} {{_ 'cards-count'}}
+      if showCardsCountForList cards.count
+        span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.count}}
+
       if isMiniScreen
         if currentList
           if isWatching
@@ -28,7 +28,7 @@ template(name="listHeader")
           div.list-header-menu
             unless currentUser.isCommentOnly
               if canSeeAddCard
-                a.js-add-card.fa.fa-plus.list-header-plus-icon(title="{{_ 'add-card-to-top-of-list'}}")
+                a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
               a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
         else
           a.list-header-menu-icon.fa.fa-angle-right.js-select-list
@@ -39,12 +39,12 @@ template(name="listHeader")
         div.list-header-menu
           unless currentUser.isCommentOnly
             //if isBoardAdmin
-            //  a.fa.js-list-star.list-header-plus-icon(class="fa-star{{#unless starred}}-o{{/unless}}")
+            //  a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
             if canSeeAddCard
-              a.js-add-card.fa.fa-plus.list-header-plus-icon(title="{{_ 'add-card-to-top-of-list'}}")
+              a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
             a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
           if currentUser.isBoardAdmin
-            if showDesktopDragHandles
+            if isShowDesktopDragHandles
               a.list-header-handle.handle.fa.fa-arrows.js-list-handle
 
 template(name="editListTitleForm")
@@ -55,6 +55,13 @@ template(name="editListTitleForm")
       a.fa.fa-times-thin.js-close-inlined-form
 
 template(name="listActionPopup")
+  ul.pop-over-list
+    li
+      a.js-add-card.list-header-plus-bottom
+        i.fa.fa-plus
+        i.fa.fa-arrow-down
+        | {{_ 'add-card-to-bottom-of-list'}}
+  hr
   ul.pop-over-list
     li
       a.js-toggle-watch-list

+ 24 - 19
client/components/lists/listHeader.js

@@ -85,6 +85,14 @@ BlazeComponent.extendComponent({
     return limit >= 0 && count >= limit;
   },
 
+  cardsCountForListIsOne(count) {
+    if (count === 1) {
+      return TAPi18n.__('cards-count-one');
+    } else {
+      return TAPi18n.__('cards-count');
+    }
+  },
+
   events() {
     return [
       {
@@ -93,7 +101,7 @@ BlazeComponent.extendComponent({
           this.starred(!this.starred());
         },
         'click .js-open-list-menu': Popup.open('listAction'),
-        'click .js-add-card'(event) {
+        'click .js-add-card.list-header-plus-top'(event) {
           const listDom = $(event.target).parents(
             `#js-list-${this.currentData()._id}`,
           )[0];
@@ -114,18 +122,7 @@ BlazeComponent.extendComponent({
 Template.listHeader.helpers({
   isBoardAdmin() {
     return Meteor.user().isBoardAdmin();
-  },
-
-  showDesktopDragHandles() {
-    currentUser = Meteor.user();
-    if (currentUser) {
-      return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-      return true;
-    } else {
-      return false;
-    }
-  },
+  }
 });
 
 Template.listActionPopup.helpers({
@@ -144,23 +141,31 @@ Template.listActionPopup.helpers({
 
 Template.listActionPopup.events({
   'click .js-list-subscribe'() {},
+  'click .js-add-card.list-header-plus-bottom'(event) {
+    const listDom = $(`#js-list-${this._id}`)[0];
+    const listComponent = BlazeComponent.getComponentForElement(listDom);
+    listComponent.openForm({
+      position: 'bottom',
+    });
+    Popup.back();
+  },
   'click .js-set-color-list': Popup.open('setListColor'),
   'click .js-select-cards'() {
     const cardIds = this.allCards().map(card => card._id);
     MultiSelection.add(cardIds);
-    Popup.close();
+    Popup.back();
   },
   'click .js-toggle-watch-list'() {
     const currentList = this;
     const level = currentList.findWatcher(Meteor.userId()) ? null : 'watching';
     Meteor.call('watch', 'list', currentList._id, level, (err, ret) => {
-      if (!err && ret) Popup.close();
+      if (!err && ret) Popup.back();
     });
   },
   'click .js-close-list'(event) {
     event.preventDefault();
     this.archive();
-    Popup.close();
+    Popup.back();
   },
   'click .js-set-wip-limit': Popup.open('setWipLimit'),
   'click .js-more': Popup.open('listMore'),
@@ -236,7 +241,7 @@ BlazeComponent.extendComponent({
 
 Template.listMorePopup.events({
   'click .js-delete': Popup.afterConfirm('listDelete', function() {
-    Popup.close();
+    Popup.back();
     // TODO how can we avoid the fetch call?
     const allCards = this.allCards().fetch();
     const allCardIds = _.pluck(allCards, '_id');
@@ -302,11 +307,11 @@ BlazeComponent.extendComponent({
         },
         'click .js-submit'() {
           this.currentList.setColor(this.currentColor.get());
-          Popup.close();
+          Popup.back();
         },
         'click .js-remove-color'() {
           this.currentList.setColor(null);
-          Popup.close();
+          Popup.back();
         },
       },
     ];

+ 2 - 2
client/components/main/dueCards.js

@@ -38,12 +38,12 @@ BlazeComponent.extendComponent({
       {
         'click .js-due-cards-view-me'() {
           Utils.setDueCardsView('me');
-          Popup.close();
+          Popup.back();
         },
 
         'click .js-due-cards-view-all'() {
           Utils.setDueCardsView('all');
-          Popup.close();
+          Popup.back();
         },
       },
     ];

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

@@ -1,4 +1,6 @@
 template(name="editor")
+  a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
+  span.copied-tooltip {{_ 'copied'}}
   textarea.editor(
     dir="auto"
     class="{{class}}"

+ 277 - 259
client/components/main/editor.js

@@ -4,281 +4,299 @@ const specialHandles = [
 ];
 const specialHandleNames = specialHandles.map(m => m.username);
 
-Template.editor.onRendered(() => {
-  const textareaSelector = 'textarea';
-  const mentions = [
-    // User mentions
-    {
-      match: /\B@([\w.]*)$/,
-      search(term, callback) {
-        const currentBoard = Boards.findOne(Session.get('currentBoard'));
-        callback(
-          _.union(
-          currentBoard
-            .activeMembers()
-            .map(member => {
-              const username = Users.findOne(member.userId).username;
-              return username.includes(term) ? username : null;
-            })
-            .filter(Boolean), [...specialHandleNames])
-        );
-      },
-      template(value) {
-        return value;
-      },
-      replace(username) {
-        return `@${username} `;
+
+BlazeComponent.extendComponent({
+  onRendered() {
+    const textareaSelector = 'textarea';
+    const mentions = [
+      // User mentions
+      {
+        match: /\B@([\w.]*)$/,
+        search(term, callback) {
+          const currentBoard = Boards.findOne(Session.get('currentBoard'));
+          callback(
+            _.union(
+            currentBoard
+              .activeMembers()
+              .map(member => {
+                const user = Users.findOne(member.userId);
+                const username = user.username;
+                const fullName = user.profile && user.profile !== undefined ?  user.profile.fullname : "";
+                return username.includes(term) || fullName.includes(term) ?  fullName + "(" + username + ")" : null;
+              })
+              .filter(Boolean), [...specialHandleNames])
+          );
+        },
+        template(value) {
+          return value;
+        },
+        replace(username) {
+          return `@${username} `;
+        },
+        index: 1,
       },
-      index: 1,
-    },
-  ];
-  const enableTextarea = function() {
-    const $textarea = this.$(textareaSelector);
-    autosize($textarea);
-    $textarea.escapeableTextComplete(mentions);
-  };
-  if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) {
-    const isSmall = Utils.isMiniScreen();
-    const toolbar = isSmall
-      ? [
-          ['view', ['fullscreen']],
-          ['table', ['table']],
-          ['font', ['bold', 'underline']],
-          //['fontsize', ['fontsize']],
-          ['color', ['color']],
-        ]
-      : [
-          ['style', ['style']],
-          ['font', ['bold', 'underline', 'clear']],
-          ['fontsize', ['fontsize']],
-          ['fontname', ['fontname']],
-          ['color', ['color']],
-          ['para', ['ul', 'ol', 'paragraph']],
-          ['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']], // modal popup has issue somehow :(
-          ['view', ['fullscreen', 'codeview', 'help']],
-        ];
-    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 enableTextarea = function() {
+      const $textarea = this.$(textareaSelector);
+      autosize($textarea);
+      $textarea.escapeableTextComplete(mentions);
     };
-    const editor = '.editor';
-    const selectors = [
-      `.js-new-description-form ${editor}`,
-      `.js-new-comment-form ${editor}`,
-      `.js-edit-comment ${editor}`,
-    ].join(','); // only new comment and edit comment
-    const inputs = $(selectors);
-    if (inputs.length === 0) {
-      // only enable richereditor to new comment or edit comment no others
-      enableTextarea();
-    } else {
-      const placeholder = inputs.attr('placeholder') || '';
-      const mSummernotes = [];
-      const getSummernote = function(input) {
-        const idx = inputs.index(input);
-        if (idx > -1) {
-          return mSummernotes[idx];
-        }
-        return undefined;
+    if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) {
+      const isSmall = Utils.isMiniScreen();
+      const toolbar = isSmall
+        ? [
+            ['view', ['fullscreen']],
+            ['table', ['table']],
+            ['font', ['bold', 'underline']],
+            //['fontsize', ['fontsize']],
+            ['color', ['color']],
+          ]
+        : [
+            ['style', ['style']],
+            ['font', ['bold', 'underline', 'clear']],
+            ['fontsize', ['fontsize']],
+            ['fontname', ['fontname']],
+            ['color', ['color']],
+            ['para', ['ul', 'ol', 'paragraph']],
+            ['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']], // modal popup has issue somehow :(
+            ['view', ['fullscreen', 'codeview', 'help']],
+          ];
+      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;
       };
-      inputs.each(function(idx, input) {
-        mSummernotes[idx] = $(input).summernote({
-          placeholder,
-          callbacks: {
-            onInit(object) {
-              const originalInput = this;
-              $(originalInput).on('submitted', function() {
-                // when comment is submitted, the original textarea will be set to '', so shall we
-                if (!this.value) {
-                  const sn = getSummernote(this);
-                  sn && sn.summernote('code', '');
-                }
-              });
-              const jEditor = object && object.editable;
-              const toolbar = object && object.toolbar;
-              if (jEditor !== undefined) {
-                jEditor.escapeableTextComplete(mentions);
-              }
-              if (toolbar !== undefined) {
-                const fBtn = toolbar.find('.btn-fullscreen');
-                fBtn.on('click', function() {
-                  const $this = $(this),
-                    isActive = $this.hasClass('active');
-                  $('.minicards,#header-quick-access').toggle(!isActive); // mini card is still showing when editor is in fullscreen mode, we hide here manually
+      const editor = '.editor';
+      const selectors = [
+        `.js-new-description-form ${editor}`,
+        `.js-new-comment-form ${editor}`,
+        `.js-edit-comment ${editor}`,
+      ].join(','); // only new comment and edit comment
+      const inputs = $(selectors);
+      if (inputs.length === 0) {
+        // only enable richereditor to new comment or edit comment no others
+        enableTextarea();
+      } else {
+        const placeholder = inputs.attr('placeholder') || '';
+        const mSummernotes = [];
+        const getSummernote = function(input) {
+          const idx = inputs.index(input);
+          if (idx > -1) {
+            return mSummernotes[idx];
+          }
+          return undefined;
+        };
+        inputs.each(function(idx, input) {
+          mSummernotes[idx] = $(input).summernote({
+            placeholder,
+            callbacks: {
+              onInit(object) {
+                const originalInput = this;
+                $(originalInput).on('submitted', function() {
+                  // when comment is submitted, the original textarea will be set to '', so shall we
+                  if (!this.value) {
+                    const sn = getSummernote(this);
+                    sn && sn.summernote('code', '');
+                  }
                 });
-              }
-            },
+                const jEditor = object && object.editable;
+                const toolbar = object && object.toolbar;
+                if (jEditor !== undefined) {
+                  jEditor.escapeableTextComplete(mentions);
+                }
+                if (toolbar !== undefined) {
+                  const fBtn = toolbar.find('.btn-fullscreen');
+                  fBtn.on('click', function() {
+                    const $this = $(this),
+                      isActive = $this.hasClass('active');
+                    $('.minicards,#header-quick-access').toggle(!isActive); // mini card is still showing when editor is in fullscreen mode, we hide here manually
+                  });
+                }
+              },
 
-            onImageUpload(files) {
-              const $summernote = getSummernote(this);
-              if (files && files.length > 0) {
-                const image = files[0];
-                const currentCard = Cards.findOne(Session.get('currentCard'));
-                const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
-                const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
-                const insertImage = src => {
-                  // process all image upload types to the description/comment window
-                  const img = document.createElement('img');
-                  img.src = src;
-                  img.setAttribute('width', '100%');
-                  $summernote.summernote('insertNode', img);
-                };
-                const processData = function(fileObj) {
-                  Utils.processUploadedAttachment(
-                    currentCard,
-                    fileObj,
-                    attachment => {
-                      if (
-                        attachment &&
-                        attachment._id &&
-                        attachment.isImage()
-                      ) {
-                        attachment.one('uploaded', function() {
-                          const maxTry = 3;
-                          const checkItvl = 500;
-                          let retry = 0;
-                          const checkUrl = function() {
-                            // even though uploaded event fired, attachment.url() is still null somehow //TODO
-                            const url = attachment.url();
-                            if (url) {
-                              insertImage(
-                                `${location.protocol}//${location.host}${url}`,
-                              );
-                            } else {
-                              retry++;
-                              if (retry < maxTry) {
-                                setTimeout(checkUrl, checkItvl);
+              onImageUpload(files) {
+                const $summernote = getSummernote(this);
+                if (files && files.length > 0) {
+                  const image = files[0];
+                  const currentCard = Utils.getCurrentCard();
+                  const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
+                  const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
+                  const insertImage = src => {
+                    // process all image upload types to the description/comment window
+                    const img = document.createElement('img');
+                    img.src = src;
+                    img.setAttribute('width', '100%');
+                    $summernote.summernote('insertNode', img);
+                  };
+                  const processData = function(fileObj) {
+                    Utils.processUploadedAttachment(
+                      currentCard,
+                      fileObj,
+                      attachment => {
+                        if (
+                          attachment &&
+                          attachment._id &&
+                          attachment.isImage()
+                        ) {
+                          attachment.one('uploaded', function() {
+                            const maxTry = 3;
+                            const checkItvl = 500;
+                            let retry = 0;
+                            const checkUrl = function() {
+                              // even though uploaded event fired, attachment.url() is still null somehow //TODO
+                              const url = attachment.url();
+                              if (url) {
+                                insertImage(
+                                  `${location.protocol}//${location.host}${url}`,
+                                );
+                              } else {
+                                retry++;
+                                if (retry < maxTry) {
+                                  setTimeout(checkUrl, checkItvl);
+                                }
                               }
+                            };
+                            checkUrl();
+                          });
+                        }
+                      },
+                    );
+                  };
+                  if (MAX_IMAGE_PIXEL) {
+                    const reader = new FileReader();
+                    reader.onload = function(e) {
+                      const dataurl = e && e.target && e.target.result;
+                      if (dataurl !== undefined) {
+                        // need to shrink image
+                        Utils.shrinkImage({
+                          dataurl,
+                          maxSize: MAX_IMAGE_PIXEL,
+                          ratio: COMPRESS_RATIO,
+                          toBlob: true,
+                          callback(blob) {
+                            if (blob !== false) {
+                              blob.name = image.name;
+                              processData(blob);
                             }
-                          };
-                          checkUrl();
+                          },
                         });
                       }
-                    },
-                  );
-                };
-                if (MAX_IMAGE_PIXEL) {
-                  const reader = new FileReader();
-                  reader.onload = function(e) {
-                    const dataurl = e && e.target && e.target.result;
-                    if (dataurl !== undefined) {
-                      // need to shrink image
-                      Utils.shrinkImage({
-                        dataurl,
-                        maxSize: MAX_IMAGE_PIXEL,
-                        ratio: COMPRESS_RATIO,
-                        toBlob: true,
-                        callback(blob) {
-                          if (blob !== false) {
-                            blob.name = image.name;
-                            processData(blob);
-                          }
-                        },
-                      });
-                    }
-                  };
-                  reader.readAsDataURL(image);
-                } else {
-                  processData(image);
+                    };
+                    reader.readAsDataURL(image);
+                  } else {
+                    processData(image);
+                  }
                 }
-              }
-            },
-            onPaste(e) {
-              var clipboardData = e.clipboardData;
-              var pastedData = clipboardData.getData('Text');
+              },
+              onPaste(e) {
+                var clipboardData = e.clipboardData;
+                var pastedData = clipboardData.getData('Text');
 
-              //if pasted data is an image, exit
-              if (!pastedData.length) {
-                e.preventDefault();
-                return;
-              }
+                //if pasted data is an image, exit
+                if (!pastedData.length) {
+                  e.preventDefault();
+                  return;
+                }
 
-              // clear up unwanted tag info when user pasted in text
-              const thisNote = this;
-              const updatePastedText = function(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 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('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code.
-              };
-              setTimeout(function() {
-                //this kinda sucks, but if you don't do a setTimeout,
-                //the function is called before the text is really pasted.
-                updatePastedText(thisNote);
-              }, 10);
+                // clear up unwanted tag info when user pasted in text
+                const thisNote = this;
+                const updatePastedText = function(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 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('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code.
+                };
+                setTimeout(function() {
+                  //this kinda sucks, but if you don't do a setTimeout,
+                  //the function is called before the text is really pasted.
+                  updatePastedText(thisNote);
+                }, 10);
+              },
             },
-          },
-          dialogsInBody: true,
-          spellCheck: true,
-          disableGrammar: false,
-          disableDragAndDrop: false,
-          toolbar,
-          popover: {
-            image: [
-              ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
-              ['float', ['floatLeft', 'floatRight', 'floatNone']],
-              ['remove', ['removeMedia']],
-            ],
-            link: [['link', ['linkDialogShow', 'unlink']]],
-            table: [
-              ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
-              ['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
-            ],
-            air: [
-              ['color', ['color']],
-              ['font', ['bold', 'underline', 'clear']],
-            ],
-          },
-          height: 200,
+            dialogsInBody: true,
+            spellCheck: true,
+            disableGrammar: false,
+            disableDragAndDrop: false,
+            toolbar,
+            popover: {
+              image: [
+                ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
+                ['float', ['floatLeft', 'floatRight', 'floatNone']],
+                ['remove', ['removeMedia']],
+              ],
+              link: [['link', ['linkDialogShow', 'unlink']]],
+              table: [
+                ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
+                ['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
+              ],
+              air: [
+                ['color', ['color']],
+                ['font', ['bold', 'underline', 'clear']],
+              ],
+            },
+            height: 200,
+          });
         });
-      });
+      }
+    } else {
+      enableTextarea();
     }
-  } else {
-    enableTextarea();
+  },
+  events() {
+    return [
+      {
+        'click a.fa.fa-copy'(event) {
+          const $editor = this.$('textarea.editor');
+          const promise = Utils.copyTextToClipboard($editor[0].value);
+
+          const $tooltip = this.$('.copied-tooltip');
+          Utils.showCopied(promise, $tooltip);
+        },
+      }
+    ]
   }
-});
+}).register('editor');
 
 import DOMPurify from 'dompurify';
 

+ 7 - 0
client/components/main/editor.styl

@@ -0,0 +1,7 @@
+.new-comment,
+.inlined-form
+  a.fa.fa-copy
+    float: right
+    position: relative
+    top: 20px
+    right: 6px

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

@@ -16,12 +16,14 @@ template(name="header")
             each currentBoard.lists
               li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
                 a.js-select-list
-                  = title
+                  +viewer
+                    = title
           else
             each currentUser.starredBoards
               li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
                 a(href="{{pathFor 'board' id=_id slug=slug}}")
-                  = title
+                  +viewer
+                    = title
         #header-new-board-icon
       else
         //-
@@ -36,7 +38,8 @@ template(name="header")
             unless currentSetting.customTopLeftCornerLogoLinkUrl
               img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
           unless currentSetting.customTopLeftCornerLogoImageUrl
-            img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
+            div#headerIsSettingDatabaseCallDone
+              img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
         span.allBoards
           a(href="{{pathFor 'home'}}")
             span.fa.fa-home
@@ -49,7 +52,8 @@ template(name="header")
           each currentUser.starredBoards
             li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
               a(href="{{pathFor 'board' id=_id slug=slug}}")
-                = title
+                +viewer
+                  = title
           else
             li.current.empty {{_ 'quick-access-description'}}
 
@@ -90,3 +94,5 @@ template(name="offlineWarning")
     p
       i.fa.fa-warning
       | {{_ 'app-is-offline'}}
+
+      a.app-try-reconnect {{_ 'app-try-reconnect'}}

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

@@ -1,7 +1,23 @@
 Meteor.subscribe('user-admin');
 Meteor.subscribe('boards');
 Meteor.subscribe('setting');
+Template.header.onCreated(function(){
+  const templateInstance = this;
+  templateInstance.currentSetting = new ReactiveVar();
+  templateInstance.isLoading = new ReactiveVar(false);
 
+  Meteor.subscribe('setting', {
+    onReady() {
+      templateInstance.currentSetting.set(Settings.findOne());
+      let currSetting = templateInstance.currentSetting.curValue;
+      if(currSetting && currSetting !== undefined && currSetting.customLoginLogoImageUrl !== undefined && document.getElementById("headerIsSettingDatabaseCallDone") != null)
+        document.getElementById("headerIsSettingDatabaseCallDone").style.display = 'none';
+      else if(document.getElementById("headerIsSettingDatabaseCallDone") != null)
+        document.getElementById("headerIsSettingDatabaseCallDone").style.display = 'block';
+      return this.stop();
+    },
+  });
+});
 Template.header.helpers({
   wrappedHeader() {
     return !Session.get('currentBoard');
@@ -41,3 +57,10 @@ Template.header.events({
     Session.set('currentCard', null);
   },
 });
+
+Template.offlineWarning.events({
+  'click a.app-try-reconnect'(event) {
+    event.preventDefault();
+    Meteor.reconnect();
+  },
+});

+ 11 - 0
client/components/main/header.styl

@@ -135,6 +135,14 @@
         padding: 12px 10px
         margin: -10px 0px
 
+        .viewer
+          display: inline
+          white-space: nowrap
+
+          p
+            display: inline
+            white-space: nowrap
+
       &.current
         color: darken(white, 5%)
 
@@ -242,3 +250,6 @@
   p
     margin: 7px
     padding: 0
+
+#headerIsSettingDatabaseCallDone
+  display: none;

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

@@ -34,8 +34,9 @@ template(name="userFormsLayout")
           img(src="{{currentSetting.customLoginLogoImageUrl}}" width="300" height="auto")
           br
       unless currentSetting.customLoginLogoImageUrl
-        img(src="{{pathFor '/wekan-logo.svg'}}" alt="" width="300" height="auto")
-        br
+        div#isSettingDatabaseCallDone
+          img(src="{{pathFor '/wekan-logo.svg'}}" alt="" width="300" height="auto")
+          br
       if currentSetting.textBelowCustomLoginLogo
         +viewer
           | {{currentSetting.textBelowCustomLoginLogo}}
@@ -47,6 +48,13 @@ template(name="userFormsLayout")
         +Template.dynamic(template=content)
         if currentSetting.displayAuthenticationMethod
           +connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
+        if isLegalNoticeLinkExist
+          div#legalNoticeDiv
+            span#legalNoticeSpan {{_ 'acceptance_of_our_legalNotice'}}
+            a#legalNoticeAtLink.at-link(href="{{currentSetting.legalNotice}}", target="_blank", rel="noopener noreferrer")
+              | {{_ 'legalNotice'}}
+          if getLegalNoticeWithWritTraduction
+            div
         div.at-form-lang
           select.select-lang.js-userform-set-language
             each languages

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

@@ -6,6 +6,9 @@ const i18nTagToT9n = i18nTag => {
   return i18nTag;
 };
 
+let alreadyCheck = 1;
+let isCheckDone = false;
+
 const validator = {
   set(obj, prop, value) {
     if (prop === 'state' && value !== 'signIn') {
@@ -20,6 +23,8 @@ const validator = {
   },
 };
 
+// let isSettingDatabaseFctCallDone = false;
+
 Template.userFormsLayout.onCreated(function() {
   const templateInstance = this;
   templateInstance.currentSetting = new ReactiveVar();
@@ -28,6 +33,18 @@ Template.userFormsLayout.onCreated(function() {
   Meteor.subscribe('setting', {
     onReady() {
       templateInstance.currentSetting.set(Settings.findOne());
+      let currSetting = templateInstance.currentSetting.curValue;
+      let oidcBtnElt = $("#at-oidc");
+      if(currSetting && currSetting !== undefined && currSetting.oidcBtnText !== undefined && oidcBtnElt != null && oidcBtnElt != undefined){
+        let htmlvalue = "<i class='fa fa-oidc'></i>" + currSetting.oidcBtnText;
+        oidcBtnElt.html(htmlvalue);
+      }
+
+      // isSettingDatabaseFctCallDone = true;
+      if(currSetting && currSetting !== undefined && currSetting.customLoginLogoImageUrl !== undefined)
+        document.getElementById("isSettingDatabaseCallDone").style.display = 'none';
+      else
+        document.getElementById("isSettingDatabaseCallDone").style.display = 'block';
       return this.stop();
     },
   });
@@ -56,6 +73,31 @@ Template.userFormsLayout.helpers({
     return Template.instance().currentSetting.get();
   },
 
+  // isSettingDatabaseCallDone(){
+  //   return isSettingDatabaseFctCallDone;
+  // },
+
+  isLegalNoticeLinkExist(){
+    const currSet = Template.instance().currentSetting.get();
+    if(currSet && currSet !== undefined && currSet != null){
+      return currSet.legalNotice !== undefined && currSet.legalNotice.trim() != "";
+    }
+    else
+      return false;
+  },
+
+  getLegalNoticeWithWritTraduction(){
+    let spanLegalNoticeElt = $("#legalNoticeSpan");
+    if(spanLegalNoticeElt != null && spanLegalNoticeElt != undefined){
+      spanLegalNoticeElt.html(TAPi18n.__('acceptance_of_our_legalNotice', {}, T9n.getLanguage() || 'en'));
+    }
+    let atLinkLegalNoticeElt = $("#legalNoticeAtLink");
+    if(atLinkLegalNoticeElt != null && atLinkLegalNoticeElt != undefined){
+      atLinkLegalNoticeElt.html(TAPi18n.__('legalNotice', {}, T9n.getLanguage() || 'en'));
+    }
+    return true;
+  },
+
   isLoading() {
     return Template.instance().isLoading.get();
   },
@@ -79,6 +121,10 @@ Template.userFormsLayout.helpers({
         name = 'مَصرى';
       } else if (lang.name === 'de-CH') {
         name = 'Deutsch (Schweiz)';
+      } else if (lang.name === 'de-AT') {
+        name = 'Deutsch (Österreich)';
+      } else if (lang.name === 'en-DE') {
+        name = 'English (Germany)';
       } else if (lang.name === 'fa-IR') {
         // fa-IR = Persian (Iran)
         name = 'فارسی/پارسی (ایران‎)';
@@ -86,14 +132,28 @@ Template.userFormsLayout.helpers({
         name = 'Français (Belgique)';
       } else if (lang.name === 'fr-CA') {
         name = 'Français (Canada)';
+      } else if (lang.name === 'fr-CH') {
+        name = 'Français (Schweiz)';
+      } else if (lang.name === 'gu-IN') {
+        // gu-IN = Gurajati (India)
+        name = 'ગુજરાતી';
+      } else if (lang.name === 'hi-IN') {
+        // hi-IN = Hindi (India)
+        name = 'हिंदी (भारत)';
       } else if (lang.name === 'ig') {
         name = 'Igbo';
       } else if (lang.name === 'lv') {
         name = 'Latviešu';
       } else if (lang.name === 'latviešu valoda') {
         name = 'Latviešu';
+      } else if (lang.name === 'ms-MY') {
+        // ms-MY = Malay (Malaysia)
+        name = 'بهاس ملايو';
       } else if (lang.name === 'en-IT') {
         name = 'English (Italy)';
+      } else if (lang.name === 'el-GR') {
+        // el-GR = Greek (Greece)
+        name = 'Ελληνικά (Ελλάδα)';
       } else if (lang.name === 'Español') {
         name = 'español';
       } else if (lang.name === 'es_419') {
@@ -125,6 +185,7 @@ Template.userFormsLayout.helpers({
       } else if (lang.name === 'st') {
         name = 'Sãotomense';
       } else if (lang.name === '繁体中文(台湾)') {
+        // Traditional Chinese (Taiwan)
         name = '繁體中文(台灣)';
       }
       return { tag, name };
@@ -157,6 +218,53 @@ Template.userFormsLayout.events({
         templateInstance.isLoading.set(false);
       });
     }
+    isCheckDone = false;
+  },
+  'click #at-signUp'(event, templateInstance){
+    isCheckDone = false;
+  },
+  'DOMSubtreeModified #at-oidc'(event){
+    if(alreadyCheck <= 2){
+      let currSetting = Settings.findOne();
+      let oidcBtnElt = $("#at-oidc");
+      if(currSetting && currSetting !== undefined && currSetting.oidcBtnText !== undefined && oidcBtnElt != null && oidcBtnElt != undefined){
+        let htmlvalue = "<i class='fa fa-oidc'></i>" + currSetting.oidcBtnText;
+        if(alreadyCheck == 1){
+          alreadyCheck++;
+          oidcBtnElt.html("");
+        }
+        else{
+          alreadyCheck++;
+          oidcBtnElt.html(htmlvalue);
+        }
+      }
+    }
+    else{
+      alreadyCheck = 1;
+    }
+  },
+  'DOMSubtreeModified .at-form'(event){
+    if(alreadyCheck <= 2 && !isCheckDone){
+      if(document.getElementById("at-oidc") != null){
+        let currSetting = Settings.findOne();
+        let oidcBtnElt = $("#at-oidc");
+        if(currSetting && currSetting !== undefined && currSetting.oidcBtnText !== undefined && oidcBtnElt != null && oidcBtnElt != undefined){
+          let htmlvalue = "<i class='fa fa-oidc'></i>" + currSetting.oidcBtnText;
+          if(alreadyCheck == 1){
+            alreadyCheck++;
+            oidcBtnElt.html("");
+          }
+          else{
+            alreadyCheck++;
+            isCheckDone = true;
+            oidcBtnElt.html(htmlvalue);
+          }
+        }
+      }
+    }
+    else{
+      alreadyCheck = 1;
+    }
   },
 });
 

+ 9 - 1
client/components/main/layouts.styl

@@ -433,7 +433,7 @@ a
       margin-top: 0px
 
   .wrapper
-    height: 100%
+    height: calc(100% - 31px)
     margin: 0px
 
   .panel-default
@@ -542,3 +542,11 @@ a
 
   100%
     transform: rotate(360deg)
+
+#isSettingDatabaseCallDone
+  display: none;
+
+.at-link
+  color: #17683a;
+  text-decoration: underline;
+  text-decoration-color: #17683a;

+ 1 - 12
client/components/main/popup.styl

@@ -14,8 +14,7 @@ $popupWidth = 300px
   margin-top: 5px
 
   hr
-    margin: 4px -10px
-    width: $popupWidth
+    margin: 4px 0px
 
   p,
   textarea,
@@ -23,7 +22,6 @@ $popupWidth = 300px
   input[type="email"],
   input[type="password"],
   input[type="file"]
-    margin: 4px 0 12px
     width: 100%
 
   select
@@ -313,22 +311,13 @@ $popupWidth = 300px
         input[type="email"],
         input[type="password"],
         input[type="file"]
-          margin: 4px 0 12px
           width: 100%
           box-sizing: border-box
 
     .pop-over-list
       li > a
         width: calc(100% - 20px)
-        padding: 10px 10px
         margin: 0px 0px
-        border-bottom: 1px solid #eee
-
-    hr
-      width: 100%
-      height: 20px
-      margin: 0px 0px
-      color: #eee
 
     for depth in (1..6)
       .popup-container-depth-{depth}

+ 1 - 1
client/components/rules/actions/cardActions.js

@@ -232,7 +232,7 @@ BlazeComponent.extendComponent({
         },
         'click .js-submit'() {
           this.colorButtonValue.set(this.currentColor.get());
-          Popup.close();
+          Popup.back();
         },
       },
     ];

+ 1 - 1
client/components/rules/triggers/boardTriggers.js

@@ -116,6 +116,6 @@ Template.boardCardTitlePopup.events({
       .trim();
     Popup.getOpenerComponent().setNameFilter(title);
     event.preventDefault();
-    Popup.close();
+    Popup.back();
   },
 });

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

@@ -21,7 +21,7 @@ template(name='statistics')
     table
       tbody
         tr
-          th Wekan {{_ 'info'}}
+          th WeKan ® {{_ 'info'}}
           td {{statistics.version}}
         tr
           th {{_ 'Meteor_version'}}
@@ -65,3 +65,49 @@ template(name='statistics')
         tr
           th {{_ 'OS_Cpus'}}
           td {{statistics.os.cpus.length}}
+        unless isSandstorm
+          tr
+            th {{_ 'Node_heap_total_heap_size'}}
+            td {{bytesToSize statistics.nodeHeapStats.totalHeapSize}}
+          tr
+            th {{_ 'Node_heap_total_heap_size_executable'}}
+            td {{bytesToSize statistics.nodeHeapStats.totalHeapSizeExecutable}}
+          tr
+            th {{_ 'Node_heap_total_physical_size'}}
+            td {{bytesToSize statistics.nodeHeapStats.totalPhysicalSize}}
+          tr
+            th {{_ 'Node_heap_total_available_size'}}
+            td {{bytesToSize statistics.nodeHeapStats.totalAvailableSize}}
+          tr
+            th {{_ 'Node_heap_used_heap_size'}}
+            td {{bytesToSize statistics.nodeHeapStats.usedHeapSize}}
+          tr
+            th {{_ 'Node_heap_heap_size_limit'}}
+            td {{bytesToSize statistics.nodeHeapStats.heapSizeLimit}}
+          tr
+            th {{_ 'Node_heap_malloced_memory'}}
+            td {{bytesToSize statistics.nodeHeapStats.mallocedMemory}}
+          tr
+            th {{_ 'Node_heap_peak_malloced_memory'}}
+            td {{bytesToSize statistics.nodeHeapStats.peakMallocedMemory}}
+          tr
+            th {{_ 'Node_heap_does_zap_garbage'}}
+            td {{statistics.nodeHeapStats.doesZapGarbage}}
+          tr
+            th {{_ 'Node_heap_number_of_native_contexts'}}
+            td {{statistics.nodeHeapStats.numberOfNativeContexts}}
+          tr
+            th {{_ 'Node_heap_number_of_detached_contexts'}}
+            td {{statistics.nodeHeapStats.numberOfDetachedContexts}}
+          tr
+            th {{_ 'Node_memory_usage_rss'}}
+            td {{bytesToSize statistics.nodeMemoryUsage.rss}}
+          tr
+            th {{_ 'Node_memory_usage_heap_total'}}
+            td {{bytesToSize statistics.nodeMemoryUsage.heapTotal}}
+          tr
+            th {{_ 'Node_memory_usage_heap_used'}}
+            td {{bytesToSize statistics.nodeMemoryUsage.heapUsed}}
+          tr
+            th {{_ 'Node_memory_usage_external'}}
+            td {{bytesToSize statistics.nodeMemoryUsage.external}}

+ 43 - 2
client/components/settings/peopleBody.jade

@@ -40,6 +40,11 @@ template(name="people")
               | {{_ 'search'}}
             .ext-box-right
               span {{#unless isMiniScreen}}{{_ 'people-number'}}{{/unless}} #{peopleNumber}
+            .divAddOrRemoveTeam#divAddOrRemoveTeam
+              button#addOrRemoveTeam
+                i.fa.fa-edit
+                | {{_ 'add'}} / {{_ 'delete'}} {{_ 'teams'}}
+
       .content-body
         .side-menu
           ul
@@ -97,9 +102,13 @@ template(name="teamGeneral")
         +teamRow(teamId=team._id)
 
 template(name="peopleGeneral")
+  #divAddOrRemoveTeamContainer
+    +modifyTeamsUsers
   table
     tbody
       tr
+        th
+          +selectAllUser
         th {{_ 'username'}}
         th {{_ 'fullname'}}
         th {{_ 'initials'}}
@@ -117,6 +126,10 @@ template(name="peopleGeneral")
       each user in peopleList
         +peopleRow(userId=user._id)
 
+template(name="selectAllUser")
+  | {{_ 'dueCardsViewChange-choice-all'}}
+  input.allUserChkBox(type="checkbox", id="chkSelectAll")
+
 template(name="newOrgRow")
   a.new-org
     i.fa.fa-plus-square
@@ -202,6 +215,12 @@ template(name="teamRow")
 
 template(name="peopleRow")
   tr
+    if userData.loginDisabled
+      td
+        input.selectUserChkBox(type="checkbox", disabled="disabled", id="{{userData._id}}")
+    else
+      td
+        input.selectUserChkBox(type="checkbox", id="{{userData._id}}")
     if userData.loginDisabled
       td.username <s>{{ userData.username }}</s>
     else
@@ -342,7 +361,7 @@ template(name="editUserPopup")
       input.js-profile-fullname(type="text" value=user.profile.fullname required)
     label
       | {{_ 'initials'}}
-      input.js-profile-initials(type="text" value=user.profile.initials required)
+      input.js-profile-initials(type="text" value=user.profile.initials)
     label
       | {{_ 'admin'}}
       select.select-role.js-profile-isadmin
@@ -453,6 +472,24 @@ template(name="newTeamPopup")
     div.buttonsContainer
       input.primary.wide(type="submit" value="{{_ 'save'}}")
 
+template(name="modifyTeamsUsers")
+  label
+    | {{_ 'teams'}}
+    select.js-teamsUser#jsteamsUser
+      each value in teamsDatas
+        option(value="{{value._id}}") {{_ value.teamDisplayName}}
+  hr
+  label
+    | {{_ 'r-action'}}
+    .form-group.flex
+      input.wekan-form-control#addAction(type="radio" name="action" value="true" checked="checked")
+      span {{_ 'add'}}
+      input.wekan-form-control#deleteAction(type="radio" name="action" value="false")
+      span {{_ 'delete'}}
+  div.buttonsContainer
+    input.primary.wide#addTeamBtn(type="submit" value="{{_ 'save'}}")
+    input.primary.wide#cancelBtn(type="submit" value="{{_ 'cancel'}}")
+
 template(name="newUserPopup")
   form
     //label.hide.userId(type="text" value=user._id)
@@ -469,7 +506,7 @@ template(name="newUserPopup")
       input.js-profile-username(type="text" value="" required)
     label
       | {{_ 'initials'}}
-      input.js-profile-initials(type="text" value="" required)
+      input.js-profile-initials(type="text" value="")
     label
       | {{_ 'email'}}
       span.error.hide.email-taken
@@ -571,10 +608,14 @@ template(name="settingsUserPopup")
       a.impersonate-user
         i.fa.fa-user
         | {{_ 'impersonate-user'}}
+    br
     hr
     li
       form
         label.hide.userId(type="text" value=user._id)
+        label
+          | {{_ 'delete-user-confirm-popup' }}
+        br
         div.buttonsContainer
           input#deleteButton.card-details-red.right.wide(type="button" value="{{_ 'delete'}}")
   // Delete is enabled, but there is still bug of leaving empty user avatars

+ 179 - 31
client/components/settings/peopleBody.js

@@ -2,6 +2,7 @@ const orgsPerPage = 25;
 const teamsPerPage = 25;
 const usersPerPage = 25;
 let userOrgsTeamsAction = ""; //poosible actions 'addOrg', 'addTeam', 'removeOrg' or 'removeTeam' when adding or modifying a user
+let selectedUserChkBoxUserIds = [];
 
 BlazeComponent.extendComponent({
   mixins() {
@@ -81,6 +82,9 @@ BlazeComponent.extendComponent({
         'click #searchButton'() {
           this.filterPeople();
         },
+        'click #addOrRemoveTeam'(){
+          document.getElementById("divAddOrRemoveTeamContainer").style.display = 'block';
+        },
         'keydown #searchInput'(event) {
           if (event.keyCode === 13 && !event.shiftKey) {
             this.filterPeople();
@@ -140,6 +144,7 @@ BlazeComponent.extendComponent({
   },
   orgList() {
     const orgs = Org.find(this.findOrgsOptions.get(), {
+      sort: { orgDisplayName: 1 },
       fields: { _id: true },
     });
     this.numberOrgs.set(orgs.count(false));
@@ -147,6 +152,7 @@ BlazeComponent.extendComponent({
   },
   teamList() {
     const teams = Team.find(this.findTeamsOptions.get(), {
+      sort: { teamDisplayName: 1 },
       fields: { _id: true },
     });
     this.numberTeams.set(teams.count(false));
@@ -154,6 +160,7 @@ BlazeComponent.extendComponent({
   },
   peopleList() {
     const users = Users.find(this.findUsersOptions.get(), {
+      sort: { username: 1 },
       fields: { _id: true },
     });
     this.numberPeople.set(users.count(false));
@@ -247,10 +254,10 @@ Template.editUserPopup.helpers({
     return Template.instance().authenticationMethods.get();
   },
   orgsDatas() {
-    return Org.find({}, {sort: { createdAt: -1 }});
+    return Org.find({}, {sort: { orgDisplayName: 1 }});
   },
   teamsDatas() {
-    return Team.find({}, {sort: { createdAt: -1 }});
+    return Team.find({}, {sort: { teamDisplayName: 1 }});
   },
   isSelected(match) {
     const userId = Template.instance().data.userId;
@@ -320,10 +327,10 @@ Template.newUserPopup.helpers({
     return Template.instance().authenticationMethods.get();
   },
   orgsDatas() {
-    return Org.find({}, {sort: { createdAt: -1 }});
+    return Org.find({}, {sort: { orgDisplayName: 1 }});
   },
   teamsDatas() {
-    return Team.find({}, {sort: { createdAt: -1 }});
+    return Team.find({}, {sort: { teamDisplayName: 1 }});
   },
   isSelected(match) {
     const userId = Template.instance().data.userId;
@@ -385,11 +392,111 @@ BlazeComponent.extendComponent({
       {
         'click a.edit-user': Popup.open('editUser'),
         'click a.more-settings-user': Popup.open('settingsUser'),
+        'click .selectUserChkBox': function(ev){
+            if(ev.currentTarget){
+              if(ev.currentTarget.checked){
+                if(!selectedUserChkBoxUserIds.includes(ev.currentTarget.id)){
+                  selectedUserChkBoxUserIds.push(ev.currentTarget.id);
+                }
+              }
+              else{
+                if(selectedUserChkBoxUserIds.includes(ev.currentTarget.id)){
+                  let index = selectedUserChkBoxUserIds.indexOf(ev.currentTarget.id);
+                  if(index > -1)
+                    selectedUserChkBoxUserIds.splice(index, 1);
+                }
+              }
+            }
+            if(selectedUserChkBoxUserIds.length > 0)
+              document.getElementById("divAddOrRemoveTeam").style.display = 'block';
+            else
+              document.getElementById("divAddOrRemoveTeam").style.display = 'none';
+        },
       },
     ];
   },
 }).register('peopleRow');
 
+BlazeComponent.extendComponent({
+  onCreated() {},
+  teamsDatas() {
+    return Team.find({}, {sort: { teamDisplayName: 1 }});
+  },
+  events() {
+    return [
+      {
+        'click #cancelBtn': function(){
+          let selectedElt = document.getElementById("jsteamsUser");
+          document.getElementById("divAddOrRemoveTeamContainer").style.display = 'none';
+        },
+        'click #addTeamBtn': function(){
+          let selectedElt;
+          let selectedEltValue;
+          let selectedEltValueId;
+          let userTms = [];
+          let currentUser;
+          let currUserTeamIndex;
+
+          selectedElt = document.getElementById("jsteamsUser");
+          selectedEltValue = selectedElt.options[selectedElt.selectedIndex].text;
+          selectedEltValueId = selectedElt.options[selectedElt.selectedIndex].value;
+
+          if(document.getElementById('addAction').checked){
+            for(let i = 0; i < selectedUserChkBoxUserIds.length; i++){
+              currentUser = Users.findOne(selectedUserChkBoxUserIds[i]);
+              userTms = currentUser.teams;
+              if(userTms == undefined || userTms.length == 0){
+                userTms = [];
+                userTms.push({
+                  "teamId": selectedEltValueId,
+                  "teamDisplayName": selectedEltValue,
+                })
+              }
+              else if(userTms.length > 0)
+              {
+                currUserTeamIndex = userTms.findIndex(function(t){ return t.teamId == selectedEltValueId});
+                if(currUserTeamIndex == -1){
+                  userTms.push({
+                    "teamId": selectedEltValueId,
+                    "teamDisplayName": selectedEltValue,
+                  });
+                }
+              }
+
+              Users.update(selectedUserChkBoxUserIds[i], {
+                $set:{
+                  teams: userTms
+                }
+              });
+            }
+          }
+          else{
+            for(let i = 0; i < selectedUserChkBoxUserIds.length; i++){
+              currentUser = Users.findOne(selectedUserChkBoxUserIds[i]);
+              userTms = currentUser.teams;
+              if(userTms !== undefined || userTms.length > 0)
+              {
+                currUserTeamIndex = userTms.findIndex(function(t){ return t.teamId == selectedEltValueId});
+                if(currUserTeamIndex != -1){
+                  userTms.splice(currUserTeamIndex, 1);
+                }
+              }
+
+              Users.update(selectedUserChkBoxUserIds[i], {
+                $set:{
+                  teams: userTms
+                }
+              });
+            }
+          }
+
+          document.getElementById("divAddOrRemoveTeamContainer").style.display = 'none';
+        },
+      },
+    ];
+  },
+}).register('modifyTeamsUsers');
+
 BlazeComponent.extendComponent({
   events() {
     return [
@@ -420,6 +527,41 @@ BlazeComponent.extendComponent({
   },
 }).register('newUserRow');
 
+BlazeComponent.extendComponent({
+  events() {
+    return [
+      {
+        'click .allUserChkBox': function(ev){
+          selectedUserChkBoxUserIds = [];
+          const checkboxes = document.getElementsByClassName("selectUserChkBox");
+          if(ev.currentTarget){
+            if(ev.currentTarget.checked){
+              for (let i=0; i<checkboxes.length; i++) {
+                if (!checkboxes[i].disabled) {
+                 selectedUserChkBoxUserIds.push(checkboxes[i].id);
+                 checkboxes[i].checked = true;
+                }
+             }
+            }
+            else{
+              for (let i=0; i<checkboxes.length; i++) {
+                if (!checkboxes[i].disabled) {
+                 checkboxes[i].checked = false;
+                }
+             }
+            }
+          }
+
+          if(selectedUserChkBoxUserIds.length > 0)
+            document.getElementById("divAddOrRemoveTeam").style.display = 'block';
+          else
+            document.getElementById("divAddOrRemoveTeam").style.display = 'none';
+        },
+      },
+    ];
+  },
+}).register('selectAllUser');
+
 Template.editOrgPopup.events({
   submit(event, templateInstance) {
     event.preventDefault();
@@ -431,8 +573,7 @@ Template.editOrgPopup.events({
     const orgDesc = templateInstance.find('.js-orgDesc').value.trim();
     const orgShortName = templateInstance.find('.js-orgShortName').value.trim();
     const orgWebsite = templateInstance.find('.js-orgWebsite').value.trim();
-    const orgIsActive =
-      templateInstance.find('.js-org-isactive').value.trim() == 'true';
+    const orgIsActive = templateInstance.find('.js-org-isactive').value.trim() == 'true';
 
     const isChangeOrgDisplayName = orgDisplayName !== org.orgDisplayName;
     const isChangeOrgDesc = orgDesc !== org.orgDesc;
@@ -458,7 +599,7 @@ Template.editOrgPopup.events({
       );
     }
 
-    Popup.close();
+    Popup.back();
   },
 });
 
@@ -502,7 +643,7 @@ Template.editTeamPopup.events({
       );
     }
 
-    Popup.close();
+    Popup.back();
   },
 });
 
@@ -617,7 +758,7 @@ Template.editUserPopup.events({
           } else {
             usernameMessageElement.hide();
             emailMessageElement.hide();
-            Popup.close();
+            Popup.back();
           }
         },
       );
@@ -631,7 +772,7 @@ Template.editUserPopup.events({
           }
         } else {
           usernameMessageElement.hide();
-          Popup.close();
+          Popup.back();
         }
       });
     } else if (isChangeEmail) {
@@ -648,11 +789,11 @@ Template.editUserPopup.events({
             }
           } else {
             emailMessageElement.hide();
-            Popup.close();
+            Popup.back();
           }
         },
       );
-    } else Popup.close();
+    } else Popup.back();
   },
   'click #addUserOrg'(event) {
     event.preventDefault();
@@ -787,7 +928,7 @@ Template.newOrgPopup.events({
       orgWebsite,
       orgIsActive,
     );
-    Popup.close();
+    Popup.back();
   },
 });
 
@@ -813,7 +954,7 @@ Template.newTeamPopup.events({
       teamWebsite,
       teamIsActive,
     );
-    Popup.close();
+    Popup.back();
   },
 });
 
@@ -839,20 +980,24 @@ Template.newUserPopup.events({
     let userTeamsIdsList = userTeamsIds.split(",");
     let userTms = [];
     for(let i = 0; i < userTeamsList.length; i++){
-      userTms.push({
-        "teamId": userTeamsIdsList[i],
-        "teamDisplayName": userTeamsList[i],
-      })
+      if(!!userTeamsIdsList[i] && !!userTeamsList[i]) {
+        userTms.push({
+          "teamId": userTeamsIdsList[i],
+          "teamDisplayName": userTeamsList[i],
+        })
+      }
     }
 
     let userOrgsList = userOrgs.split(",");
     let userOrgsIdsList = userOrgsIds.split(",");
     let userOrganizations = [];
     for(let i = 0; i < userOrgsList.length; i++){
-      userOrganizations.push({
-        "orgId": userOrgsIdsList[i],
-        "orgDisplayName": userOrgsList[i],
-      })
+      if(!!userOrgsIdsList[i] && !!userOrgsList[i]) {
+        userOrganizations.push({
+          "orgId": userOrgsIdsList[i],
+          "orgDisplayName": userOrgsList[i],
+        })
+      }
     }
 
     Meteor.call(
@@ -882,11 +1027,11 @@ Template.newUserPopup.events({
         } else {
           usernameMessageElement.hide();
           emailMessageElement.hide();
-          Popup.close();
+          Popup.back();
         }
       },
     );
-    Popup.close();
+    Popup.back();
   },
   'click #addUserOrgNewUser'(event) {
     event.preventDefault();
@@ -940,7 +1085,7 @@ Template.settingsOrgPopup.events({
       return;
     }
     Org.remove(this.orgId);
-    Popup.close();
+    Popup.back();
   }
 });
 
@@ -958,7 +1103,7 @@ Template.settingsTeamPopup.events({
       return;
     }
     Team.remove(this.teamId);
-    Popup.close();
+    Popup.back();
   }
 });
 
@@ -975,10 +1120,13 @@ Template.settingsUserPopup.events({
   },
   'click #deleteButton'(event) {
     event.preventDefault();
+    Users.remove(this.userId);
     /*
-    // Delete is not enabled yet, because it does leave empty user avatars
-    // to boards: boards members, card members and assignees have
-    // empty users. See:
+    // Delete user is enabled, but you should remove user from all boards
+    // before deleting user, because there is possibility of leaving empty user avatars
+    // to boards. You can remove non-existing user ids manually from database,
+    // if that happens.
+    //. See:
     // - wekan/client/components/settings/peopleBody.jade deleteButton
     // - wekan/client/components/settings/peopleBody.js deleteButton
     // - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
@@ -986,9 +1134,9 @@ Template.settingsUserPopup.events({
     //   but that should be used to remove user from all boards similarly
     // - wekan/models/users.js Delete is not enabled
     //
-    //Users.remove(this.userId);
+    //
     */
-    Popup.close();
+    Popup.back();
   },
 });
 

+ 29 - 0
client/components/settings/peopleBody.styl

@@ -55,3 +55,32 @@ table
 
 .js-teams,.js-teamsNewUser
   display: none;
+
+.selectUserChkBox,.allUserChkBox
+  position: static !important;
+  visibility: visible !important;
+  left: 0 !important;
+  display: block !important;
+
+#divAddOrRemoveTeam
+  background: green;
+  display: none;
+
+#addOrRemoveTeam
+  background: green;
+  color: white;
+
+#divAddOrRemoveTeamContainer
+  display: none;
+  margin: auto;
+  width: 50%;
+  border: 3px solid green;
+  padding: 10px;
+
+#cancelBtn
+  margin-left: 5% !important;
+  background: orange;
+  color: white;
+
+#deleteAction
+  margin-left: 5% !important;

+ 30 - 1
client/components/settings/settingBody.jade

@@ -22,6 +22,10 @@ template(name="setting")
               a.js-setting-menu(data-id="account-setting")
                 i.fa.fa-users
                 | {{_ 'accounts'}}
+            li
+              a.js-setting-menu(data-id="tableVisibilityMode-setting")
+                i.fa.fa-eye
+                | {{_ 'tableVisibilityMode'}}
             li
               a.js-setting-menu(data-id="announcement-setting")
                 i.fa.fa-bullhorn
@@ -44,6 +48,8 @@ template(name="setting")
               +email
           else if accountSetting.get
             +accountSettings
+          else if tableVisibilityModeSetting.get
+            +tableVisibilityModeSettings
           else if announcementSetting.get
             +announcementSettings
           else if layoutSetting.get
@@ -96,7 +102,7 @@ template(name='email')
     //  li.smtp-form
     //    .title {{_ 'smtp-username'}}
     //    .form-group
-    //      input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
+    //      input.wekan-form-control#mail-server-u"accounts-allowUserNameChange": "Allow Username Change",sername(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
     //  li.smtp-form
     //    .title {{_ 'smtp-password'}}
     //    .form-group
@@ -120,6 +126,17 @@ template(name='email')
     li
       button.js-send-smtp-test-email.primary {{_ 'send-smtp-test'}}
 
+template(name='tableVisibilityModeSettings')
+  ul#tableVisibilityMode-setting.setting-detail
+    li.tableVisibilityMode-form
+      .title {{_ 'tableVisibilityMode-allowPrivateOnly'}}
+      .form-group.flex
+        input.wekan-form-control#accounts-allowPrivateOnly(type="radio" name="allowPrivateOnly" value="true" checked="{{#if allowPrivateOnly}}checked{{/if}}")
+        span {{_ 'yes'}}
+        input.wekan-form-control#accounts-allowPrivateOnly(type="radio" name="allowPrivateOnly" value="false" checked="{{#unless allowPrivateOnly}}checked{{/unless}}")
+        span {{_ 'no'}}
+      button.js-tableVisibilityMode-save.primary {{_ 'save'}}
+
 template(name='accountSettings')
   ul#account-setting.setting-detail
     li
@@ -163,6 +180,18 @@ template(name='announcementSettings')
 
 template(name='layoutSettings')
   ul#layout-setting.setting-detail
+    li.layout-form
+      .title {{_ 'oidc-button-text'}}
+      .form-group
+        input.wekan-form-control#oidcBtnTextvalue(type="text", placeholder="" value="{{currentSetting.oidcBtnText}}")
+    li.layout-form
+      .title {{_ 'can-invite-if-same-mailDomainName'}}
+      .form-group
+        input.wekan-form-control#mailDomainNamevalue(type="text", placeholder="" value="{{currentSetting.mailDomainName}}")
+    li.layout-form
+      .title {{_ 'custom-legal-notice-link-url'}}
+      .form-group
+        input.wekan-form-control#legalNoticevalue(type="text", placeholder="" value="{{currentSetting.legalNotice}}")
     li.layout-form
       .title {{_ 'display-authentication-method'}}
       .form-group.flex

+ 62 - 0
client/components/settings/settingBody.js

@@ -7,6 +7,7 @@ BlazeComponent.extendComponent({
     this.generalSetting = new ReactiveVar(true);
     this.emailSetting = new ReactiveVar(false);
     this.accountSetting = new ReactiveVar(false);
+    this.tableVisibilityModeSetting = new ReactiveVar(false);
     this.announcementSetting = new ReactiveVar(false);
     this.layoutSetting = new ReactiveVar(false);
     this.webhookSetting = new ReactiveVar(false);
@@ -14,6 +15,7 @@ BlazeComponent.extendComponent({
     Meteor.subscribe('setting');
     Meteor.subscribe('mailServer');
     Meteor.subscribe('accountSettings');
+    Meteor.subscribe('tableVisibilityModeSettings');
     Meteor.subscribe('announcements');
     Meteor.subscribe('globalwebhooks');
   },
@@ -88,6 +90,7 @@ BlazeComponent.extendComponent({
       this.announcementSetting.set('announcement-setting' === targetID);
       this.layoutSetting.set('layout-setting' === targetID);
       this.webhookSetting.set('webhook-setting' === targetID);
+      this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
     }
   },
 
@@ -196,6 +199,22 @@ BlazeComponent.extendComponent({
     )
       .val()
       .trim();
+
+    const oidcBtnText = $(
+      '#oidcBtnTextvalue',
+    )
+      .val()
+      .trim();
+    const mailDomainName = $(
+      '#mailDomainNamevalue',
+    )
+      .val()
+      .trim();
+    const legalNotice = $(
+      '#legalNoticevalue',
+    )
+      .val()
+      .trim();
     const hideLogoChange = $('input[name=hideLogo]:checked').val() === 'true';
     const displayAuthenticationMethod =
       $('input[name=displayAuthenticationMethod]:checked').val() === 'true';
@@ -218,6 +237,9 @@ BlazeComponent.extendComponent({
           defaultAuthenticationMethod,
           automaticLinkedUrlSchemes,
           spinnerName,
+          oidcBtnText,
+          mailDomainName,
+          legalNotice,
         },
       });
     } catch (e) {
@@ -317,6 +339,46 @@ BlazeComponent.extendComponent({
   },
 }).register('accountSettings');
 
+BlazeComponent.extendComponent({
+  saveTableVisibilityChange() {
+    const allowPrivateOnly =
+      $('input[name=allowPrivateOnly]:checked').val() === 'true';
+    TableVisibilityModeSettings.update('tableVisibilityMode-allowPrivateOnly', {
+      $set: { booleanValue: allowPrivateOnly },
+    });
+  },
+  allowPrivateOnly() {
+    return TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
+  },
+  allHideSystemMessages() {
+    Meteor.call('setAllUsersHideSystemMessages', (err, ret) => {
+      if (!err && ret) {
+        if (ret === true) {
+          const message = `${TAPi18n.__(
+            'now-system-messages-of-all-users-are-hidden',
+          )}`;
+          alert(message);
+        }
+      } else {
+        const reason = err.reason || '';
+        const message = `${TAPi18n.__(err.error)}\n${reason}`;
+        alert(message);
+      }
+    });
+  },
+
+  events() {
+    return [
+      {
+        'click button.js-tableVisibilityMode-save': this.saveTableVisibilityChange,
+      },
+      {
+        'click button.js-all-hide-system-messages': this.allHideSystemMessages,
+      },
+    ];
+  },
+}).register('tableVisibilityModeSettings');
+
 BlazeComponent.extendComponent({
   onCreated() {
     this.loading = new ReactiveVar(false);

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

@@ -11,7 +11,7 @@
   color: #727479
   background: #dedede
   width 100%
-  height 100%
+  height calc(100% - 80px)
   position: absolute;
 
   .content-title
@@ -88,6 +88,8 @@
                   &.is-checked
                     background #fff
 
+          input[type=radio]
+            margin: 4px
 
 .option
   @extends .flex

+ 55 - 31
client/components/sidebar/sidebar.jade

@@ -31,26 +31,28 @@ template(name='homeSidebar')
     +activities(mode="board")
 
 template(name="membersWidget")
-  .board-widget.board-widget-members
-    h3
-      i.fa.fa-users
-      | {{_ 'organizations'}}
-
-    .board-widget-content
-      +boardOrgGeneral
-      .clearfix
-  br
-  hr
-  .board-widget.board-widget-members
-    h3
-      i.fa.fa-users
-      | {{_ 'teams'}}
-
-    .board-widget-content
-      +boardTeamGeneral
-      .clearfix
-  br
-  hr
+  if AtLeastOneOrgWasCreated
+    .board-widget.board-widget-members
+      h3
+        i.fa.fa-users
+        | {{_ 'organizations'}}
+
+      .board-widget-content
+        +boardOrgGeneral
+        .clearfix
+    br
+    hr
+  if AtLeastOneTeamWasCreated
+    .board-widget.board-widget-members
+      h3
+        i.fa.fa-users
+        | {{_ 'teams'}}
+
+      .board-widget-content
+        +boardTeamGeneral
+        .clearfix
+    br
+    hr
   .board-widget.board-widget-members
     h3
       i.fa.fa-users
@@ -89,11 +91,20 @@ template(name="boardOrgGeneral")
   table
     tbody
       tr
-        th {{_ 'displayName'}}
+        th
+          | {{_ 'add-organizations'}}
+          br
+          i.addOrganizationsLabel
+            | {{_ 'to-create-organizations-contact-admin'}}
+          br
+          i.addOrganizationsLabel
+            | {{_ 'add-organizations-label'}}
         th
           if currentUser.isBoardAdmin
             a.member.orgOrTeamMember.add-member.js-manage-board-addOrg(title="{{_ 'add-members'}}")
-              i.fa.fa-plus
+              i.addTeamFaPlus.fa.fa-plus
+            .divaddfaplusminus
+              | {{_ 'add'}}
       each org in currentBoard.activeOrgs
         +boardOrgRow(orgId=org.orgId)
 
@@ -101,11 +112,20 @@ template(name="boardTeamGeneral")
   table
     tbody
       tr
-        th {{_ 'displayName'}}
+        th
+          | {{_ 'add-teams'}}
+          br
+          i.addTeamsLabel
+            | {{_ 'to-create-teams-contact-admin'}}
+          br
+          i.addTeamsLabel
+            | {{_ 'add-teams-label'}}
         th
           if currentUser.isBoardAdmin
             a.member.orgOrTeamMember.add-member.js-manage-board-addTeam(title="{{_ 'add-members'}}")
-              i.fa.fa-plus
+              i.addTeamFaPlus.fa.fa-plus
+            .divaddfaplusminus
+              | {{_ 'add'}}
       each currentBoard.activeTeams
         +boardTeamRow(teamId=this.teamId)
 
@@ -398,7 +418,11 @@ template(name="exportBoard")
     li
       a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
         i.fa.fa-share-alt
-        | {{_ 'export-board-csv'}}
+        | {{_ 'export-board-csv'}} ,
+    li
+      a(href="{{exportScsvUrl}}", download="{{exportCsvFilename}}")
+        i.fa.fa-share-alt
+        | {{_ 'export-board-csv'}} ;
     li
       a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
         i.fa.fa-share-alt
@@ -470,12 +494,12 @@ template(name="removeBoardOrgPopup")
   form
     input.hide#hideOrgId(type="text" value=org._id)
     label
-      | {{_ 'leave-board'}} ?
+      | {{_ 'remove-organization-from-board'}}
   br
   hr
   div.buttonsContainer
-    input.primary.wide.leaveBoardBtn#leaveBoardBtn(type="submit" value="{{_ 'leave-board'}}")
-    input.primary.wide.cancelLeaveBoardBtn#cancelLeaveBoardBtn(type="submit" value="{{_ 'Cancel'}}")
+    input.primary.wide.leaveBoardBtn#leaveBoardBtn(type="submit" value="{{_ 'confirm-btn'}}")
+    input.primary.wide.cancelLeaveBoardBtn#cancelLeaveBoardBtn(type="submit" value="{{_ 'cancel'}}")
 
 template(name="addBoardTeamPopup")
   select.js-boardTeams#jsBoardTeams
@@ -487,12 +511,12 @@ template(name="removeBoardTeamPopup")
   form
     input.hide#hideTeamId(type="text" value=team._id)
     label
-      | {{_ 'leave-board'}} ?
+      | {{_ 'remove-team-from-table'}}
   br
   hr
   div.buttonsContainer
-    input.primary.wide.leaveBoardBtn#leaveBoardTeamBtn(type="submit" value="{{_ 'leave-board'}}")
-    input.primary.wide.cancelLeaveBoardBtn#cancelLeaveBoardTeamBtn(type="submit" value="{{_ 'Cancel'}}")
+    input.primary.wide.leaveBoardBtn#leaveBoardTeamBtn(type="submit" value="{{_ 'confirm-btn'}}")
+    input.primary.wide.cancelLeaveBoardBtn#cancelLeaveBoardTeamBtn(type="submit" value="{{_ 'cancel'}}")
 
 template(name="addMemberPopup")
   .js-search-member

+ 149 - 20
client/components/sidebar/sidebar.js

@@ -183,19 +183,20 @@ Template.memberPopup.helpers({
   },
 });
 
+
 Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
   'click .js-open-rules-view'() {
     Modal.openWide('rulesMain');
-    Popup.close();
+    Popup.back();
   },
   'click .js-custom-fields'() {
     Sidebar.setView('customFields');
-    Popup.close();
+    Popup.back();
   },
   'click .js-open-archives'() {
     Sidebar.setView('archives');
-    Popup.close();
+    Popup.back();
   },
   'click .js-change-board-color': Popup.open('boardChangeColor'),
   'click .js-change-language': Popup.open('changeLanguage'),
@@ -208,7 +209,7 @@ Template.boardMenuPopup.events({
   }),
   'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
-    Popup.close();
+    Popup.back();
     Boards.remove(currentBoard._id);
     FlowRouter.go('home');
   }),
@@ -251,7 +252,7 @@ Template.boardMenuPopup.helpers({
 Template.memberPopup.events({
   'click .js-filter-member'() {
     Filter.members.toggle(this.userId);
-    Popup.close();
+    Popup.back();
   },
   'click .js-change-role': Popup.open('changePermissions'),
   'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
@@ -265,12 +266,12 @@ Template.memberPopup.events({
       card.unassignAssignee(memberId);
     });
     Boards.findOne(boardId).removeMember(memberId);
-    Popup.close();
+    Popup.back();
   }),
   'click .js-leave-member': Popup.afterConfirm('leaveBoard', () => {
     const boardId = Session.get('currentBoard');
     Meteor.call('quitBoard', boardId, () => {
-      Popup.close();
+      Popup.back();
       FlowRouter.go('home');
     });
   }),
@@ -290,6 +291,42 @@ Template.leaveBoardPopup.helpers({
     return Boards.findOne(Session.get('currentBoard'));
   },
 });
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.error = new ReactiveVar('');
+    this.loading = new ReactiveVar(false);
+    this.findOrgsOptions = new ReactiveVar({});
+    this.findTeamsOptions = new ReactiveVar({});
+
+    this.page = new ReactiveVar(1);
+    this.teamPage = new ReactiveVar(1);
+    this.autorun(() => {
+      const limitOrgs = this.page.get() * Number.MAX_SAFE_INTEGER;
+      this.subscribe('org', this.findOrgsOptions.get(), limitOrgs, () => {});
+    });
+
+    this.autorun(() => {
+      const limitTeams = this.teamPage.get() * Number.MAX_SAFE_INTEGER;
+      this.subscribe('team', this.findTeamsOptions.get(), limitTeams, () => {});
+    });
+  },
+
+  onRendered() {
+    this.setLoading(false);
+  },
+
+  setError(error) {
+    this.error.set(error);
+  },
+
+  setLoading(w) {
+    this.loading.set(w);
+  },
+
+  isLoading() {
+    return this.loading.get();
+  },
+}).register('membersWidget');
 
 Template.membersWidget.helpers({
   isInvited() {
@@ -307,6 +344,21 @@ Template.membersWidget.helpers({
   isBoardAdmin() {
     return Meteor.user().isBoardAdmin();
   },
+  AtLeastOneOrgWasCreated(){
+    let orgs = Org.find({}, {sort: { createdAt: -1 }});
+    if(orgs === undefined)
+      return false;
+
+    return orgs.count() > 0;
+  },
+
+  AtLeastOneTeamWasCreated(){
+    let teams = Team.find({}, {sort: { createdAt: -1 }});
+    if(teams === undefined)
+      return false;
+
+    return teams.count() > 0;
+  },
 });
 
 Template.membersWidget.events({
@@ -408,7 +460,7 @@ BlazeComponent.extendComponent({
               activities: ['all'],
             });
           }
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -460,6 +512,21 @@ BlazeComponent.extendComponent({
     };
     const queryParams = {
       authToken: Accounts._storedLoginToken(),
+      delimiter: ',',
+    };
+    return FlowRouter.path(
+      '/api/boards/:boardId/export/csv',
+      params,
+      queryParams,
+    );
+  },
+  exportScsvUrl() {
+    const params = {
+      boardId: Session.get('currentBoard'),
+    };
+    const queryParams = {
+      authToken: Accounts._storedLoginToken(),
+      delimiter: ';',
     };
     return FlowRouter.path(
       '/api/boards/:boardId/export/csv',
@@ -1162,7 +1229,7 @@ BlazeComponent.extendComponent({
       self.setLoading(false);
       if (err) self.setError(err.error);
       else if (ret.email) self.setError('email-sent');
-      else Popup.close();
+      else Popup.back();
     });
   },
 
@@ -1249,7 +1316,7 @@ BlazeComponent.extendComponent({
             }
           }
 
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -1258,8 +1325,7 @@ BlazeComponent.extendComponent({
 
 Template.addBoardOrgPopup.helpers({
   orgsDatas() {
-    // return Org.find({}, {sort: { createdAt: -1 }});
-    let orgs = Org.find({}, {sort: { createdAt: -1 }});
+    let orgs = Org.find({}, {sort: { orgDisplayName: 1 }});
     return orgs;
   },
 });
@@ -1313,10 +1379,10 @@ BlazeComponent.extendComponent({
 
           Meteor.call('setBoardOrgs', boardOrganizations, currentBoard._id);
 
-          Popup.close();
+          Popup.back();
         },
         'click #cancelLeaveBoardBtn'(){
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -1340,6 +1406,13 @@ BlazeComponent.extendComponent({
       const limitTeams = this.page.get() * Number.MAX_SAFE_INTEGER;
       this.subscribe('team', this.findOrgsOptions.get(), limitTeams, () => {});
     });
+
+    this.findUsersOptions = new ReactiveVar({});
+    this.userPage = new ReactiveVar(1);
+    this.autorun(() => {
+      const limitUsers = this.userPage.get() * Number.MAX_SAFE_INTEGER;
+      this.subscribe('people', this.findUsersOptions.get(), limitUsers, () => {});
+    });
   },
 
   onRendered() {
@@ -1384,11 +1457,39 @@ BlazeComponent.extendComponent({
             })
 
             if (selectedTeamId != "-1") {
-              Meteor.call('setBoardTeams', boardTeams, currentBoard._id);
+              let members = currentBoard.members;
+
+              let query = {
+                "teams.teamId": { $in: boardTeams.map(t => t.teamId) },
+              };
+
+              const boardTeamUsers = Users.find(query, {
+                sort: { sort: 1 },
+              });
+
+              if(boardTeams !== undefined && boardTeams.length > 0){
+                let index;
+                if(boardTeamUsers && boardTeamUsers.count() > 0){
+                  boardTeamUsers.forEach((u) => {
+                    index = members.findIndex(function(m){ return m.userId == u._id});
+                    if(index == -1){
+                      members.push({
+                        "isActive": true,
+                        "isAdmin": u.isAdmin !== undefined ? u.isAdmin : false,
+                        "isCommentOnly" : false,
+                        "isNoComments" : false,
+                        "userId": u._id,
+                      });
+                    }
+                  });
+                }
+              }
+
+              Meteor.call('setBoardTeams', boardTeams, members, currentBoard._id);
             }
           }
 
-          Popup.close();
+          Popup.back();
         },
       },
     ];
@@ -1397,7 +1498,7 @@ BlazeComponent.extendComponent({
 
 Template.addBoardTeamPopup.helpers({
   teamsDatas() {
-    let teams = Team.find({}, {sort: { createdAt: -1 }});
+    let teams = Team.find({}, {sort: { teamDisplayName: 1 }});
     return teams;
   },
 });
@@ -1413,6 +1514,13 @@ BlazeComponent.extendComponent({
       const limitTeams = this.page.get() * Number.MAX_SAFE_INTEGER;
       this.subscribe('team', this.findOrgsOptions.get(), limitTeams, () => {});
     });
+
+    this.findUsersOptions = new ReactiveVar({});
+    this.userPage = new ReactiveVar(1);
+    this.autorun(() => {
+      const limitUsers = this.userPage.get() * Number.MAX_SAFE_INTEGER;
+      this.subscribe('people', this.findUsersOptions.get(), limitUsers, () => {});
+    });
   },
 
   onRendered() {
@@ -1449,12 +1557,33 @@ BlazeComponent.extendComponent({
             }
           }
 
-          Meteor.call('setBoardTeams', boardTeams, currentBoard._id);
+          let members = currentBoard.members;
+          let query = {
+            "teams.teamId": stringTeamId
+          };
+
+          const boardTeamUsers = Users.find(query, {
+            sort: { sort: 1 },
+          });
+
+          if(currentBoard.teams !== undefined && currentBoard.teams.length > 0){
+            let index;
+            if(boardTeamUsers && boardTeamUsers.count() > 0){
+              boardTeamUsers.forEach((u) => {
+                index = members.findIndex(function(m){ return m.userId == u._id});
+                if(index !== -1 && (u.isAdmin === undefined || u.isAdmin == false)){
+                  members.splice(index, 1);
+                }
+              });
+            }
+          }
+
+          Meteor.call('setBoardTeams', boardTeams, members, currentBoard._id);
 
-          Popup.close();
+          Popup.back();
         },
         'click #cancelLeaveBoardTeamBtn'(){
-          Popup.close();
+          Popup.back();
         },
       },
     ];

+ 21 - 0
client/components/sidebar/sidebar.styl

@@ -224,3 +224,24 @@
 .cancelLeaveBoardBtn
   margin-left: 5% !important
   background-color: red !important
+
+.addTeamsLabel, .addOrganizationsLabel
+  font-weight: normal
+
+.js-manage-board-removeTeam:hover, .js-manage-board-removeTeam.is-active,
+.js-manage-board-removeOrg:hover, .js-manage-board-removeOrg.is-active
+  box-shadow: 0 0 0 2px #e23210 inset !important
+
+.js-manage-board-addTeam:hover, .js-manage-board-addTeam.is-active,
+.js-manage-board-addOrg:hover , .js-manage-board-addOrg.is-active
+  box-shadow: 0 0 0 2px #73ea10 inset !important
+
+.addTeamFaPlus
+  color: green !important
+
+.removeTeamFaMinus
+  color: red !important
+
+.divaddfaplusminus
+  padding-top: 5px;
+  margin-left: 40px;

+ 8 - 8
client/components/sidebar/sidebarArchives.js

@@ -1,4 +1,4 @@
-archivedRequested = false;
+//archivedRequested = false;
 const subManager = new SubsManager();
 
 BlazeComponent.extendComponent({
@@ -13,7 +13,7 @@ BlazeComponent.extendComponent({
       const currentBoardId = Session.get('currentBoard');
       if (!currentBoardId) return;
       const handle = subManager.subscribe('board', currentBoardId, true);
-      archivedRequested = true;
+      //archivedRequested = true;
       Tracker.nonreactive(() => {
         Tracker.autorun(() => {
           this.isArchiveReady.set(handle.ready());
@@ -94,13 +94,13 @@ BlazeComponent.extendComponent({
         'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
           const cardId = this._id;
           Cards.remove(cardId);
-          Popup.close();
+          Popup.back();
         }),
         'click .js-delete-all-cards': Popup.afterConfirm('cardDelete', () => {
           this.archivedCards().forEach(card => {
             Cards.remove(card._id);
           });
-          Popup.close();
+          Popup.back();
         }),
 
         'click .js-restore-list'() {
@@ -115,13 +115,13 @@ BlazeComponent.extendComponent({
 
         'click .js-delete-list': Popup.afterConfirm('listDelete', function() {
           this.remove();
-          Popup.close();
+          Popup.back();
         }),
         'click .js-delete-all-lists': Popup.afterConfirm('listDelete', () => {
           this.archivedLists().forEach(list => {
             list.remove();
           });
-          Popup.close();
+          Popup.back();
         }),
 
         'click .js-restore-swimlane'() {
@@ -138,7 +138,7 @@ BlazeComponent.extendComponent({
           'swimlaneDelete',
           function() {
             this.remove();
-            Popup.close();
+            Popup.back();
           },
         ),
         'click .js-delete-all-swimlanes': Popup.afterConfirm(
@@ -147,7 +147,7 @@ BlazeComponent.extendComponent({
             this.archivedSwimlanes().forEach(swimlane => {
               swimlane.remove();
             });
-            Popup.close();
+            Popup.back();
           },
         ),
       },

+ 8 - 0
client/components/sidebar/sidebarCustomFields.jade

@@ -43,6 +43,14 @@ template(name="createCustomFieldPopup")
                   option(value=value selected="selected") {{name}}
                 else
                   option(value=value) {{name}}
+            a.flex.js-field-show-sum-at-top-of-list(class="{{#if showSumAtTopOfList}}is-checked{{/if}}")
+                .materialCheckBox(class="{{#if showSumAtTopOfList}}is-checked{{/if}}")
+                span {{_ 'showSum-field-on-list'}}
+
+        div.js-field-settings.js-field-settings-currency(class="{{#if isTypeNotSelected 'number'}}hide{{/if}}")
+            a.flex.js-field-show-sum-at-top-of-list(class="{{#if showSumAtTopOfList}}is-checked{{/if}}")
+                .materialCheckBox(class="{{#if showSumAtTopOfList}}is-checked{{/if}}")
+                span {{_ 'showSum-field-on-list'}}
 
         div.js-field-settings.js-field-settings-dropdown(class="{{#if isTypeNotSelected 'dropdown'}}hide{{/if}}")
             label

+ 12 - 2
client/components/sidebar/sidebarCustomFields.js

@@ -234,6 +234,14 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
           $target.find('.materialCheckBox').toggleClass('is-checked');
           $target.toggleClass('is-checked');
         },
+        'click .js-field-show-sum-at-top-of-list'(evt) {
+          let $target = $(evt.target);
+          if (!$target.hasClass('js-field-show-sum-at-top-of-list')) {
+            $target = $target.parent();
+          }
+          $target.find('.materialCheckBox').toggleClass('is-checked');
+          $target.toggleClass('is-checked');
+        },
         'click .primary'(evt) {
           evt.preventDefault();
 
@@ -248,6 +256,8 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
               this.find('.js-field-automatically-on-card.is-checked') !== null,
             alwaysOnCard:
               this.find('.js-field-always-on-card.is-checked') !== null,
+            showSumAtTopOfList:
+              this.find('.js-field-show-sum-at-top-of-list.is-checked') !== null,
           };
 
           // insert or update
@@ -273,7 +283,7 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({
             } else {
               CustomFields.remove(customField._id);
             }
-            Popup.close();
+            Popup.back();
           },
         ),
       },
@@ -292,6 +302,6 @@ CreateCustomFieldPopup.register('createCustomFieldPopup');
   'submit'(evt) {
     const customFieldId = this._id;
     CustomFields.remove(customFieldId);
-    Popup.close();
+    Popup.back();
   }
 });*/

+ 8 - 1
client/components/sidebar/sidebarFilters.jade

@@ -4,11 +4,18 @@
   and #each x in y constructors to fix this.
 
 template(name="filterSidebar")
-  h3 {{_ 'list-filter-label'}}
+  h3
+    i.fa.fa-trello
+    | {{_ 'list-filter-label'}}
   ul.sidebar-list
     form.js-list-filter
       input(type="text")
   hr
+  h3
+    i.fa.fa-list-alt
+    | {{_ 'filter-card-title-label'}}
+  input.js-field-card-filter(type="text")
+  hr
   h3
     i.fa.fa-tags
     | {{_ 'filter-labels-label'}}

+ 11 - 6
client/components/sidebar/sidebarFilters.js

@@ -8,6 +8,11 @@ BlazeComponent.extendComponent({
           evt.preventDefault();
           Filter.lists.set(this.find('.js-list-filter input').value.trim());
         },
+        'change .js-field-card-filter'(evt) {
+          evt.preventDefault();
+          Filter.title.set(this.find('.js-field-card-filter').value.trim());
+          Filter.resetExceptions();
+        },
         'click .js-toggle-label-filter'(evt) {
           evt.preventDefault();
           Filter.labelIds.toggle(this.currentData()._id);
@@ -94,14 +99,14 @@ BlazeComponent.extendComponent({
 }).register('filterSidebar');
 
 function mutateSelectedCards(mutationName, ...args) {
-  Cards.find(MultiSelection.getMongoSelector()).forEach(card => {
+  Cards.find(MultiSelection.getMongoSelector(), {sort: ['sort']}).forEach(card => {
     card[mutationName](...args);
   });
 }
 
 BlazeComponent.extendComponent({
   mapSelection(kind, _id) {
-    return Cards.find(MultiSelection.getMongoSelector()).map(card => {
+    return Cards.find(MultiSelection.getMongoSelector(), {sort: ['sort']}).map(card => {
       const methodName = kind === 'label' ? 'hasLabel' : 'isAssigned';
       return card[methodName](_id);
     });
@@ -171,22 +176,22 @@ Template.multiselectionSidebar.helpers({
 Template.disambiguateMultiLabelPopup.events({
   'click .js-remove-label'() {
     mutateSelectedCards('removeLabel', this._id);
-    Popup.close();
+    Popup.back();
   },
   'click .js-add-label'() {
     mutateSelectedCards('addLabel', this._id);
-    Popup.close();
+    Popup.back();
   },
 });
 
 Template.disambiguateMultiMemberPopup.events({
   'click .js-unassign-member'() {
     mutateSelectedCards('assignMember', this._id);
-    Popup.close();
+    Popup.back();
   },
   'click .js-assign-member'() {
     mutateSelectedCards('unassignMember', this._id);
-    Popup.close();
+    Popup.back();
   },
 });
 

+ 5 - 1
client/components/sidebar/sidebarSearches.jade

@@ -3,10 +3,14 @@ template(name="searchSidebar")
     input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
   .list-body
     .minilists.clearfix.js-minilists
+      hr
+      | {{_ 'lists' }}
       each (lists)
         a.minilist-wrapper.js-minilist(href=originRelativeUrl)
           +minilist(this)
     .minicards.clearfix.js-minicards
-      each (results)
+      hr
+      | {{_ 'cards' }}
+      each (cards)
         a.minicard-wrapper.js-minicard(href=originRelativeUrl)
           +minicard(this)

+ 16 - 1
client/components/sidebar/sidebarSearches.js

@@ -3,7 +3,7 @@ BlazeComponent.extendComponent({
     this.term = new ReactiveVar('');
   },
 
-  results() {
+  cards() {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     return currentBoard.searchCards(this.term.get());
   },
@@ -13,9 +13,24 @@ BlazeComponent.extendComponent({
     return currentBoard.searchLists(this.term.get());
   },
 
+  clickOnMiniCard(evt) {
+    if (Utils.isMiniScreen()) {
+      evt.preventDefault();
+      Session.set('popupCardId', this.currentData()._id);
+      this.cardDetailsPopup(evt);
+    }
+  },
+
+  cardDetailsPopup(event) {
+    if (!Popup.isOpen()) {
+      Popup.open("cardDetails")(event);
+    }
+  },
+
   events() {
     return [
       {
+        'click .js-minicard': this.clickOnMiniCard,
         'submit .js-search-term-form'(evt) {
           evt.preventDefault();
           this.term.set(evt.target.searchTerm.value);

+ 1 - 1
client/components/swimlanes/swimlaneHeader.jade

@@ -26,7 +26,7 @@ template(name="swimlaneFixedHeader")
         a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
         a.fa.fa-navicon.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
       unless isMiniScreen
-        if showDesktopDragHandles
+        if isShowDesktopDragHandles
           a.swimlane-header-handle.handle.fa.fa-arrows.js-swimlane-header-handle
       if isMiniScreen
         a.swimlane-header-miniscreen-handle.handle.fa.fa-arrows.js-swimlane-header-handle

+ 4 - 17
client/components/swimlanes/swimlaneHeader.js

@@ -28,19 +28,6 @@ BlazeComponent.extendComponent({
   },
 }).register('swimlaneHeader');
 
-Template.swimlaneHeader.helpers({
-  showDesktopDragHandles() {
-    currentUser = Meteor.user();
-    if (currentUser) {
-      return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-      return true;
-    } else {
-      return false;
-    }
-  },
-});
-
 Template.swimlaneFixedHeader.helpers({
   isBoardAdmin() {
     return Meteor.user().isBoardAdmin();
@@ -52,7 +39,7 @@ Template.swimlaneActionPopup.events({
   'click .js-close-swimlane'(event) {
     event.preventDefault();
     this.archive();
-    Popup.close();
+    Popup.back();
   },
   'click .js-move-swimlane': Popup.open('moveSwimlane'),
   'click .js-copy-swimlane': Popup.open('copySwimlane'),
@@ -101,7 +88,7 @@ BlazeComponent.extendComponent({
           // XXX ideally, we should move the popup to the newly
           // created swimlane so a user can add more than one swimlane
           // with a minimum of interactions
-          Popup.close();
+          Popup.back();
         },
         'click .js-swimlane-template': Popup.open('searchElement'),
       },
@@ -131,11 +118,11 @@ BlazeComponent.extendComponent({
         },
         'click .js-submit'() {
           this.currentSwimlane.setColor(this.currentColor.get());
-          Popup.close();
+          Popup.back();
         },
         'click .js-remove-color'() {
           this.currentSwimlane.setColor(null);
-          Popup.close();
+          Popup.back();
         },
       },
     ];

+ 3 - 1
client/components/swimlanes/swimlanes.jade

@@ -14,7 +14,8 @@ template(name="swimlane")
               +addListForm
       else
         each lists
-          +list(this)
+          if visible this
+            +list(this)
           if currentCardIsInThisList _id ../_id
             +cardDetails(currentCard)
         if currentUser.isBoardMember
@@ -52,6 +53,7 @@ template(name="addListForm")
               autocomplete="off" autofocus)
             .edit-controls.clearfix
               button.primary.confirm(type="submit") {{_ 'save'}}
+              .fa.fa-times-thin.js-close-inlined-form
               unless currentBoard.isTemplatesBoard
                 unless currentBoard.isTemplateBoard
                   span.quiet

+ 37 - 48
client/components/swimlanes/swimlanes.js

@@ -9,7 +9,7 @@ function currentListIsInThisSwimlane(swimlaneId) {
 }
 
 function currentCardIsInThisList(listId, swimlaneId) {
-  const currentCard = Cards.findOne(Session.get('currentCard'));
+  const currentCard = Utils.getCurrentCard();
   const currentUser = Meteor.user();
   if (
     currentUser &&
@@ -57,7 +57,7 @@ function initSortable(boardComponent, $listsDom) {
     tolerance: 'pointer',
     helper: 'clone',
     items: '.js-list:not(.js-list-composer)',
-    placeholder: 'list placeholder',
+    placeholder: 'js-list placeholder',
     distance: 7,
     start(evt, ui) {
       ui.placeholder.height(ui.helper.height());
@@ -95,22 +95,11 @@ function initSortable(boardComponent, $listsDom) {
   //}
 
   boardComponent.autorun(() => {
-    let showDesktopDragHandles = false;
-    currentUser = Meteor.user();
-    if (currentUser) {
-      showDesktopDragHandles = (currentUser.profile || {})
-        .showDesktopDragHandles;
-    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-      showDesktopDragHandles = true;
-    } else {
-      showDesktopDragHandles = false;
-    }
-
-    if (Utils.isMiniScreen() || showDesktopDragHandles) {
+    if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
       $listsDom.sortable({
         handle: '.js-list-handle',
       });
-    } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
+    } else {
       $listsDom.sortable({
         handle: '.js-list-header',
       });
@@ -123,7 +112,7 @@ function initSortable(boardComponent, $listsDom) {
         'disabled',
         // Disable drag-dropping when user is not member/is worker
         //!userIsMember() || Meteor.user().isWorker(),
-        !Meteor.user().isBoardAdmin(),
+        !Meteor.user() || !Meteor.user().isBoardAdmin(),
         // Not disable drag-dropping while in multi-selection mode
         // MultiSelection.isActive() || !userIsMember(),
       );
@@ -136,7 +125,7 @@ BlazeComponent.extendComponent({
     const boardComponent = this.parentComponent();
     const $listsDom = this.$('.js-lists');
 
-    if (!Session.get('currentCard')) {
+    if (!Utils.getCurrentCardId()) {
       boardComponent.scrollLeft();
     }
 
@@ -148,19 +137,38 @@ BlazeComponent.extendComponent({
     this._isDragging = false;
     this._lastDragPositionX = 0;
   },
-
   id() {
     return this._id;
   },
-
   currentCardIsInThisList(listId, swimlaneId) {
     return currentCardIsInThisList(listId, swimlaneId);
   },
-
   currentListIsInThisSwimlane(swimlaneId) {
     return currentListIsInThisSwimlane(swimlaneId);
   },
-
+  visible(list) {
+    if (list.archived) {
+      // Show archived list only when filter archive is on
+      if (!Filter.archive.isSelected()) {
+        return false;
+      }
+    }
+    if (Filter.lists._isActive()) {
+      if (!list.title.match(Filter.lists.getRegexSelector())) {
+        return false;
+      }
+    }
+    if (Filter.hideEmpty.isSelected()) {
+      const swimlaneId = this.parentComponent()
+        .parentComponent()
+        .data()._id;
+      const cards = list.cards(swimlaneId);
+      if (cards.count() === 0) {
+        return false;
+      }
+    }
+    return true;
+  },
   events() {
     return [
       {
@@ -172,19 +180,8 @@ BlazeComponent.extendComponent({
           // the user will legitimately expect to be able to select some text with
           // his mouse.
 
-          let showDesktopDragHandles = false;
-          currentUser = Meteor.user();
-          if (currentUser) {
-            showDesktopDragHandles = (currentUser.profile || {})
-              .showDesktopDragHandles;
-          } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-            showDesktopDragHandles = true;
-          } else {
-            showDesktopDragHandles = false;
-          }
-
           const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
-            Utils.isMiniScreen() || showDesktopDragHandles
+            Utils.isMiniScreenOrShowDesktopDragHandles()
               ? ['.js-list-handle', '.js-swimlane-header-handle']
               : ['.js-list-header'],
           );
@@ -240,13 +237,15 @@ BlazeComponent.extendComponent({
       {
         submit(evt) {
           evt.preventDefault();
+          const lastList = this.currentBoard.getLastList();
+          const sortIndex = Utils.calculateIndexData(lastList, null).base;
           const titleInput = this.find('.list-name-input');
           const title = titleInput.value.trim();
           if (title) {
             Lists.insert({
               title,
               boardId: Session.get('currentBoard'),
-              sort: $('.list').length,
+              sort: sortIndex,
               type: this.isListTemplatesSwimlane ? 'template-list' : 'list',
               swimlaneId: this.currentBoard.isTemplatesBoard()
                 ? this.currentSwimlane._id
@@ -264,16 +263,6 @@ BlazeComponent.extendComponent({
 }).register('addListForm');
 
 Template.swimlane.helpers({
-  showDesktopDragHandles() {
-    currentUser = Meteor.user();
-    if (currentUser) {
-      return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
-      return true;
-    } else {
-      return false;
-    }
-  },
   canSeeAddList() {
     return Meteor.user().isBoardAdmin();
     /*
@@ -291,8 +280,8 @@ BlazeComponent.extendComponent({
   },
   visible(list) {
     if (list.archived) {
-      // Show archived list only when filter archive is on or archive is selected
-      if (!(Filter.archive.isSelected() || archivedRequested)) {
+      // Show archived list only when filter archive is on
+      if (!Filter.archive.isSelected()) {
         return false;
       }
     }
@@ -316,7 +305,7 @@ BlazeComponent.extendComponent({
     const boardComponent = this.parentComponent();
     const $listsDom = this.$('.js-lists');
 
-    if (!Session.get('currentCard')) {
+    if (!Utils.getCurrentCardId()) {
       boardComponent.scrollLeft();
     }
 
@@ -359,7 +348,7 @@ class MoveSwimlaneComponent extends BlazeComponent {
             boardId = bSelect.options[bSelect.selectedIndex].value;
             Meteor.call(this.serverMethod, this.currentSwimlane._id, boardId);
           }
-          Popup.close();
+          Popup.back();
         },
       },
     ];

+ 6 - 2
client/components/users/userAvatar.jade

@@ -32,7 +32,9 @@ template(name="boardOrgRow")
     td
       if currentUser.isBoardAdmin
         a.member.orgOrTeamMember.add-member.js-manage-board-removeOrg(title="{{_ 'remove-from-board'}}")
-          i.fa.fa-minus
+          i.removeTeamFaMinus.fa.fa-minus
+        .divaddfaplusminus
+          | {{_ 'remove-btn'}}
 
 template(name="boardTeamRow")
   tr
@@ -43,7 +45,9 @@ template(name="boardTeamRow")
     td
       if currentUser.isBoardAdmin
         a.member.orgOrTeamMember.add-member.js-manage-board-removeTeam(title="{{_ 'remove-from-board'}}")
-          i.fa.fa-minus
+          i.removeTeamFaMinus.fa.fa-minus
+        .divaddfaplusminus
+          | {{_ 'remove-btn'}}
 
 template(name="boardOrgName")
   svg.avatar.avatar-initials(viewBox="0 0 {{orgViewPortWidth}} 15")

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