浏览代码

merged with wekan master @ v5.38

Stefan Maaßen 3 年之前
父节点
当前提交
cb418f5e23
共有 100 个文件被更改,包括 8449 次插入8743 次删除
  1. 12 0
      .babelrc
  2. 88 17
      .devcontainer/Dockerfile
  3. 12 7
      .devcontainer/docker-compose.yml
  4. 36 0
      .dockerignore
  5. 2 1
      .eslintrc.json
  6. 18 5
      .future-snap/broken-snapcraft.yaml
  7. 198 0
      .future-snap/old-rebuild-wekan.sh
  8. 1 1
      .future-snap/snapcraft.yaml
  9. 10 2
      .github/ISSUE_TEMPLATE.md
  10. 62 0
      .github/workflows/codeql-analysis.yml
  11. 160 0
      .github/workflows/test_suite.yml
  12. 3 1
      .gitignore
  13. 10 0
      .gitpod.Dockerfile
  14. 4 0
      .gitpod.yml
  15. 64 15
      .meteor/packages
  16. 1 1
      .meteor/release
  17. 121 69
      .meteor/versions
  18. 0 20
      .sandstorm-meteor-1.8/.meteor/.finished-upgraders
  19. 0 2
      .sandstorm-meteor-1.8/.meteor/.gitignore
  20. 0 7
      .sandstorm-meteor-1.8/.meteor/.id
  21. 0 100
      .sandstorm-meteor-1.8/.meteor/packages
  22. 0 2
      .sandstorm-meteor-1.8/.meteor/platforms
  23. 0 1
      .sandstorm-meteor-1.8/.meteor/release
  24. 0 198
      .sandstorm-meteor-1.8/.meteor/versions
  25. 0 914
      .sandstorm-meteor-1.8/cfs_access-point.txt
  26. 0 238
      .sandstorm-meteor-1.8/export.js
  27. 0 640
      .sandstorm-meteor-1.8/ldap.js
  28. 0 163
      .sandstorm-meteor-1.8/oidc_server.js
  29. 0 4361
      .sandstorm-meteor-1.8/package-lock.json
  30. 0 73
      .sandstorm-meteor-1.8/package.json
  31. 0 853
      .sandstorm-meteor-1.8/wekanCreator.js
  32. 2 2
      .travis.yml
  33. 1 1
      .tx/config
  34. 2910 0
      CHANGELOG.md
  35. 37 7
      Dockerfile
  36. 77 0
      Dockerfile.arm64v8
  37. 15 10
      README.md
  38. 3 3
      SECURITY.md
  39. 1 1
      Stackerfile.yml
  40. 291 0
      api.py
  41. 6 0
      client/00-startup.js
  42. 70 37
      client/components/activities/activities.jade
  43. 58 19
      client/components/activities/activities.js
  44. 5 1
      client/components/activities/activities.styl
  45. 1 1
      client/components/activities/comments.jade
  46. 1 0
      client/components/activities/comments.js
  47. 5 1
      client/components/boards/boardArchive.js
  48. 1 1
      client/components/boards/boardBody.jade
  49. 18 17
      client/components/boards/boardBody.js
  50. 769 1
      client/components/boards/boardColors.styl
  51. 47 12
      client/components/boards/boardHeader.jade
  52. 117 28
      client/components/boards/boardHeader.js
  53. 96 48
      client/components/boards/boardsList.jade
  54. 87 10
      client/components/boards/boardsList.js
  55. 37 5
      client/components/boards/boardsList.styl
  56. 5 5
      client/components/cards/attachments.jade
  57. 6 0
      client/components/cards/attachments.js
  58. 47 2
      client/components/cards/cardCustomFields.jade
  59. 140 0
      client/components/cards/cardCustomFields.js
  60. 4 0
      client/components/cards/cardDate.jade
  61. 89 94
      client/components/cards/cardDate.js
  62. 9 5
      client/components/cards/cardDate.styl
  63. 7 0
      client/components/cards/cardDescription.jade
  64. 34 0
      client/components/cards/cardDescription.js
  65. 59 0
      client/components/cards/cardDescription.styl
  66. 650 299
      client/components/cards/cardDetails.jade
  67. 779 184
      client/components/cards/cardDetails.js
  68. 220 18
      client/components/cards/cardDetails.styl
  69. 22 9
      client/components/cards/checklists.jade
  70. 46 12
      client/components/cards/checklists.js
  71. 28 1
      client/components/cards/checklists.styl
  72. 13 0
      client/components/cards/labels.styl
  73. 40 5
      client/components/cards/minicard.jade
  74. 47 7
      client/components/cards/minicard.js
  75. 20 4
      client/components/cards/minicard.styl
  76. 44 0
      client/components/cards/resultCard.jade
  77. 11 0
      client/components/cards/resultCard.js
  78. 24 0
      client/components/cards/resultCard.styl
  79. 10 9
      client/components/cards/subtasks.jade
  80. 12 3
      client/components/cards/subtasks.js
  81. 6 6
      client/components/forms/forms.styl
  82. 37 0
      client/components/import/csvMembersMapper.js
  83. 32 30
      client/components/import/import.jade
  84. 77 21
      client/components/import/import.js
  85. 4 0
      client/components/import/import.styl
  86. 1 1
      client/components/import/trelloMembersMapper.js
  87. 1 1
      client/components/import/wekanMembersMapper.js
  88. 20 32
      client/components/lists/list.js
  89. 5 7
      client/components/lists/list.styl
  90. 9 12
      client/components/lists/listBody.jade
  91. 65 48
      client/components/lists/listBody.js
  92. 14 13
      client/components/lists/listHeader.jade
  93. 58 7
      client/components/lists/listHeader.js
  94. 17 0
      client/components/main/brokenCards.jade
  95. 18 0
      client/components/main/brokenCards.js
  96. 31 0
      client/components/main/brokenCards.styl
  97. 57 0
      client/components/main/dueCards.jade
  98. 111 0
      client/components/main/dueCards.js
  99. 4 0
      client/components/main/dueCards.styl
  100. 59 12
      client/components/main/editor.js

+ 12 - 0
.babelrc

@@ -0,0 +1,12 @@
+{ 
+  "presets": [ 
+    "@babel/preset-stage-3" 
+  ],
+  "env": {
+    "COVERAGE": {
+      "plugins": [
+        "istanbul"
+      ]
+    }
+  }
+}

+ 88 - 17
.devcontainer/Dockerfile

@@ -1,13 +1,13 @@
-FROM ubuntu:disco
+FROM quay.io/wekan/ubuntu:groovy-20210115
 LABEL maintainer="sgr"
 LABEL maintainer="sgr"
 
 
-ENV BUILD_DEPS="gnupg gosu bsdtar wget curl bzip2 g++ build-essential python git ca-certificates iproute2"
+ENV BUILD_DEPS="gnupg gosu libarchive-tools wget curl bzip2 g++ build-essential python git ca-certificates iproute2"
 ENV DEBIAN_FRONTEND=noninteractive
 ENV DEBIAN_FRONTEND=noninteractive
 
 
 ENV \
 ENV \
     DEBUG=false \
     DEBUG=false \
-    NODE_VERSION=8.17.0 \
-    METEOR_RELEASE=1.8.1 \
+    NODE_VERSION=v12.22.3 \
+    METEOR_RELEASE=1.10.2 \
     USE_EDGE=false \
     USE_EDGE=false \
     METEOR_EDGE=1.5-beta.17 \
     METEOR_EDGE=1.5-beta.17 \
     NPM_VERSION=latest \
     NPM_VERSION=latest \
@@ -15,16 +15,20 @@ ENV \
     ARCHITECTURE=linux-x64 \
     ARCHITECTURE=linux-x64 \
     SRC_PATH=./ \
     SRC_PATH=./ \
     WITH_API=true \
     WITH_API=true \
+    RESULTS_PER_PAGE="" \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
-    RICHER_CARD_COMMENT_EDITOR=true \
+    RICHER_CARD_COMMENT_EDITOR=false \
+    CARD_OPENED_WEBHOOK_ENABLED=false \
+    ATTACHMENTS_STORE_PATH="" \
     MAX_IMAGE_PIXEL="" \
     MAX_IMAGE_PIXEL="" \
     IMAGE_COMPRESS_RATIO="" \
     IMAGE_COMPRESS_RATIO="" \
-    BIGEVENTS_PATTERN="" \
+    NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
+    BIGEVENTS_PATTERN=NONE \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     NOTIFY_DUE_AT_HOUR_OF_DAY="" \
     NOTIFY_DUE_AT_HOUR_OF_DAY="" \
     EMAIL_NOTIFICATION_TIMEOUT=30000 \
     EMAIL_NOTIFICATION_TIMEOUT=30000 \
@@ -36,6 +40,8 @@ ENV \
     TRUSTED_URL="" \
     TRUSTED_URL="" \
     WEBHOOKS_ATTRIBUTES="" \
     WEBHOOKS_ATTRIBUTES="" \
     OAUTH2_ENABLED=false \
     OAUTH2_ENABLED=false \
+    OAUTH2_CA_CERT="" \
+    OAUTH2_ADFS_ENABLED=false \
     OAUTH2_LOGIN_STYLE=redirect \
     OAUTH2_LOGIN_STYLE=redirect \
     OAUTH2_CLIENT_ID="" \
     OAUTH2_CLIENT_ID="" \
     OAUTH2_SECRET="" \
     OAUTH2_SECRET="" \
@@ -108,23 +114,40 @@ ENV \
     CORS="" \
     CORS="" \
     CORS_ALLOW_HEADERS="" \
     CORS_ALLOW_HEADERS="" \
     CORS_EXPOSE_HEADERS="" \
     CORS_EXPOSE_HEADERS="" \
-    DEFAULT_AUTHENTICATION_METHOD=""
+    DEFAULT_AUTHENTICATION_METHOD="" \
+    PASSWORD_LOGIN_ENABLED=true \
+    CAS_ENABLED=false \
+    CAS_BASE_URL="" \
+    CAS_LOGIN_URL="" \
+    CAS_VALIDATE_URL="" \
+    SAML_ENABLED=false \
+    SAML_PROVIDER="" \
+    SAML_ENTRYPOINT="" \
+    SAML_ISSUER="" \
+    SAML_CERT="" \
+    SAML_IDPSLO_REDIRECTURL="" \
+    SAML_PRIVATE_KEYFILE="" \
+    SAML_PUBLIC_CERTFILE="" \
+    SAML_IDENTIFIER_FORMAT="" \
+    SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE="" \
+    SAML_ATTRIBUTES="" \
+    DEFAULT_WAIT_SPINNER=""
 
 
 # Install OS
 # Install OS
 RUN set -o xtrace \
 RUN set -o xtrace \
   && useradd --user-group -m --system --home-dir /home/wekan wekan \
   && useradd --user-group -m --system --home-dir /home/wekan wekan \
   && apt-get update \
   && apt-get update \
-	&& apt-get install --assume-yes --no-install-recommends apt-utils apt-transport-https ca-certificates 2>&1 \
-	&& apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
+  && apt-get install --assume-yes --no-install-recommends apt-utils apt-transport-https ca-certificates 2>&1 \
+  && apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
 
 
 # Install NodeJS
 # Install NodeJS
 RUN set -o xtrace \
 RUN set -o xtrace \
   && cd /tmp \
   && cd /tmp \
-  && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-$ARCHITECTURE.tar.xz" \
-  && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
-  && grep " node-v$NODE_VERSION-$ARCHITECTURE.tar.xz\$" SHASUMS256.txt.asc | sha256sum -c - \
-  && tar -xJf "node-v$NODE_VERSION-$ARCHITECTURE.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
-  && rm "node-v$NODE_VERSION-$ARCHITECTURE.tar.xz" SHASUMS256.txt.asc \
+  && curl -fsSLO --compressed "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$ARCHITECTURE.tar.xz" \
+  && curl -fsSLO --compressed "https://nodejs.org/dist/$NODE_VERSION/SHASUMS256.txt.asc" \
+  && grep " node-$NODE_VERSION-$ARCHITECTURE.tar.xz\$" SHASUMS256.txt.asc | sha256sum -c - \
+  && tar -xJf "node-$NODE_VERSION-$ARCHITECTURE.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
+  && rm "node-$NODE_VERSION-$ARCHITECTURE.tar.xz" SHASUMS256.txt.asc \
   && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
   && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
   && mkdir -p /usr/local/lib/node_modules/fibers/.node-gyp /root/.node-gyp/${NODE_VERSION} /home/wekan/.config \
   && mkdir -p /usr/local/lib/node_modules/fibers/.node-gyp /root/.node-gyp/${NODE_VERSION} /home/wekan/.config \
   && npm install -g npm@${NPM_VERSION} \
   && npm install -g npm@${NPM_VERSION} \
@@ -146,17 +169,65 @@ RUN set -o xtrace \
 
 
 ENV PATH=$PATH:/home/wekan/.meteor/
 ENV PATH=$PATH:/home/wekan/.meteor/
 
 
-# Copy source dir
 USER root
 USER root
 
 
 RUN echo "export PATH=$PATH" >> /etc/environment
 RUN echo "export PATH=$PATH" >> /etc/environment
 
 
+USER wekan
+
+# Copy source dir
 RUN set -o xtrace \
 RUN set -o xtrace \
-  && mkdir /home/wekan/app
+  && mkdir -p /home/wekan/app/.meteor \
+  && mkdir -p /home/wekan/app/packages
 
 
-COPY ${SRC_PATH} /home/wekan/app/
+COPY \
+    .meteor/.finished-upgraders \
+    .meteor/.id \
+    .meteor/cordova-plugins \
+    .meteor/packages \
+    .meteor/platforms \
+    .meteor/release \
+    .meteor/versions \
+    /home/wekan/app/.meteor/
+
+COPY \
+    package.json \
+    settings.json \
+    /home/wekan/app/
+
+COPY \
+    packages \
+    /home/wekan/app/packages/
+
+USER root
 
 
 RUN set -o xtrace \
 RUN set -o xtrace \
   && chown -R wekan:wekan /home/wekan/app /home/wekan/.meteor
   && chown -R wekan:wekan /home/wekan/app /home/wekan/.meteor
 
 
 USER wekan
 USER wekan
+
+RUN \
+    set -o xtrace && \
+    sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js && \
+    cd /home/wekan/.meteor && \
+    /home/wekan/.meteor/meteor -- help;
+
+RUN \
+    set -o xtrace && \
+    # Build app
+    cd /home/wekan/app && \
+    /home/wekan/.meteor/meteor add standard-minifier-js && \
+    /home/wekan/.meteor/meteor npm install && \
+    /home/wekan/.meteor/meteor build --directory /home/wekan/app_build
+
+RUN \
+    set -o xtrace && \
+    cd /home/wekan/app_build/bundle/programs/server/ && \
+    chmod u+w package.json npm-shrinkwrap.json && \
+    npm install
+
+ENV PORT=3000
+EXPOSE $PORT
+WORKDIR /home/wekan/app
+
+CMD ["/home/wekan/.meteor/meteor", "run", "--verbose", "--settings", "settings.json"]

+ 12 - 7
.devcontainer/docker-compose.yml

@@ -3,17 +3,18 @@ version: '3.7'
 services:
 services:
 
 
   wekandb-dev:
   wekandb-dev:
-    image: mongo:4.0.12
+    image: mongo:4.4
     container_name: wekan-dev-db
     container_name: wekan-dev-db
     restart: unless-stopped
     restart: unless-stopped
-    command: mongod --smallfiles --oplogSize 128
+    command: mongod --oplogSize 128
     networks:
     networks:
       - wekan-dev-tier
       - wekan-dev-tier
     expose:
     expose:
       - 27017
       - 27017
     volumes:
     volumes:
-      - wekan-dev-db:/data/db
-      - wekan-dev-db-dump:/dump
+      - ./volumes/wekan-db:/data/db
+      - ./volumes/wekan-db-dump:/dump
+      - /etc/localtime:/etc/localtime:ro
 
 
   wekan-dev:
   wekan-dev:
     container_name: wekan-dev-app
     container_name: wekan-dev-app
@@ -35,9 +36,13 @@ services:
     depends_on:
     depends_on:
       - wekandb-dev
       - wekandb-dev
     volumes:
     volumes:
-      - ..:/app:delegated
-    command:
-      sleep infinity
+      - ../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:
 volumes:
   wekan-dev-db:
   wekan-dev-db:

+ 36 - 0
.dockerignore

@@ -0,0 +1,36 @@
+*~
+*.swp
+.meteor-spk
+*.sublime-workspace
+tmp/
+node_modules/
+npm-debug.log
+.gitmodules
+.vscode/
+.idea/
+.build/*
+**/parts/
+**/stage
+**/prime
+**/*.snap
+snap/.snapcraft/
+.idea
+.DS_Store
+.DS_Store?
+.build*
+*.browserify.js.cached
+*.browserify.js.map
+.build*
+versions.json
+.versions
+.npm
+.build*
+._*
+.Trashes
+Thumbs.db
+ehthumbs.db
+.eslintcache
+.meteor/local
+.devcontainer/docker-compose.extend.yml
+.devcontainer/volumes*/
+.git

+ 2 - 1
.eslintrc.json

@@ -11,6 +11,7 @@
     "browser": true,
     "browser": true,
     "meteor": true
     "meteor": true
   },
   },
+  "parser": "babel-eslint",
   "parserOptions": {
   "parserOptions": {
     "ecmaVersion": 2018,
     "ecmaVersion": 2018,
     "sourceType": "module"
     "sourceType": "module"
@@ -44,7 +45,7 @@
     "no-spaced-func": 2,
     "no-spaced-func": 2,
     "no-trailing-spaces": 2,
     "no-trailing-spaces": 2,
     "operator-linebreak": 2,
     "operator-linebreak": 2,
-    "quotes": [2, "single"],
+    "quotes": [2, "single", { "avoidEscape": true }],
     "semi-spacing": 2,
     "semi-spacing": 2,
     "space-unary-ops": 2,
     "space-unary-ops": 2,
     "arrow-spacing": 2,
     "arrow-spacing": 2,

+ 18 - 5
.sandstorm-meteor-1.8/snapcraft.yaml → .future-snap/broken-snapcraft.yaml

@@ -65,9 +65,9 @@ apps:
 
 
 parts:
 parts:
     mongodb:
     mongodb:
-        source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
+        source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.6.tgz
         plugin: dump
         plugin: dump
-        stage-packages: [libssl1.0.0]
+        stage-packages: [libssl1.0.0, libcurl3]
         filesets:
         filesets:
             mongo:
             mongo:
                 - usr
                 - usr
@@ -81,19 +81,20 @@ parts:
     wekan:
     wekan:
         source: .
         source: .
         plugin: nodejs
         plugin: nodejs
-        node-engine: 8.17.0
+        node-engine: 12.22.3
         node-packages:
         node-packages:
             - node-gyp
             - node-gyp
             - node-pre-gyp
             - node-pre-gyp
-            - fibers@2.0.0
+            - fibers
         build-packages:
         build-packages:
             - ca-certificates
             - ca-certificates
             - apt-utils
             - apt-utils
             - python
             - python
-#            - python3
+            - python3
             - g++
             - g++
             - capnproto
             - capnproto
             - curl
             - curl
+            - libcurl3
             - execstack
             - execstack
             - nodejs
             - nodejs
             - npm
             - npm
@@ -104,6 +105,18 @@ parts:
             rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
             rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
             # Create the OpenAPI specification
             # Create the OpenAPI specification
             rm -rf .build
             rm -rf .build
+            ## Use Meteor 1.8.x on Snap
+            #rm -rf .meteor
+            #mv .snap-meteor-1.8/.meteor .
+            #mv .snap-meteor-1.8/package.json .
+            #mv .snap-meteor-1.8/package-lock.json .
+            ## Meteor 1.9.x has changes to Buffer() => Buffer.alloc(), so reverting those
+            #mv .snap-meteor-1.8/cfs_access-point.txt fix-download-unicode/
+            #mv .snap-meteor-1.8/export.js models/
+            #mv .snap-meteor-1.8/wekanCreator.js models/
+            #mv .snap-meteor-1.8/ldap.js packages/wekan-ldap/server/ldap.js
+            #mv .snap-meteor-1.8/oidc_server.js packages/wekan-oidc/oidc_server.js
+            rm -rf .snap-meteor-1.8
             #mkdir -p .build/python
             #mkdir -p .build/python
             #cd .build/python
             #cd .build/python
             #git clone --depth 1 -b master https://github.com/Kronuz/esprima-python
             #git clone --depth 1 -b master https://github.com/Kronuz/esprima-python

+ 198 - 0
.future-snap/old-rebuild-wekan.sh

@@ -0,0 +1,198 @@
+#!/bin/bash
+
+echo "Note: If you use other locale than en_US.UTF-8 , you need to additionally install en_US.UTF-8"
+echo "      with 'sudo dpkg-reconfigure locales' , so that MongoDB works correctly."
+echo "      You can still use any other locale as your main locale."
+
+#Below script installs newest node 8.x for Debian/Ubuntu/Mint.
+#NODE_VERSION=12.21.0
+#X64NODE="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz"
+
+function pause(){
+	read -p "$*"
+}
+
+function cprec(){
+	if [[ -d "$1" ]]; then
+		if [[ ! -d "$2" ]]; then
+			sudo mkdir -p "$2"
+		fi
+
+		for i in $(ls -A "$1"); do
+			cprec "$1/$i" "$2/$i"
+		done
+	else
+		sudo cp "$1" "$2"
+	fi
+}
+
+# sudo npm doesn't work right, so this is a workaround
+function npm_call(){
+	TMPDIR="/tmp/tmp_npm_prefix"
+	if [[ -d "$TMPDIR" ]]; then
+		rm -rf $TMPDIR
+	fi
+	mkdir $TMPDIR
+	NPM_PREFIX="$(npm config get prefix)"
+	npm config set prefix $TMPDIR
+	npm "$@"
+	npm config set prefix "$NPM_PREFIX"
+
+	echo "Moving files to $NPM_PREFIX"
+	for i in $(ls -A $TMPDIR); do
+		cprec "$TMPDIR/$i" "$NPM_PREFIX/$i"
+	done
+	rm -rf $TMPDIR
+}
+
+#function wekan_repo_check(){
+## UNCOMMENTING, IT'S NOT REQUIRED THAT /HOME/USERNAME IS /HOME/WEKAN
+#	git_remotes="$(git remote show 2>/dev/null)"
+#	res=""
+#	for i in $git_remotes; do
+#		res="$(git remote get-url $i | sed 's/.*wekan\/wekan.*/wekan\/wekan/')"
+#		if [[ "$res" == "wekan/wekan" ]]; then
+#		    break
+#		fi
+#	done
+#
+#	if [[ "$res" != "wekan/wekan" ]]; then
+#		echo "$PWD is not a wekan repository"
+#		exit;
+#	fi
+#}
+
+echo
+PS3='Please enter your choice: '
+options=("Install Wekan dependencies" "Build Wekan" "Run Meteor for dev on http://localhost:4000" "Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000" "Run Meteor for dev on http://CUSTOM-IP-ADDRESS:PORT" "Quit")
+
+select opt in "${options[@]}"
+do
+    case $opt in
+        "Install Wekan dependencies")
+
+		if [[ "$OSTYPE" == "linux-gnu" ]]; then
+	                echo "Linux";
+			# Debian, Ubuntu, Mint
+			sudo apt-get install -y build-essential gcc g++ make git curl wget
+			# npm nodejs
+			#sudo npm -g install npm
+			curl -0 -L https://npmjs.org/install.sh | sudo sh
+			sudo chown -R $(id -u):$(id -g) $HOME/.npm
+			sudo npm -g install n
+			sudo n 12.21.0
+			#curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
+			#sudo apt-get install -y nodejs
+		elif [[ "$OSTYPE" == "darwin"* ]]; then
+		        echo "macOS";
+			pause '1) Install XCode 2) Install Node 8.x from https://nodejs.org/en/ 3) Press [Enter] key to continue.'
+		elif [[ "$OSTYPE" == "cygwin" ]]; then
+		        # POSIX compatibility layer and Linux environment emulation for Windows
+		        echo "TODO: Add Cygwin";
+			exit;
+		elif [[ "$OSTYPE" == "msys" ]]; then
+		        # Lightweight shell and GNU utilities compiled for Windows (part of MinGW)
+		        echo "TODO: Add msys on Windows";
+			exit;
+		elif [[ "$OSTYPE" == "win32" ]]; then
+		        # I'm not sure this can happen.
+		        echo "TODO: Add Windows";
+			exit;
+		elif [[ "$OSTYPE" == "freebsd"* ]]; then
+		        echo "TODO: Add FreeBSD";
+			exit;
+		else
+		        echo "Unknown"
+			echo ${OSTYPE}
+			exit;
+		fi
+
+		## Latest npm with Meteor 1.8.x
+		npm_call -g install npm
+		npm_call -g install node-gyp
+		# Latest fibers for Meteor 1.8.x
+		sudo mkdir -p /usr/local/lib/node_modules/fibers/.node-gyp
+		npm_call -g install fibers
+		# Install Meteor, if it's not yet installed
+		curl https://install.meteor.com | bash
+		sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
+		break
+		;;
+
+    "Build Wekan")
+		echo "Building Wekan."
+		#wekan_repo_check
+		# REPOS BELOW ARE INCLUDED TO WEKAN REPO
+		#rm -rf packages/kadira-flow-router packages/meteor-useraccounts-core packages/meteor-accounts-cas packages/wekan-ldap packages/wekan-ldap packages/wekan-scrfollbar packages/meteor-accounts-oidc packages/markdown
+		#mkdir packages
+		#cd packages
+		#git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router
+		#git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core
+		#git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git
+		#git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git
+		#git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git
+		#git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git
+		#git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git
+                #mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan_accounts-oidc
+                #mv meteor-accounts-oidc/packages/switch_oidc wekan_oidc
+                #rm -rf meteor-accounts-oidc
+		#if [[ "$OSTYPE" == "darwin"* ]]; then
+		#	echo "sed at macOS";
+		#	sed -i '' 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js
+		#else
+		#	echo "sed at ${OSTYPE}"
+		#	sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js
+		#fi
+		#cd ..
+		sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
+		rm -rf node_modules .meteor/local
+		npm install
+		rm -rf .build
+		meteor build .build --directory
+		cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
+		# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
+		rm -rf .build/bundle/programs/web.browser.legacy
+		#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
+		#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
+		#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
+		#cd ~/repos/wekan/.build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt
+		#rm -rf node_modules/bcrypt
+		#meteor npm install bcrypt
+		cd .build/bundle/programs/server
+		rm -rf node_modules
+		npm install
+		#meteor npm install bcrypt
+		cd ../../../..
+		echo Done.
+		break
+		;;
+
+    "Run Meteor for dev on http://localhost:4000")
+		WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000
+		break
+		;;
+
+    "Run Meteor for dev on http://CURRENT-IP-ADDRESS:4000")
+		IPADDRESS=$(ip a | grep 'noprefixroute' | grep 'inet ' | cut -d: -f2 | awk '{ print $2}' | cut -d '/' -f 1)
+		echo "Your IP address is $IPADDRESS"
+		WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000
+		break
+		;;
+
+    "Run Meteor for dev on http://CUSTOM-IP-ADDRESS:PORT")
+		ip address
+		echo "From above list, what is your IP address?"
+		read IPADDRESS
+		echo "On what port you would like to run Wekan?"
+		read PORT
+		echo "ROOT_URL=http://$IPADDRESS:$PORT"
+    WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:$PORT meteor run --exclude-archs web.browser.legacy,web.cordova --port $PORT
+		break
+    ;;
+
+    "Quit")
+		break
+    ;;
+    *) echo invalid option;;
+    esac
+done

+ 1 - 1
.sandstorm-meteor-1.8/future/snapcraft.yaml → .future-snap/snapcraft.yaml

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

+ 10 - 2
.github/ISSUE_TEMPLATE.md

@@ -1,8 +1,16 @@
 ## Issue
 ## Issue
 
 
+Note: With Docker, please don't use latest tag. Only use release tags.
+See https://github.com/wekan/wekan/issues/3874
+
+If you can not login for any reason:
+- https://github.com/wekan/wekan/wiki/Forgot-Password
+
+Email settings:
+- https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
+
 Add these issues to elsewhere:
 Add these issues to elsewhere:
-- Snap: https://github.com/wekan/wekan-snap/issues
-- LDAP: https://github.com/wekan/wekan-ldap/issues
+- SECURITY ISSUES: https://github.com/wekan/wekan/blob/master/SECURITY.md
 - UCS: https://github.com/wekan/univention/issues
 - UCS: https://github.com/wekan/univention/issues
 
 
 Other Wekan issues can be added here.
 Other Wekan issues can be added here.

+ 62 - 0
.github/workflows/codeql-analysis.yml

@@ -0,0 +1,62 @@
+name: "CodeQL"
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [master]
+  schedule:
+    - cron: '0 16 * * 3'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+
+    strategy:
+      fail-fast: false
+      matrix:
+        # Override automatic language detection by changing the below list
+        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
+        language: ['javascript', 'python']
+        # Learn more...
+        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+      with:
+        # We must fetch at least the immediate parents so that if this is
+        # a pull request then we can checkout the head.
+        fetch-depth: 2
+
+    # If this run was triggered by a pull request event, then checkout
+    # the head of the pull request instead of the merge commit.
+    - run: git checkout HEAD^2
+      if: ${{ github.event_name == 'pull_request' }}
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1

+ 160 - 0
.github/workflows/test_suite.yml

@@ -0,0 +1,160 @@
+name: Test suite
+
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+
+jobs:
+# the following are optional jobs and need to be configured according
+# to this project's settings:
+#
+#  lintcode:
+#    name: Javascript lint
+#    runs-on: ubuntu-latest
+#    steps:
+#    - name: checkout
+#      uses: actions/checkout@v2
+#
+#    - name: setup node
+#      uses: actions/setup-node@v1
+#      with:
+#        node-version: '12.x'
+#
+#    - name: cache dependencies
+#      uses: actions/cache@v1
+#      with:
+#        path: ~/.npm
+#        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+#        restore-keys: |
+#          ${{ runner.os }}-node-
+#
+#    - run: npm install
+#    - run: npm run lint:code
+#
+#  lintstyle:
+#    name: SCSS lint
+#    runs-on: ubuntu-latest
+#    needs: [lintcode]
+#    steps:
+#    - name: checkout
+#      uses: actions/checkout@v2
+#
+#    - name: setup node
+#      uses: actions/setup-node@v1
+#      with:
+#        node-version: '12.x'
+#
+#    - name: cache dependencies
+#      uses: actions/cache@v1
+#      with:
+#        path: ~/.npm
+#        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+#        restore-keys: |
+#          ${{ runner.os }}-node-
+#    - run: npm install
+#    - run: npm run lint:style
+#
+#  lintdocs:
+#    name: documentation lint
+#    runs-on: ubuntu-latest
+#    needs: [lintcode,lintstyle]
+#    steps:
+#    - name: checkout
+#      uses: actions/checkout@v2
+#
+#    - name: setup node
+#      uses: actions/setup-node@v1
+#      with:
+#        node-version: '12.x'
+#
+#    - name: cache dependencies
+#      uses: actions/cache@v1
+#      with:
+#        path: ~/.npm
+#        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+#        restore-keys: |
+#          ${{ runner.os }}-node-
+#
+#    - run: npm install
+#    - run: npm run lint:markdown
+
+  tests:
+    name: Meteor ${{ matrix.meteor }} tests
+    runs-on: ubuntu-latest
+    steps:
+
+      # CHECKOUTS
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      # CACHING
+      - name: Install Meteor
+        id: cache-meteor-install
+        uses: actions/cache@v2
+        with:
+          path: ~/.meteor
+          key: v1-meteor-${{ hashFiles('.meteor/versions') }}
+          restore-keys: |
+                v1-meteor-
+
+      - name: Cache NPM dependencies
+        id: cache-meteor-npm
+        uses: actions/cache@v2
+        with:
+          path: ~/.npm
+          key: v1-npm-${{ hashFiles('package-lock.json') }}
+          restore-keys: |
+                v1-npm-
+
+      - name: Cache Meteor build
+        id: cache-meteor-build
+        uses: actions/cache@v2
+        with:
+          path: |
+            .meteor/local/resolver-result-cache.json
+            .meteor/local/plugin-cache
+            .meteor/local/isopacks
+            .meteor/local/bundler-cache/scanner
+          key: v1-meteor_build_cache-${{ github.ref }}-${{ github.sha }}
+          restore-key: |
+            v1-meteor_build_cache-
+
+      - name: Setup meteor
+        uses: meteorengineer/setup-meteor@v1
+        with:
+          meteor-release: '2.2'
+
+      - name: Install NPM Dependencies
+        run: meteor npm ci
+
+      - name: Run Tests
+        run: sh ./test-wekan.sh -cv
+
+      - name: Upload coverage
+        uses: actions/upload-artifact@v2
+        with:
+          name: coverage-folder
+          path: .coverage/
+
+  coverage:
+    name: Coverage report
+    runs-on: ubuntu-latest
+    needs: [tests]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Download coverage
+        uses: actions/download-artifact@v2
+        with:
+          name: coverage-folder
+          path: .coverage/
+
+
+      - name: Coverage Report
+        uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
+        with:
+          path: ".coverage/lcov.info"
+          min_coverage: 1 # TODO add tests and increase to 95!

+ 3 - 1
.gitignore

@@ -5,6 +5,7 @@
 tmp/
 tmp/
 node_modules/
 node_modules/
 npm-debug.log
 npm-debug.log
+.gitmodules
 .vscode/
 .vscode/
 .idea/
 .idea/
 .build/*
 .build/*
@@ -30,5 +31,6 @@ Thumbs.db
 ehthumbs.db
 ehthumbs.db
 .eslintcache
 .eslintcache
 .meteor/local
 .meteor/local
-.meteor-1.6-snap/.meteor/local
 .devcontainer/docker-compose.extend.yml
 .devcontainer/docker-compose.extend.yml
+.devcontainer/volumes*/
+.coverage

+ 10 - 0
.gitpod.Dockerfile

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

+ 4 - 0
.gitpod.yml

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

+ 64 - 15
.meteor/packages

@@ -6,8 +6,8 @@
 meteor-base@1.4.0
 meteor-base@1.4.0
 
 
 # Build system
 # Build system
-ecmascript@0.14.2
-standard-minifier-css@1.6.0
+ecmascript@0.15.1
+standard-minifier-css@1.7.2
 standard-minifier-js@2.6.0
 standard-minifier-js@2.6.0
 mquandalle:jade
 mquandalle:jade
 coffeescript@2.4.1!
 coffeescript@2.4.1!
@@ -17,13 +17,13 @@ es5-shim@4.8.0
 
 
 # Collections
 # Collections
 aldeed:collection2
 aldeed:collection2
-cfs:standard-packages
+wekan-cfs-standard-packages
 cottz:publish-relations
 cottz:publish-relations
 dburles:collection-helpers
 dburles:collection-helpers
 idmontie:migrations
 idmontie:migrations
 matb33:collection-hooks
 matb33:collection-hooks
 matteodem:easy-search
 matteodem:easy-search
-mongo@1.9.0
+mongo@1.11.0
 mquandalle:collection-mutations
 mquandalle:collection-mutations
 
 
 # Account system
 # Account system
@@ -70,24 +70,19 @@ rajit:bootstrap3-datepicker
 shell-server@0.5.0
 shell-server@0.5.0
 simple:rest-accounts-password
 simple:rest-accounts-password
 useraccounts:core
 useraccounts:core
-email@1.2.3
+email@2.0.0
 horka:swipebox
 horka:swipebox
-dynamic-import@0.5.1
-staringatlights:fast-render
+dynamic-import@0.6.0
 
 
-accounts-password@1.6.0
-cfs:gridfs
+accounts-password@1.7.0
+wekan-cfs-gridfs
 rzymek:fullcalendar
 rzymek:fullcalendar
 momentjs:moment@2.22.2
 momentjs:moment@2.22.2
 browser-policy-framing@1.1.0
 browser-policy-framing@1.1.0
 mquandalle:moment
 mquandalle:moment
 msavin:usercache
 msavin:usercache
-wekan-scrollbar
-mquandalle:perfect-scrollbar
-mdg:meteor-apm-agent@3.2.0-rc.0!
 # Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
 # Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
 coagmano:stylus@1.1.0!
 coagmano:stylus@1.1.0!
-lucasantoniassi:accounts-lockout
 meteorhacks:subs-manager
 meteorhacks:subs-manager
 meteorhacks:picker
 meteorhacks:picker
 lamhieu:unblock
 lamhieu:unblock
@@ -95,6 +90,60 @@ meteorhacks:aggregate@1.3.0
 wekan-markdown
 wekan-markdown
 konecty:mongo-counter
 konecty:mongo-counter
 percolate:synced-cron
 percolate:synced-cron
+wekan-cfs-filesystem
+steffo:meteor-accounts-saml
+rajit:bootstrap3-datepicker-fi
+rajit:bootstrap3-datepicker-ar
+rajit:bootstrap3-datepicker-bg
+rajit:bootstrap3-datepicker-br
+rajit:bootstrap3-datepicker-ca
+rajit:bootstrap3-datepicker-cs
+rajit:bootstrap3-datepicker-da
+rajit:bootstrap3-datepicker-de
+rajit:bootstrap3-datepicker-el
+rajit:bootstrap3-datepicker-en-gb
+rajit:bootstrap3-datepicker-eo
+rajit:bootstrap3-datepicker-es
+rajit:bootstrap3-datepicker-eu
+rajit:bootstrap3-datepicker-fa
+rajit:bootstrap3-datepicker-fr
+rajit:bootstrap3-datepicker-gl
+rajit:bootstrap3-datepicker-he
+rajit:bootstrap3-datepicker-hi
+rajit:bootstrap3-datepicker-hu
+rajit:bootstrap3-datepicker-hy
+rajit:bootstrap3-datepicker-id
+rajit:bootstrap3-datepicker-it
+rajit:bootstrap3-datepicker-ja
+rajit:bootstrap3-datepicker-ka
+rajit:bootstrap3-datepicker-km
+rajit:bootstrap3-datepicker-ko
+rajit:bootstrap3-datepicker-lv
+rajit:bootstrap3-datepicker-mk
+rajit:bootstrap3-datepicker-mn
+rajit:bootstrap3-datepicker-nb
+rajit:bootstrap3-datepicker-nl
+rajit:bootstrap3-datepicker-oc
+rajit:bootstrap3-datepicker-pl
+rajit:bootstrap3-datepicker-pt-br
+rajit:bootstrap3-datepicker-pt
+rajit:bootstrap3-datepicker-ro
+rajit:bootstrap3-datepicker-ru
+rajit:bootstrap3-datepicker-sl
+rajit:bootstrap3-datepicker-sr
+rajit:bootstrap3-datepicker-sv
+rajit:bootstrap3-datepicker-sw
+rajit:bootstrap3-datepicker-ta
+rajit:bootstrap3-datepicker-th
+rajit:bootstrap3-datepicker-tr
+rajit:bootstrap3-datepicker-uk
+rajit:bootstrap3-datepicker-vi
+rajit:bootstrap3-datepicker-zh-cn
+rajit:bootstrap3-datepicker-zh-tw
+staringatlights:fast-render
+spacebars
 easylogic:summernote
 easylogic:summernote
-cfs:filesystem
-ostrio:cookies
+pascoual:pdfkit
+wekan-accounts-lockout
+lmieulet:meteor-coverage
+meteortesting:mocha

+ 1 - 1
.meteor/release

@@ -1 +1 @@
-METEOR@1.10.1
+METEOR@2.2

+ 121 - 69
.meteor/versions

@@ -1,7 +1,7 @@
 3stack:presence@1.1.2
 3stack:presence@1.1.2
-accounts-base@1.6.0
+accounts-base@1.9.0
 accounts-oauth@1.2.0
 accounts-oauth@1.2.0
-accounts-password@1.6.0
+accounts-password@1.7.1
 aldeed:collection2@2.10.0
 aldeed:collection2@2.10.0
 aldeed:collection2-core@1.2.0
 aldeed:collection2-core@1.2.0
 aldeed:schema-deny@1.1.0
 aldeed:schema-deny@1.1.0
@@ -10,37 +10,20 @@ aldeed:simple-schema@1.5.4
 allow-deny@1.1.0
 allow-deny@1.1.0
 arillo:flow-router-helpers@0.5.2
 arillo:flow-router-helpers@0.5.2
 audit-argument-checks@1.0.7
 audit-argument-checks@1.0.7
-autoupdate@1.6.0
-babel-compiler@7.5.3
+autoupdate@1.7.0
+babel-compiler@7.6.1
 babel-runtime@1.5.0
 babel-runtime@1.5.0
 base64@1.0.12
 base64@1.0.12
 binary-heap@1.0.11
 binary-heap@1.0.11
-blaze@2.3.4
-blaze-tools@1.0.10
-boilerplate-generator@1.7.0
+blaze@2.5.0
+blaze-tools@1.1.2
+boilerplate-generator@1.7.1
 browser-policy-common@1.0.11
 browser-policy-common@1.0.11
 browser-policy-framing@1.1.0
 browser-policy-framing@1.1.0
 caching-compiler@1.2.2
 caching-compiler@1.2.2
-caching-html-compiler@1.1.3
+caching-html-compiler@1.2.0
 callback-hook@1.3.0
 callback-hook@1.3.0
-cfs:access-point@0.1.49
-cfs:base-package@0.0.30
-cfs:collection@0.5.5
-cfs:collection-filters@0.2.4
-cfs:data-man@0.0.6
-cfs:file@0.1.17
-cfs:filesystem@0.1.2
-cfs:gridfs@0.0.34
 cfs:http-methods@0.0.32
 cfs:http-methods@0.0.32
-cfs:http-publish@0.0.13
-cfs:power-queue@0.9.11
-cfs:reactive-list@0.0.9
-cfs:reactive-property@0.0.4
-cfs:standard-packages@0.5.10
-cfs:storage-adapter@0.2.4
-cfs:tempstore@0.1.6
-cfs:upload-http@0.0.20
-cfs:worker@0.1.5
 check@1.3.1
 check@1.3.1
 chuangbo:cookie@1.1.0
 chuangbo:cookie@1.1.0
 coagmano:stylus@1.1.0
 coagmano:stylus@1.1.0
@@ -49,20 +32,20 @@ coffeescript-compiler@2.4.1
 cottz:publish-relations@2.0.8
 cottz:publish-relations@2.0.8
 dburles:collection-helpers@1.1.0
 dburles:collection-helpers@1.1.0
 ddp@1.4.0
 ddp@1.4.0
-ddp-client@2.3.3
+ddp-client@2.4.1
 ddp-common@1.4.0
 ddp-common@1.4.0
-ddp-rate-limiter@1.0.7
-ddp-server@2.3.1
+ddp-rate-limiter@1.0.9
+ddp-server@2.3.3
 deps@1.0.12
 deps@1.0.12
 diff-sequence@1.1.1
 diff-sequence@1.1.1
-dynamic-import@0.5.2
+dynamic-import@0.6.0
 easylogic:summernote@0.8.8
 easylogic:summernote@0.8.8
-ecmascript@0.14.3
+ecmascript@0.15.1
 ecmascript-runtime@0.7.0
 ecmascript-runtime@0.7.0
-ecmascript-runtime-client@0.10.0
-ecmascript-runtime-server@0.9.0
+ecmascript-runtime-client@0.11.1
+ecmascript-runtime-server@0.10.1
 ejson@1.1.1
 ejson@1.1.1
-email@1.2.3
+email@2.0.0
 es5-shim@4.8.0
 es5-shim@4.8.0
 fastclick@1.0.13
 fastclick@1.0.13
 fetch@0.1.1
 fetch@0.1.1
@@ -70,10 +53,10 @@ fortawesome:fontawesome@4.7.0
 geojson-utils@1.0.10
 geojson-utils@1.0.10
 horka:swipebox@1.0.2
 horka:swipebox@1.0.2
 hot-code-push@1.0.4
 hot-code-push@1.0.4
-html-tools@1.0.11
-htmljs@1.0.11
-http@1.4.2
-id-map@1.1.0
+html-tools@1.1.2
+htmljs@1.1.1
+http@1.4.4
+id-map@1.1.1
 idmontie:migrations@1.0.3
 idmontie:migrations@1.0.3
 inter-process-messaging@0.1.1
 inter-process-messaging@0.1.1
 jquery@1.11.11
 jquery@1.11.11
@@ -84,14 +67,13 @@ kenton:accounts-sandstorm@0.7.0
 konecty:mongo-counter@0.0.5_3
 konecty:mongo-counter@0.0.5_3
 lamhieu:meteorx@2.1.1
 lamhieu:meteorx@2.1.1
 lamhieu:unblock@1.0.0
 lamhieu:unblock@1.0.0
-launch-screen@1.2.0
+launch-screen@1.2.1
 livedata@1.0.18
 livedata@1.0.18
+lmieulet:meteor-coverage@3.2.0
 localstorage@1.2.0
 localstorage@1.2.0
-logging@1.1.20
-lucasantoniassi:accounts-lockout@1.0.0
+logging@1.2.0
 matb33:collection-hooks@0.9.1
 matb33:collection-hooks@0.9.1
 matteodem:easy-search@1.6.4
 matteodem:easy-search@1.6.4
-mdg:meteor-apm-agent@3.2.5
 mdg:validation-error@0.5.1
 mdg:validation-error@0.5.1
 meteor@1.9.3
 meteor@1.9.3
 meteor-base@1.4.0
 meteor-base@1.4.0
@@ -101,19 +83,22 @@ meteorhacks:collection-utils@1.2.0
 meteorhacks:picker@1.0.3
 meteorhacks:picker@1.0.3
 meteorhacks:subs-manager@1.6.4
 meteorhacks:subs-manager@1.6.4
 meteorspark:util@0.2.0
 meteorspark:util@0.2.0
-minifier-css@1.5.0
+meteortesting:browser-tests@1.3.4
+meteortesting:mocha@2.0.1
+meteortesting:mocha-core@8.0.1
+minifier-css@1.5.4
 minifier-js@2.6.0
 minifier-js@2.6.0
 minifiers@1.1.8-faster-rebuild.0
 minifiers@1.1.8-faster-rebuild.0
-minimongo@1.5.0
+minimongo@1.6.2
 mobile-status-bar@1.1.0
 mobile-status-bar@1.1.0
 modern-browsers@0.1.5
 modern-browsers@0.1.5
-modules@0.15.0
+modules@0.16.0
 modules-runtime@0.12.0
 modules-runtime@0.12.0
-momentjs:moment@2.24.0
-mongo@1.9.1
-mongo-decimal@0.1.1
+momentjs:moment@2.29.1
+mongo@1.11.1
+mongo-decimal@0.1.2
 mongo-dev-server@1.1.0
 mongo-dev-server@1.1.0
-mongo-id@1.0.7
+mongo-id@1.0.8
 mongo-livedata@1.0.12
 mongo-livedata@1.0.12
 mousetrap:mousetrap@1.4.6_1
 mousetrap:mousetrap@1.4.6_1
 mquandalle:autofocus@1.0.0
 mquandalle:autofocus@1.0.0
@@ -124,16 +109,15 @@ mquandalle:jquery-textcomplete@0.8.0_1
 mquandalle:jquery-ui-drag-drop-sort@0.2.0
 mquandalle:jquery-ui-drag-drop-sort@0.2.0
 mquandalle:moment@1.0.1
 mquandalle:moment@1.0.1
 mquandalle:mousetrap-bindglobal@0.0.1
 mquandalle:mousetrap-bindglobal@0.0.1
-mquandalle:perfect-scrollbar@0.6.5_2
 msavin:usercache@1.8.0
 msavin:usercache@1.8.0
-npm-bcrypt@0.9.3
-npm-mongo@3.7.0
-oauth@1.3.0
+npm-bcrypt@0.9.4
+npm-mongo@3.9.0
+oauth@1.3.2
 oauth2@1.3.0
 oauth2@1.3.0
 observe-sequence@1.0.16
 observe-sequence@1.0.16
 ongoworks:speakingurl@1.1.0
 ongoworks:speakingurl@1.1.0
 ordered-dict@1.1.0
 ordered-dict@1.1.0
-ostrio:cookies@2.6.0
+pascoual:pdfkit@1.0.7
 peerlibrary:assert@0.3.0
 peerlibrary:assert@0.3.0
 peerlibrary:base-component@0.16.0
 peerlibrary:base-component@0.16.0
 peerlibrary:blaze-components@0.15.1
 peerlibrary:blaze-components@0.15.1
@@ -144,11 +128,60 @@ promise@0.11.2
 raix:eventemitter@0.1.3
 raix:eventemitter@0.1.3
 raix:handlebar-helpers@0.2.5
 raix:handlebar-helpers@0.2.5
 rajit:bootstrap3-datepicker@1.7.1_1
 rajit:bootstrap3-datepicker@1.7.1_1
+rajit:bootstrap3-datepicker-ar@1.7.1
+rajit:bootstrap3-datepicker-bg@1.7.1
+rajit:bootstrap3-datepicker-br@1.7.1
+rajit:bootstrap3-datepicker-ca@1.7.1
+rajit:bootstrap3-datepicker-cs@1.7.1
+rajit:bootstrap3-datepicker-da@1.7.1
+rajit:bootstrap3-datepicker-de@1.7.1
+rajit:bootstrap3-datepicker-el@1.7.1
+rajit:bootstrap3-datepicker-en-gb@1.7.1
+rajit:bootstrap3-datepicker-eo@1.7.1
+rajit:bootstrap3-datepicker-es@1.7.1
+rajit:bootstrap3-datepicker-eu@1.7.1
+rajit:bootstrap3-datepicker-fa@1.7.1
+rajit:bootstrap3-datepicker-fi@1.7.1
+rajit:bootstrap3-datepicker-fr@1.7.1
+rajit:bootstrap3-datepicker-gl@1.7.1
+rajit:bootstrap3-datepicker-he@1.7.1
+rajit:bootstrap3-datepicker-hi@1.7.1
+rajit:bootstrap3-datepicker-hu@1.7.1
+rajit:bootstrap3-datepicker-hy@1.7.1
+rajit:bootstrap3-datepicker-id@1.7.1
+rajit:bootstrap3-datepicker-it@1.7.1
+rajit:bootstrap3-datepicker-ja@1.7.1
+rajit:bootstrap3-datepicker-ka@1.7.1
+rajit:bootstrap3-datepicker-km@1.7.1
+rajit:bootstrap3-datepicker-ko@1.7.1
+rajit:bootstrap3-datepicker-lv@1.7.1
+rajit:bootstrap3-datepicker-mk@1.7.1
+rajit:bootstrap3-datepicker-mn@1.7.1
+rajit:bootstrap3-datepicker-nb@1.7.1
+rajit:bootstrap3-datepicker-nl@1.7.1
+rajit:bootstrap3-datepicker-oc@1.7.1
+rajit:bootstrap3-datepicker-pl@1.7.1
+rajit:bootstrap3-datepicker-pt@1.7.1
+rajit:bootstrap3-datepicker-pt-br@1.7.1
+rajit:bootstrap3-datepicker-ro@1.7.1
+rajit:bootstrap3-datepicker-ru@1.7.1
+rajit:bootstrap3-datepicker-sl@1.7.1
+rajit:bootstrap3-datepicker-sr@1.7.1
+rajit:bootstrap3-datepicker-sv@1.7.1
+rajit:bootstrap3-datepicker-sw@1.7.1
+rajit:bootstrap3-datepicker-ta@1.7.1
+rajit:bootstrap3-datepicker-th@1.7.1
+rajit:bootstrap3-datepicker-tr@1.7.1
+rajit:bootstrap3-datepicker-uk@1.7.1
+rajit:bootstrap3-datepicker-vi@1.7.1
+rajit:bootstrap3-datepicker-zh-cn@1.7.1
+rajit:bootstrap3-datepicker-zh-tw@1.7.1
 random@1.2.0
 random@1.2.0
 rate-limit@1.0.9
 rate-limit@1.0.9
+react-fast-refresh@0.1.1
 reactive-dict@1.3.0
 reactive-dict@1.3.0
 reactive-var@1.0.11
 reactive-var@1.0.11
-reload@1.3.0
+reload@1.3.1
 retry@1.1.0
 retry@1.1.0
 routepolicy@1.1.0
 routepolicy@1.1.0
 rzymek:fullcalendar@3.8.0
 rzymek:fullcalendar@3.8.0
@@ -162,37 +195,56 @@ simple:json-routes@2.1.0
 simple:rest-accounts-password@1.1.2
 simple:rest-accounts-password@1.1.2
 simple:rest-bearer-token-parser@1.0.1
 simple:rest-bearer-token-parser@1.0.1
 simple:rest-json-error-handler@1.0.1
 simple:rest-json-error-handler@1.0.1
-socket-stream-client@0.2.3
+socket-stream-client@0.3.3
 softwarerero:accounts-t9n@1.3.11
 softwarerero:accounts-t9n@1.3.11
-spacebars@1.0.15
-spacebars-compiler@1.1.3
-srp@1.0.12
-standard-minifier-css@1.6.0
+spacebars@1.2.0
+spacebars-compiler@1.2.1
+srp@1.1.0
+standard-minifier-css@1.7.2
 standard-minifier-js@2.6.0
 standard-minifier-js@2.6.0
-staringatlights:fast-render@3.2.0
+staringatlights:fast-render@3.3.0
 staringatlights:inject-data@2.3.0
 staringatlights:inject-data@2.3.0
+steffo:meteor-accounts-saml@0.0.18
 tap:i18n@1.8.2
 tap:i18n@1.8.2
 templates:tabs@2.3.0
 templates:tabs@2.3.0
-templating@1.3.2
-templating-compiler@1.3.3
-templating-runtime@1.3.2
-templating-tools@1.1.2
+templating@1.4.0
+templating-compiler@1.4.1
+templating-runtime@1.4.0
+templating-tools@1.2.0
 tracker@1.2.0
 tracker@1.2.0
 twbs:bootstrap@3.3.6
 twbs:bootstrap@3.3.6
 ui@1.0.13
 ui@1.0.13
 underscore@1.0.10
 underscore@1.0.10
-url@1.2.0
+url@1.3.2
 useraccounts:core@1.14.2
 useraccounts:core@1.14.2
 useraccounts:flow-routing@1.14.2
 useraccounts:flow-routing@1.14.2
 useraccounts:unstyled@1.14.2
 useraccounts:unstyled@1.14.2
 verron:autosize@3.0.8
 verron:autosize@3.0.8
-webapp@1.9.1
-webapp-hashing@1.0.9
+webapp@1.10.1
+webapp-hashing@1.1.0
 wekan-accounts-cas@0.1.0
 wekan-accounts-cas@0.1.0
+wekan-accounts-lockout@1.0.0
 wekan-accounts-oidc@1.0.10
 wekan-accounts-oidc@1.0.10
+wekan-cfs-access-point@0.1.50
+wekan-cfs-base-package@0.0.30
+wekan-cfs-collection@0.5.5
+wekan-cfs-collection-filters@0.2.4
+wekan-cfs-data-man@0.0.6
+wekan-cfs-file@0.1.17
+wekan-cfs-filesystem@0.1.2
+wekan-cfs-gridfs@0.0.34
+wekan-cfs-http-methods@0.0.32
+wekan-cfs-http-publish@0.0.13
+wekan-cfs-power-queue@0.9.11
+wekan-cfs-reactive-list@0.0.9
+wekan-cfs-reactive-property@0.0.4
+wekan-cfs-standard-packages@0.5.10
+wekan-cfs-storage-adapter@0.2.4
+wekan-cfs-tempstore@0.1.6
+wekan-cfs-upload-http@0.0.21
+wekan-cfs-worker@0.1.5
 wekan-ldap@0.0.2
 wekan-ldap@0.0.2
-wekan-markdown@1.0.7
+wekan-markdown@1.0.9
 wekan-oidc@1.0.12
 wekan-oidc@1.0.12
-wekan-scrollbar@3.1.3
 yasaricli:slugify@0.0.7
 yasaricli:slugify@0.0.7
 zimme:active-route@2.3.2
 zimme:active-route@2.3.2

+ 0 - 20
.sandstorm-meteor-1.8/.meteor/.finished-upgraders

@@ -1,20 +0,0 @@
-# This file contains information which helps Meteor properly upgrade your
-# app when you run 'meteor update'. You should check it into version control
-# with your project.
-
-notices-for-0.9.0
-notices-for-0.9.1
-0.9.4-platform-file
-notices-for-facebook-graph-api-2
-1.2.0-standard-minifiers-package
-1.2.0-meteor-platform-split
-1.2.0-cordova-changes
-1.2.0-breaking-changes
-1.3.0-split-minifiers-package
-1.3.5-remove-old-dev-bundle-link
-1.4.0-remove-old-dev-bundle-link
-1.4.1-add-shell-server-package
-1.4.3-split-account-service-packages
-1.5-add-dynamic-import-package
-1.7-split-underscore-from-meteor-base
-1.8.3-split-jquery-from-blaze

+ 0 - 2
.sandstorm-meteor-1.8/.meteor/.gitignore

@@ -1,2 +0,0 @@
-dev_bundle
-local

+ 0 - 7
.sandstorm-meteor-1.8/.meteor/.id

@@ -1,7 +0,0 @@
-# This file contains a token that is unique to your project.
-# Check it into your repository along with the rest of this directory.
-# It can be used for purposes such as:
-#   - ensuring you don't accidentally deploy one app on top of another
-#   - providing package authors with aggregated statistics
-
-dvyihgykyzec6y1dpg

+ 0 - 100
.sandstorm-meteor-1.8/.meteor/packages

@@ -1,100 +0,0 @@
-# Meteor packages used by this project, one per line.
-#
-# 'meteor add' and 'meteor remove' will edit this file for you,
-# but you can also edit it by hand.
-
-meteor-base@1.4.0
-
-# Build system
-ecmascript@0.13.2
-standard-minifier-css@1.5.4
-standard-minifier-js@2.5.2
-mquandalle:jade
-
-# Polyfills
-es5-shim@4.8.0
-
-# Collections
-aldeed:collection2
-cfs:standard-packages
-cottz:publish-relations
-dburles:collection-helpers
-idmontie:migrations
-matb33:collection-hooks
-matteodem:easy-search
-mongo@1.7.0
-mquandalle:collection-mutations
-
-# Account system
-kenton:accounts-sandstorm
-service-configuration@1.0.11
-useraccounts:unstyled
-useraccounts:flow-routing
-wekan-ldap
-wekan-accounts-cas
-wekan-accounts-oidc
-
-# Utilities
-check@1.3.1
-jquery@1.11.10
-random@1.1.0
-reactive-dict@1.3.0
-session@1.2.0
-tracker@1.2.0
-underscore@1.0.10
-3stack:presence
-alethes:pages
-arillo:flow-router-helpers
-audit-argument-checks@1.0.7
-kadira:blaze-layout
-kadira:dochead
-mquandalle:autofocus
-ongoworks:speakingurl
-raix:handlebar-helpers
-tap:i18n
-http@1.4.2
-
-# UI components
-blaze
-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
-simple:json-routes
-rajit:bootstrap3-datepicker
-shell-server@0.4.0
-simple:rest-accounts-password
-useraccounts:core
-email@1.2.3
-horka:swipebox
-dynamic-import@0.5.1
-staringatlights:fast-render
-
-accounts-password@1.5.2
-cfs:gridfs
-rzymek:fullcalendar
-momentjs:moment@2.22.2
-browser-policy-framing@1.1.0
-mquandalle:moment
-msavin:usercache
-wekan-scrollbar
-mquandalle:perfect-scrollbar
-mdg:meteor-apm-agent@3.2.0-rc.0!
-# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
-coagmano:stylus@1.1.0!
-lucasantoniassi:accounts-lockout
-meteorhacks:subs-manager
-meteorhacks:picker
-lamhieu:unblock
-meteorhacks:aggregate@1.3.0
-wekan-markdown
-konecty:mongo-counter
-percolate:synced-cron
-easylogic:summernote
-cfs:filesystem
-ostrio:cookies

+ 0 - 2
.sandstorm-meteor-1.8/.meteor/platforms

@@ -1,2 +0,0 @@
-server
-browser

+ 0 - 1
.sandstorm-meteor-1.8/.meteor/release

@@ -1 +0,0 @@
-METEOR@1.8.3

+ 0 - 198
.sandstorm-meteor-1.8/.meteor/versions

@@ -1,198 +0,0 @@
-3stack:presence@1.1.2
-accounts-base@1.4.5
-accounts-oauth@1.1.16
-accounts-password@1.5.2
-aldeed:collection2@2.10.0
-aldeed:collection2-core@1.2.0
-aldeed:schema-deny@1.1.0
-aldeed:schema-index@1.1.1
-aldeed:simple-schema@1.5.4
-alethes:pages@1.8.6
-allow-deny@1.1.0
-arillo:flow-router-helpers@0.5.2
-audit-argument-checks@1.0.7
-autoupdate@1.6.0
-babel-compiler@7.4.2
-babel-runtime@1.4.0
-base64@1.0.12
-binary-heap@1.0.11
-blaze@2.3.4
-blaze-tools@1.0.10
-boilerplate-generator@1.6.0
-browser-policy-common@1.0.11
-browser-policy-framing@1.1.0
-caching-compiler@1.2.1
-caching-html-compiler@1.1.3
-callback-hook@1.2.0
-cfs:access-point@0.1.49
-cfs:base-package@0.0.30
-cfs:collection@0.5.5
-cfs:collection-filters@0.2.4
-cfs:data-man@0.0.6
-cfs:file@0.1.17
-cfs:filesystem@0.1.2
-cfs:gridfs@0.0.34
-cfs:http-methods@0.0.32
-cfs:http-publish@0.0.13
-cfs:power-queue@0.9.11
-cfs:reactive-list@0.0.9
-cfs:reactive-property@0.0.4
-cfs:standard-packages@0.5.10
-cfs:storage-adapter@0.2.4
-cfs:tempstore@0.1.6
-cfs:upload-http@0.0.20
-cfs:worker@0.1.5
-check@1.3.1
-chuangbo:cookie@1.1.0
-coagmano:stylus@1.1.0
-coffeescript@1.0.17
-cottz:publish-relations@2.0.8
-dburles:collection-helpers@1.1.0
-ddp@1.4.0
-ddp-client@2.3.3
-ddp-common@1.4.0
-ddp-rate-limiter@1.0.7
-ddp-server@2.3.0
-deps@1.0.12
-diff-sequence@1.1.1
-dynamic-import@0.5.1
-easylogic:summernote@0.8.8
-ecmascript@0.13.2
-ecmascript-runtime@0.7.0
-ecmascript-runtime-client@0.9.0
-ecmascript-runtime-server@0.8.0
-ejson@1.1.1
-email@1.2.3
-es5-shim@4.8.0
-fastclick@1.0.13
-fetch@0.1.1
-fortawesome:fontawesome@4.7.0
-geojson-utils@1.0.10
-horka:swipebox@1.0.2
-hot-code-push@1.0.4
-html-tools@1.0.11
-htmljs@1.0.11
-http@1.4.2
-id-map@1.1.0
-idmontie:migrations@1.0.3
-inter-process-messaging@0.1.0
-jquery@1.11.11
-kadira:blaze-layout@2.3.0
-kadira:dochead@1.5.0
-kadira:flow-router@2.12.1
-kenton:accounts-sandstorm@0.7.0
-konecty:mongo-counter@0.0.5_3
-lamhieu:meteorx@2.1.1
-lamhieu:unblock@1.0.0
-launch-screen@1.1.1
-livedata@1.0.18
-localstorage@1.2.0
-logging@1.1.20
-lucasantoniassi:accounts-lockout@1.0.0
-matb33:collection-hooks@0.9.1
-matteodem:easy-search@1.6.4
-mdg:meteor-apm-agent@3.2.5
-mdg:validation-error@0.5.1
-meteor@1.9.3
-meteor-base@1.4.0
-meteor-platform@1.2.6
-meteorhacks:aggregate@1.3.0
-meteorhacks:collection-utils@1.2.0
-meteorhacks:picker@1.0.3
-meteorhacks:subs-manager@1.6.4
-meteorspark:util@0.2.0
-minifier-css@1.4.3
-minifier-js@2.5.1
-minifiers@1.1.8-faster-rebuild.0
-minimongo@1.4.5
-mobile-status-bar@1.0.14
-modern-browsers@0.1.4
-modules@0.14.0
-modules-runtime@0.11.0
-momentjs:moment@2.24.0
-mongo@1.7.0
-mongo-decimal@0.1.1
-mongo-dev-server@1.1.0
-mongo-id@1.0.7
-mongo-livedata@1.0.12
-mousetrap:mousetrap@1.4.6_1
-mquandalle:autofocus@1.0.0
-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
-mquandalle:perfect-scrollbar@0.6.5_2
-msavin:usercache@1.8.0
-npm-bcrypt@0.9.3
-npm-mongo@3.2.0
-oauth@1.2.8
-oauth2@1.2.1
-observe-sequence@1.0.16
-ongoworks:speakingurl@1.1.0
-ordered-dict@1.1.0
-ostrio:cookies@2.5.0
-peerlibrary:assert@0.3.0
-peerlibrary:base-component@0.16.0
-peerlibrary:blaze-components@0.15.1
-peerlibrary:computed-field@0.10.0
-peerlibrary:reactive-field@0.6.0
-percolate:synced-cron@1.3.2
-promise@0.11.2
-raix:eventemitter@0.1.3
-raix:handlebar-helpers@0.2.5
-rajit:bootstrap3-datepicker@1.7.1_1
-random@1.1.0
-rate-limit@1.0.9
-reactive-dict@1.3.0
-reactive-var@1.0.11
-reload@1.3.0
-retry@1.1.0
-routepolicy@1.1.0
-rzymek:fullcalendar@3.8.0
-server-render@0.3.1
-service-configuration@1.0.11
-session@1.2.0
-sha@1.0.9
-shell-server@0.4.0
-simple:authenticate-user-by-token@1.0.1
-simple:json-routes@2.1.0
-simple:rest-accounts-password@1.1.2
-simple:rest-bearer-token-parser@1.0.1
-simple:rest-json-error-handler@1.0.1
-socket-stream-client@0.2.2
-softwarerero:accounts-t9n@1.3.11
-spacebars@1.0.15
-spacebars-compiler@1.1.3
-srp@1.0.12
-standard-minifier-css@1.5.4
-standard-minifier-js@2.5.2
-staringatlights:fast-render@3.2.0
-staringatlights:inject-data@2.3.0
-tap:i18n@1.8.2
-templates:tabs@2.3.0
-templating@1.3.2
-templating-compiler@1.3.3
-templating-runtime@1.3.2
-templating-tools@1.1.2
-tracker@1.2.0
-twbs:bootstrap@3.3.6
-ui@1.0.13
-underscore@1.0.10
-url@1.2.0
-useraccounts:core@1.14.2
-useraccounts:flow-routing@1.14.2
-useraccounts:unstyled@1.14.2
-verron:autosize@3.0.8
-webapp@1.7.5
-webapp-hashing@1.0.9
-wekan-accounts-cas@0.1.0
-wekan-accounts-oidc@1.0.10
-wekan-ldap@0.0.2
-wekan-markdown@1.0.7
-wekan-oidc@1.0.12
-wekan-scrollbar@3.1.3
-yasaricli:slugify@0.0.7
-zimme:active-route@2.3.2

+ 0 - 914
.sandstorm-meteor-1.8/cfs_access-point.txt

@@ -1,914 +0,0 @@
-(function () {
-
-/* Imports */
-var Meteor = Package.meteor.Meteor;
-var global = Package.meteor.global;
-var meteorEnv = Package.meteor.meteorEnv;
-var FS = Package['cfs:base-package'].FS;
-var check = Package.check.check;
-var Match = Package.check.Match;
-var EJSON = Package.ejson.EJSON;
-var HTTP = Package['cfs:http-methods'].HTTP;
-
-/* Package-scope variables */
-var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
-
-(function(){
-
-///////////////////////////////////////////////////////////////////////
-//                                                                   //
-// packages/cfs_access-point/packages/cfs_access-point.js            //
-//                                                                   //
-///////////////////////////////////////////////////////////////////////
-                                                                     //
-(function () {
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-//                                                                                                                    //
-// packages/cfs:access-point/access-point-common.js                                                                   //
-//                                                                                                                    //
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-                                                                                                                      //
-rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "";                                             // 1
-// Adjust the rootUrlPathPrefix if necessary                                                                          // 2
-if (rootUrlPathPrefix.length > 0) {                                                                                   // 3
-  if (rootUrlPathPrefix.slice(0, 1) !== '/') {                                                                        // 4
-    rootUrlPathPrefix = '/' + rootUrlPathPrefix;                                                                      // 5
-  }                                                                                                                   // 6
-  if (rootUrlPathPrefix.slice(-1) === '/') {                                                                          // 7
-    rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1);                                                               // 8
-  }                                                                                                                   // 9
-}                                                                                                                     // 10
-                                                                                                                      // 11
-// prepend ROOT_URL when isCordova                                                                                    // 12
-if (Meteor.isCordova) {                                                                                               // 13
-  rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, '');                  // 14
-}                                                                                                                     // 15
-                                                                                                                      // 16
-baseUrl = '/cfs';                                                                                                     // 17
-FS.HTTP = FS.HTTP || {};                                                                                              // 18
-                                                                                                                      // 19
-// Note the upload URL so that client uploader packages know what it is                                               // 20
-FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files';                                                           // 21
-                                                                                                                      // 22
-/**                                                                                                                   // 23
- * @method FS.HTTP.setBaseUrl                                                                                         // 24
- * @public                                                                                                            // 25
- * @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints.                            // 26
- * @returns {undefined}                                                                                               // 27
- */                                                                                                                   // 28
-FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) {                                                                // 29
-                                                                                                                      // 30
-  // Adjust the baseUrl if necessary                                                                                  // 31
-  if (newBaseUrl.slice(0, 1) !== '/') {                                                                               // 32
-    newBaseUrl = '/' + newBaseUrl;                                                                                    // 33
-  }                                                                                                                   // 34
-  if (newBaseUrl.slice(-1) === '/') {                                                                                 // 35
-    newBaseUrl = newBaseUrl.slice(0, -1);                                                                             // 36
-  }                                                                                                                   // 37
-                                                                                                                      // 38
-  // Update the base URL                                                                                              // 39
-  baseUrl = newBaseUrl;                                                                                               // 40
-                                                                                                                      // 41
-  // Change the upload URL so that client uploader packages know what it is                                           // 42
-  FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files';                                                         // 43
-                                                                                                                      // 44
-  // Remount URLs with the new baseUrl, unmounting the old, on the server only.                                       // 45
-  // If existingMountPoints is empty, then we haven't run the server startup                                          // 46
-  // code yet, so this new URL will be used at that point for the initial mount.                                      // 47
-  if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) {                                                 // 48
-    mountUrls();                                                                                                      // 49
-  }                                                                                                                   // 50
-};                                                                                                                    // 51
-                                                                                                                      // 52
-/*                                                                                                                    // 53
- * FS.File extensions                                                                                                 // 54
- */                                                                                                                   // 55
-                                                                                                                      // 56
-/**                                                                                                                   // 57
- * @method FS.File.prototype.url Construct the file url                                                               // 58
- * @public                                                                                                            // 59
- * @param {Object} [options]                                                                                          // 60
- * @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
- * @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
- * @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
- * @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
- * @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
- * @param {String} [options.uploading=null] A URL to return while the file is being uploaded.                         // 66
- * @param {String} [options.storing=null] A URL to return while the file is being stored.                             // 67
- * @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
- *                                                                                                                    // 69
- * Returns the HTTP URL for getting the file or its metadata.                                                         // 70
- */                                                                                                                   // 71
-FS.File.prototype.url = function(options) {                                                                           // 72
-  var self = this;                                                                                                    // 73
-  options = options || {};                                                                                            // 74
-  options = FS.Utility.extend({                                                                                       // 75
-    store: null,                                                                                                      // 76
-    auth: null,                                                                                                       // 77
-    download: false,                                                                                                  // 78
-    metadata: false,                                                                                                  // 79
-    brokenIsFine: false,                                                                                              // 80
-    uploading: null, // return this URL while uploading                                                               // 81
-    storing: null, // return this URL while storing                                                                   // 82
-    filename: null // override the filename that is shown to the user                                                 // 83
-  }, options.hash || options); // check for "hash" prop if called as helper                                           // 84
-                                                                                                                      // 85
-  // Primarily useful for displaying a temporary image while uploading an image                                       // 86
-  if (options.uploading && !self.isUploaded()) {                                                                      // 87
-    return options.uploading;                                                                                         // 88
-  }                                                                                                                   // 89
-                                                                                                                      // 90
-  if (self.isMounted()) {                                                                                             // 91
-    // See if we've stored in the requested store yet                                                                 // 92
-    var storeName = options.store || self.collection.primaryStore.name;                                               // 93
-    if (!self.hasStored(storeName)) {                                                                                 // 94
-      if (options.storing) {                                                                                          // 95
-        return options.storing;                                                                                       // 96
-      } else if (!options.brokenIsFine) {                                                                             // 97
-        // We want to return null if we know the URL will be a broken                                                 // 98
-        // link because then we can avoid rendering broken links, broken                                              // 99
-        // images, etc.                                                                                               // 100
-        return null;                                                                                                  // 101
-      }                                                                                                               // 102
-    }                                                                                                                 // 103
-                                                                                                                      // 104
-    // Add filename to end of URL if we can determine one                                                             // 105
-    var filename = options.filename || self.name({store: storeName});                                                 // 106
-    if (typeof filename === "string" && filename.length) {                                                            // 107
-      filename = '/' + filename;                                                                                      // 108
-    } else {                                                                                                          // 109
-      filename = '';                                                                                                  // 110
-    }                                                                                                                 // 111
-                                                                                                                      // 112
-    // TODO: Could we somehow figure out if the collection requires login?                                            // 113
-    var authToken = '';                                                                                               // 114
-    if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") {     // 115
-      if (options.auth !== false) {                                                                                   // 116
-        // Add reactive deps on the user                                                                              // 117
-        Meteor.userId();                                                                                              // 118
-                                                                                                                      // 119
-        var authObject = {                                                                                            // 120
-          authToken: Accounts._storedLoginToken() || ''                                                               // 121
-        };                                                                                                            // 122
-                                                                                                                      // 123
-        // If it's a number, we use that as the expiration time (in seconds)                                          // 124
-        if (options.auth === +options.auth) {                                                                         // 125
-          authObject.expiration = FS.HTTP.now() + options.auth * 1000;                                                // 126
-        }                                                                                                             // 127
-                                                                                                                      // 128
-        // Set the authToken                                                                                          // 129
-        var authString = JSON.stringify(authObject);                                                                  // 130
-        authToken = FS.Utility.btoa(authString);                                                                      // 131
-      }                                                                                                               // 132
-    } else if (typeof options.auth === "string") {                                                                    // 133
-      // If the user supplies auth token the user will be responsible for                                             // 134
-      // updating                                                                                                     // 135
-      authToken = options.auth;                                                                                       // 136
-    }                                                                                                                 // 137
-                                                                                                                      // 138
-    // Construct query string                                                                                         // 139
-    var params = {};                                                                                                  // 140
-    if (authToken !== '') {                                                                                           // 141
-      params.token = authToken;                                                                                       // 142
-    }                                                                                                                 // 143
-    if (options.download) {                                                                                           // 144
-      params.download = true;                                                                                         // 145
-    }                                                                                                                 // 146
-    if (options.store) {                                                                                              // 147
-      // We use options.store here instead of storeName because we want to omit the queryString                       // 148
-      // whenever possible, allowing users to have "clean" URLs if they want. The server will                         // 149
-      // assume the first store defined on the server, which means that we are assuming that                          // 150
-      // the first on the client is also the first on the server. If that's not the case, the                         // 151
-      // store option should be supplied.                                                                             // 152
-      params.store = options.store;                                                                                   // 153
-    }                                                                                                                 // 154
-    var queryString = FS.Utility.encodeParams(params);                                                                // 155
-    if (queryString.length) {                                                                                         // 156
-      queryString = '?' + queryString;                                                                                // 157
-    }                                                                                                                 // 158
-                                                                                                                      // 159
-    // Determine which URL to use                                                                                     // 160
-    var area;                                                                                                         // 161
-    if (options.metadata) {                                                                                           // 162
-      area = '/record';                                                                                               // 163
-    } else {                                                                                                          // 164
-      area = '/files';                                                                                                // 165
-    }                                                                                                                 // 166
-                                                                                                                      // 167
-    // Construct and return the http method url                                                                       // 168
-    return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169
-  }                                                                                                                   // 170
-                                                                                                                      // 171
-};                                                                                                                    // 172
-                                                                                                                      // 173
-                                                                                                                      // 174
-                                                                                                                      // 175
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-
-
-
-
-
-(function () {
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-//                                                                                                                    //
-// packages/cfs:access-point/access-point-handlers.js                                                                 //
-//                                                                                                                    //
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-                                                                                                                      //
-getHeaders = [];                                                                                                      // 1
-getHeadersByCollection = {};                                                                                          // 2
-                                                                                                                      // 3
-FS.HTTP.Handlers = {};                                                                                                // 4
-                                                                                                                      // 5
-/**                                                                                                                   // 6
- * @method FS.HTTP.Handlers.Del                                                                                       // 7
- * @public                                                                                                            // 8
- * @returns {any} response                                                                                            // 9
- *                                                                                                                    // 10
- * HTTP DEL request handler                                                                                           // 11
- */                                                                                                                   // 12
-FS.HTTP.Handlers.Del = function httpDelHandler(ref) {                                                                 // 13
-  var self = this;                                                                                                    // 14
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 15
-                                                                                                                      // 16
-  // If DELETE request, validate with 'remove' allow/deny, delete the file, and return                                // 17
-  FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId);                       // 18
-                                                                                                                      // 19
-  /*                                                                                                                  // 20
-   * From the DELETE spec:                                                                                            // 21
-   * A successful response SHOULD be 200 (OK) if the response includes an                                             // 22
-   * entity describing the status, 202 (Accepted) if the action has not                                               // 23
-   * yet been enacted, or 204 (No Content) if the action has been enacted                                             // 24
-   * but the response does not include an entity.                                                                     // 25
-   */                                                                                                                 // 26
-  self.setStatusCode(200);                                                                                            // 27
-                                                                                                                      // 28
-  return {                                                                                                            // 29
-    deleted: !!ref.file.remove()                                                                                      // 30
-  };                                                                                                                  // 31
-};                                                                                                                    // 32
-                                                                                                                      // 33
-/**                                                                                                                   // 34
- * @method FS.HTTP.Handlers.GetList                                                                                   // 35
- * @public                                                                                                            // 36
- * @returns {Object} response                                                                                         // 37
- *                                                                                                                    // 38
- * HTTP GET file list request handler                                                                                 // 39
- */                                                                                                                   // 40
-FS.HTTP.Handlers.GetList = function httpGetListHandler() {                                                            // 41
-  // Not Yet Implemented                                                                                              // 42
-  // Need to check publications and return file list based on                                                         // 43
-  // what user is allowed to see                                                                                      // 44
-};                                                                                                                    // 45
-                                                                                                                      // 46
-/*                                                                                                                    // 47
-  requestRange will parse the range set in request header - if not possible it                                        // 48
-  will throw fitting errors and autofill range for both partial and full ranges                                       // 49
-                                                                                                                      // 50
-  throws error or returns the object:                                                                                 // 51
-  {                                                                                                                   // 52
-    start                                                                                                             // 53
-    end                                                                                                               // 54
-    length                                                                                                            // 55
-    unit                                                                                                              // 56
-    partial                                                                                                           // 57
-  }                                                                                                                   // 58
-*/                                                                                                                    // 59
-var requestRange = function(req, fileSize) {                                                                          // 60
-  if (req) {                                                                                                          // 61
-    if (req.headers) {                                                                                                // 62
-      var rangeString = req.headers.range;                                                                            // 63
-                                                                                                                      // 64
-      // Make sure range is a string                                                                                  // 65
-      if (rangeString === ''+rangeString) {                                                                           // 66
-                                                                                                                      // 67
-        // range will be in the format "bytes=0-32767"                                                                // 68
-        var parts = rangeString.split('=');                                                                           // 69
-        var unit = parts[0];                                                                                          // 70
-                                                                                                                      // 71
-        // Make sure parts consists of two strings and range is of type "byte"                                        // 72
-        if (parts.length == 2 && unit == 'bytes') {                                                                   // 73
-          // Parse the range                                                                                          // 74
-          var range = parts[1].split('-');                                                                            // 75
-          var start = Number(range[0]);                                                                               // 76
-          var end = Number(range[1]);                                                                                 // 77
-                                                                                                                      // 78
-          // Fix invalid ranges?                                                                                      // 79
-          if (range[0] != start) start = 0;                                                                           // 80
-          if (range[1] != end || !end) end = fileSize - 1;                                                            // 81
-                                                                                                                      // 82
-          // Make sure range consists of a start and end point of numbers and start is less than end                  // 83
-          if (start < end) {                                                                                          // 84
-                                                                                                                      // 85
-            var partSize = 0 - start + end + 1;                                                                       // 86
-                                                                                                                      // 87
-            // Return the parsed range                                                                                // 88
-            return {                                                                                                  // 89
-              start: start,                                                                                           // 90
-              end: end,                                                                                               // 91
-              length: partSize,                                                                                       // 92
-              size: fileSize,                                                                                         // 93
-              unit: unit,                                                                                             // 94
-              partial: (partSize < fileSize)                                                                          // 95
-            };                                                                                                        // 96
-                                                                                                                      // 97
-          } else {                                                                                                    // 98
-            throw new Meteor.Error(416, "Requested Range Not Satisfiable");                                           // 99
-          }                                                                                                           // 100
-                                                                                                                      // 101
-        } else {                                                                                                      // 102
-          // The first part should be bytes                                                                           // 103
-          throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable");                                        // 104
-        }                                                                                                             // 105
-                                                                                                                      // 106
-      } else {                                                                                                        // 107
-        // No range found                                                                                             // 108
-      }                                                                                                               // 109
-                                                                                                                      // 110
-    } else {                                                                                                          // 111
-      // throw new Error('No request headers set for _parseRange function');                                          // 112
-    }                                                                                                                 // 113
-  } else {                                                                                                            // 114
-    throw new Error('No request object passed to _parseRange function');                                              // 115
-  }                                                                                                                   // 116
-                                                                                                                      // 117
-  return {                                                                                                            // 118
-    start: 0,                                                                                                         // 119
-    end: fileSize - 1,                                                                                                // 120
-    length: fileSize,                                                                                                 // 121
-    size: fileSize,                                                                                                   // 122
-    unit: 'bytes',                                                                                                    // 123
-    partial: false                                                                                                    // 124
-  };                                                                                                                  // 125
-};                                                                                                                    // 126
-                                                                                                                      // 127
-/**                                                                                                                   // 128
- * @method FS.HTTP.Handlers.Get                                                                                       // 129
- * @public                                                                                                            // 130
- * @returns {any} response                                                                                            // 131
- *                                                                                                                    // 132
- * HTTP GET request handler                                                                                           // 133
- */                                                                                                                   // 134
-FS.HTTP.Handlers.Get = function httpGetHandler(ref) {                                                                 // 135
-  var self = this;                                                                                                    // 136
-  // Once we have the file, we can test allow/deny validators                                                         // 137
-  // XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access?                                    // 138
-  FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/);  // 139
-                                                                                                                      // 140
-  var storeName = ref.storeName;                                                                                      // 141
-                                                                                                                      // 142
-  // If no storeName was specified, use the first defined storeName                                                   // 143
-  if (typeof storeName !== "string") {                                                                                // 144
-    // No store handed, we default to primary store                                                                   // 145
-    storeName = ref.collection.primaryStore.name;                                                                     // 146
-  }                                                                                                                   // 147
-                                                                                                                      // 148
-  // Get the storage reference                                                                                        // 149
-  var storage = ref.collection.storesLookup[storeName];                                                               // 150
-                                                                                                                      // 151
-  if (!storage) {                                                                                                     // 152
-    throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"');                                // 153
-  }                                                                                                                   // 154
-                                                                                                                      // 155
-  // Get the file                                                                                                     // 156
-  var copyInfo = ref.file.copies[storeName];                                                                          // 157
-                                                                                                                      // 158
-  if (!copyInfo) {                                                                                                    // 159
-    throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store');              // 160
-  }                                                                                                                   // 161
-                                                                                                                      // 162
-  // Set the content type for file                                                                                    // 163
-  if (typeof copyInfo.type === "string") {                                                                            // 164
-    self.setContentType(copyInfo.type);                                                                               // 165
-  } else {                                                                                                            // 166
-    self.setContentType('application/octet-stream');                                                                  // 167
-  }                                                                                                                   // 168
-                                                                                                                      // 169
-  // Add 'Content-Disposition' header if requested a download/attachment URL                                          // 170
-  if (typeof ref.download !== "undefined") {                                                                          // 171
-    var filename = ref.filename || copyInfo.name;                                                                     // 172
-    self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"');                                 // 173
-  } else {                                                                                                            // 174
-    self.addHeader('Content-Disposition', 'inline');                                                                  // 175
-  }                                                                                                                   // 176
-                                                                                                                      // 177
-  // Get the contents range from request                                                                              // 178
-  var range = requestRange(self.request, copyInfo.size);                                                              // 179
-                                                                                                                      // 180
-  // Some browsers cope better if the content-range header is                                                         // 181
-  // still included even for the full file being returned.                                                            // 182
-  self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);               // 183
-                                                                                                                      // 184
-  // If a chunk/range was requested instead of the whole file, serve that'                                            // 185
-  if (range.partial) {                                                                                                // 186
-    self.setStatusCode(206, 'Partial Content');                                                                       // 187
-  } else {                                                                                                            // 188
-    self.setStatusCode(200, 'OK');                                                                                    // 189
-  }                                                                                                                   // 190
-                                                                                                                      // 191
-  // Add any other global custom headers and collection-specific custom headers                                       // 192
-  FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) {            // 193
-    self.addHeader(header[0], header[1]);                                                                             // 194
-  });                                                                                                                 // 195
-                                                                                                                      // 196
-  // Inform clients about length (or chunk length in case of ranges)                                                  // 197
-  self.addHeader('Content-Length', range.length);                                                                     // 198
-                                                                                                                      // 199
-  // Last modified header (updatedAt from file info)                                                                  // 200
-  self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString());                                                  // 201
-                                                                                                                      // 202
-  // Inform clients that we accept ranges for resumable chunked downloads                                             // 203
-  self.addHeader('Accept-Ranges', range.unit);                                                                        // 204
-                                                                                                                      // 205
-  if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
-                                                                                                                      // 207
-  var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end});                  // 208
-                                                                                                                      // 209
-  readStream.on('error', function(err) {                                                                              // 210
-    // Send proper error message on get error                                                                         // 211
-    if (err.message && err.statusCode) {                                                                              // 212
-      self.Error(new Meteor.Error(err.statusCode, err.message));                                                      // 213
-    } else {                                                                                                          // 214
-      self.Error(new Meteor.Error(503, 'Service unavailable'));                                                       // 215
-    }                                                                                                                 // 216
-  });                                                                                                                 // 217
-                                                                                                                      // 218
-  readStream.pipe(self.createWriteStream());                                                                          // 219
-};                                                                                                                    // 220
-
-const originalHandler = FS.HTTP.Handlers.Get;
-FS.HTTP.Handlers.Get = function (ref) {
-//console.log(ref.filename);
-  try {
-     var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
-
-        if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
-            ref.filename =  encodeURIComponent(ref.filename);
-        } else if(userAgent.indexOf('firefox') >= 0) {
-            ref.filename = new Buffer(ref.filename).toString('binary');
-        } else {
-            /* safari*/
-            ref.filename = new Buffer(ref.filename).toString('binary');
-        }   
-   } catch (ex){
-        ref.filename = 'tempfix';
-   } 
-   return originalHandler.call(this, ref);
-};
-                                                                                                                      // 221
-/**                                                                                                                   // 222
- * @method FS.HTTP.Handlers.PutInsert                                                                                 // 223
- * @public                                                                                                            // 224
- * @returns {Object} response object with _id property                                                                // 225
- *                                                                                                                    // 226
- * HTTP PUT file insert request handler                                                                               // 227
- */                                                                                                                   // 228
-FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) {                                                     // 229
-  var self = this;                                                                                                    // 230
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 231
-                                                                                                                      // 232
-  FS.debug && console.log("HTTP PUT (insert) handler");                                                               // 233
-                                                                                                                      // 234
-  // Create the nice FS.File                                                                                          // 235
-  var fileObj = new FS.File();                                                                                        // 236
-                                                                                                                      // 237
-  // Set its name                                                                                                     // 238
-  fileObj.name(opts.filename || null);                                                                                // 239
-                                                                                                                      // 240
-  // Attach the readstream as the file's data                                                                         // 241
-  fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
-                                                                                                                      // 243
-  // Validate with insert allow/deny                                                                                  // 244
-  FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId);                        // 245
-                                                                                                                      // 246
-  // Insert file into collection, triggering readStream storage                                                       // 247
-  ref.collection.insert(fileObj);                                                                                     // 248
-                                                                                                                      // 249
-  // Send response                                                                                                    // 250
-  self.setStatusCode(200);                                                                                            // 251
-                                                                                                                      // 252
-  // Return the new file id                                                                                           // 253
-  return {_id: fileObj._id};                                                                                          // 254
-};                                                                                                                    // 255
-                                                                                                                      // 256
-/**                                                                                                                   // 257
- * @method FS.HTTP.Handlers.PutUpdate                                                                                 // 258
- * @public                                                                                                            // 259
- * @returns {Object} response object with _id and chunk properties                                                    // 260
- *                                                                                                                    // 261
- * HTTP PUT file update chunk request handler                                                                         // 262
- */                                                                                                                   // 263
-FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) {                                                     // 264
-  var self = this;                                                                                                    // 265
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 266
-                                                                                                                      // 267
-  var chunk = parseInt(opts.chunk, 10);                                                                               // 268
-  if (isNaN(chunk)) chunk = 0;                                                                                        // 269
-                                                                                                                      // 270
-  FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk);                                       // 271
-                                                                                                                      // 272
-  // Validate with insert allow/deny; also mounts and retrieves the file                                              // 273
-  FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId);                       // 274
-                                                                                                                      // 275
-  self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) );                                    // 276
-                                                                                                                      // 277
-  // Send response                                                                                                    // 278
-  self.setStatusCode(200);                                                                                            // 279
-                                                                                                                      // 280
-  return { _id: ref.file._id, chunk: chunk };                                                                         // 281
-};                                                                                                                    // 282
-                                                                                                                      // 283
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-
-
-
-
-
-(function () {
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-//                                                                                                                    //
-// packages/cfs:access-point/access-point-server.js                                                                   //
-//                                                                                                                    //
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-                                                                                                                      //
-var path = Npm.require("path");                                                                                       // 1
-                                                                                                                      // 2
-HTTP.publishFormats({                                                                                                 // 3
-  fileRecordFormat: function (input) {                                                                                // 4
-    // Set the method scope content type to json                                                                      // 5
-    this.setContentType('application/json');                                                                          // 6
-    if (FS.Utility.isArray(input)) {                                                                                  // 7
-      return EJSON.stringify(FS.Utility.map(input, function (obj) {                                                   // 8
-        return FS.Utility.cloneFileRecord(obj);                                                                       // 9
-      }));                                                                                                            // 10
-    } else {                                                                                                          // 11
-      return EJSON.stringify(FS.Utility.cloneFileRecord(input));                                                      // 12
-    }                                                                                                                 // 13
-  }                                                                                                                   // 14
-});                                                                                                                   // 15
-                                                                                                                      // 16
-/**                                                                                                                   // 17
- * @method FS.HTTP.setHeadersForGet                                                                                   // 18
- * @public                                                                                                            // 19
- * @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
- * @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
- * @returns {undefined}                                                                                               // 22
- */                                                                                                                   // 23
-FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) {                                          // 24
-  if (typeof collections === "string") {                                                                              // 25
-    collections = [collections];                                                                                      // 26
-  }                                                                                                                   // 27
-  if (collections) {                                                                                                  // 28
-    FS.Utility.each(collections, function(collectionName) {                                                           // 29
-      getHeadersByCollection[collectionName] = headers || [];                                                         // 30
-    });                                                                                                               // 31
-  } else {                                                                                                            // 32
-    getHeaders = headers || [];                                                                                       // 33
-  }                                                                                                                   // 34
-};                                                                                                                    // 35
-                                                                                                                      // 36
-/**                                                                                                                   // 37
- * @method FS.HTTP.publish                                                                                            // 38
- * @public                                                                                                            // 39
- * @param {FS.Collection} collection                                                                                  // 40
- * @param {Function} func - Publish function that returns a cursor.                                                   // 41
- * @returns {undefined}                                                                                               // 42
- *                                                                                                                    // 43
- * Publishes all documents returned by the cursor at a GET URL                                                        // 44
- * with the format baseUrl/record/collectionName. The publish                                                         // 45
- * function `this` is similar to normal `Meteor.publish`.                                                             // 46
- */                                                                                                                   // 47
-FS.HTTP.publish = function fsHttpPublish(collection, func) {                                                          // 48
-  var name = baseUrl + '/record/' + collection.name;                                                                  // 49
-  // Mount collection listing URL using http-publish package                                                          // 50
-  HTTP.publish({                                                                                                      // 51
-    name: name,                                                                                                       // 52
-    defaultFormat: 'fileRecordFormat',                                                                                // 53
-    collection: collection,                                                                                           // 54
-    collectionGet: true,                                                                                              // 55
-    collectionPost: false,                                                                                            // 56
-    documentGet: true,                                                                                                // 57
-    documentPut: false,                                                                                               // 58
-    documentDelete: false                                                                                             // 59
-  }, func);                                                                                                           // 60
-                                                                                                                      // 61
-  FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n');                    // 62
-};                                                                                                                    // 63
-                                                                                                                      // 64
-/**                                                                                                                   // 65
- * @method FS.HTTP.unpublish                                                                                          // 66
- * @public                                                                                                            // 67
- * @param {FS.Collection} collection                                                                                  // 68
- * @returns {undefined}                                                                                               // 69
- *                                                                                                                    // 70
- * Unpublishes a restpoint created by a call to `FS.HTTP.publish`                                                     // 71
- */                                                                                                                   // 72
-FS.HTTP.unpublish = function fsHttpUnpublish(collection) {                                                            // 73
-  // Mount collection listing URL using http-publish package                                                          // 74
-  HTTP.unpublish(baseUrl + '/record/' + collection.name);                                                             // 75
-};                                                                                                                    // 76
-                                                                                                                      // 77
-_existingMountPoints = {};                                                                                            // 78
-                                                                                                                      // 79
-/**                                                                                                                   // 80
- * @method defaultSelectorFunction                                                                                    // 81
- * @private                                                                                                           // 82
- * @returns { collection, file }                                                                                      // 83
- *                                                                                                                    // 84
- * This is the default selector function                                                                              // 85
- */                                                                                                                   // 86
-var defaultSelectorFunction = function() {                                                                            // 87
-  var self = this;                                                                                                    // 88
-  // Selector function                                                                                                // 89
-  //                                                                                                                  // 90
-  // This function will have to return the collection and the                                                         // 91
-  // file. If file not found undefined is returned - if null is returned the                                          // 92
-  // search was not possible                                                                                          // 93
-  var opts = FS.Utility.extend({}, self.query || {}, self.params || {});                                              // 94
-                                                                                                                      // 95
-  // Get the collection name from the url                                                                             // 96
-  var collectionName = opts.collectionName;                                                                           // 97
-                                                                                                                      // 98
-  // Get the id from the url                                                                                          // 99
-  var id = opts.id;                                                                                                   // 100
-                                                                                                                      // 101
-  // Get the collection                                                                                               // 102
-  var collection = FS._collections[collectionName];                                                                   // 103
-                                                                                                                      // 104
-  // Get the file if possible else return null                                                                        // 105
-  var file = (id && collection)? collection.findOne({ _id: id }): null;                                               // 106
-                                                                                                                      // 107
-  // Return the collection and the file                                                                               // 108
-  return {                                                                                                            // 109
-    collection: collection,                                                                                           // 110
-    file: file,                                                                                                       // 111
-    storeName: opts.store,                                                                                            // 112
-    download: opts.download,                                                                                          // 113
-    filename: opts.filename                                                                                           // 114
-  };                                                                                                                  // 115
-};                                                                                                                    // 116
-                                                                                                                      // 117
-/*                                                                                                                    // 118
- * @method FS.HTTP.mount                                                                                              // 119
- * @public                                                                                                            // 120
- * @param {array of string} mountPoints mount points to map rest functinality on                                      // 121
- * @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with      // 122
- *                                                                                                                    // 123
-*/                                                                                                                    // 124
-FS.HTTP.mount = function(mountPoints, selector_f) {                                                                   // 125
-  // We take mount points as an array and we get a selector function                                                  // 126
-  var selectorFunction = selector_f || defaultSelectorFunction;                                                       // 127
-                                                                                                                      // 128
-  var accessPoint = {                                                                                                 // 129
-    'stream': true,                                                                                                   // 130
-    'auth': expirationAuth,                                                                                           // 131
-    'post': function(data) {                                                                                          // 132
-      // Use the selector for finding the collection and file reference                                               // 133
-      var ref = selectorFunction.call(this);                                                                          // 134
-                                                                                                                      // 135
-      // We dont support post - this would be normal insert eg. of filerecord?                                        // 136
-      throw new Meteor.Error(501, "Not implemented", "Post is not supported");                                        // 137
-    },                                                                                                                // 138
-    'put': function(data) {                                                                                           // 139
-      // Use the selector for finding the collection and file reference                                               // 140
-      var ref = selectorFunction.call(this);                                                                          // 141
-                                                                                                                      // 142
-      // Make sure we have a collection reference                                                                     // 143
-      if (!ref.collection)                                                                                            // 144
-        throw new Meteor.Error(404, "Not Found", "No collection found");                                              // 145
-                                                                                                                      // 146
-      // Make sure we have a file reference                                                                           // 147
-      if (ref.file === null) {                                                                                        // 148
-        // No id supplied so we will create a new FS.File instance and                                                // 149
-        // insert the supplied data.                                                                                  // 150
-        return FS.HTTP.Handlers.PutInsert.apply(this, [ref]);                                                         // 151
-      } else {                                                                                                        // 152
-        if (ref.file) {                                                                                               // 153
-          return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]);                                                       // 154
-        } else {                                                                                                      // 155
-          throw new Meteor.Error(404, "Not Found", 'No file found');                                                  // 156
-        }                                                                                                             // 157
-      }                                                                                                               // 158
-    },                                                                                                                // 159
-    'get': function(data) {                                                                                           // 160
-      // Use the selector for finding the collection and file reference                                               // 161
-      var ref = selectorFunction.call(this);                                                                          // 162
-                                                                                                                      // 163
-      // Make sure we have a collection reference                                                                     // 164
-      if (!ref.collection)                                                                                            // 165
-        throw new Meteor.Error(404, "Not Found", "No collection found");                                              // 166
-                                                                                                                      // 167
-      // Make sure we have a file reference                                                                           // 168
-      if (ref.file === null) {                                                                                        // 169
-        // No id supplied so we will return the published list of files ala                                           // 170
-        // http.publish in json format                                                                                // 171
-        return FS.HTTP.Handlers.GetList.apply(this, [ref]);                                                           // 172
-      } else {                                                                                                        // 173
-        if (ref.file) {                                                                                               // 174
-          return FS.HTTP.Handlers.Get.apply(this, [ref]);                                                             // 175
-        } else {                                                                                                      // 176
-          throw new Meteor.Error(404, "Not Found", 'No file found');                                                  // 177
-        }                                                                                                             // 178
-      }                                                                                                               // 179
-    },                                                                                                                // 180
-    'delete': function(data) {                                                                                        // 181
-      // Use the selector for finding the collection and file reference                                               // 182
-      var ref = selectorFunction.call(this);                                                                          // 183
-                                                                                                                      // 184
-      // Make sure we have a collection reference                                                                     // 185
-      if (!ref.collection)                                                                                            // 186
-        throw new Meteor.Error(404, "Not Found", "No collection found");                                              // 187
-                                                                                                                      // 188
-      // Make sure we have a file reference                                                                           // 189
-      if (ref.file) {                                                                                                 // 190
-        return FS.HTTP.Handlers.Del.apply(this, [ref]);                                                               // 191
-      } else {                                                                                                        // 192
-        throw new Meteor.Error(404, "Not Found", 'No file found');                                                    // 193
-      }                                                                                                               // 194
-    }                                                                                                                 // 195
-  };                                                                                                                  // 196
-                                                                                                                      // 197
-  var accessPoints = {};                                                                                              // 198
-                                                                                                                      // 199
-  // Add debug message                                                                                                // 200
-  FS.debug && console.log('Registered HTTP method URLs:');                                                            // 201
-                                                                                                                      // 202
-  FS.Utility.each(mountPoints, function(mountPoint) {                                                                 // 203
-    // Couple mountpoint and accesspoint                                                                              // 204
-    accessPoints[mountPoint] = accessPoint;                                                                           // 205
-    // Remember our mountpoints                                                                                       // 206
-    _existingMountPoints[mountPoint] = mountPoint;                                                                    // 207
-    // Add debug message                                                                                              // 208
-    FS.debug && console.log(mountPoint);                                                                              // 209
-  });                                                                                                                 // 210
-                                                                                                                      // 211
-  // XXX: HTTP:methods should unmount existing mounts in case of overwriting?                                         // 212
-  HTTP.methods(accessPoints);                                                                                         // 213
-                                                                                                                      // 214
-};                                                                                                                    // 215
-                                                                                                                      // 216
-/**                                                                                                                   // 217
- * @method FS.HTTP.unmount                                                                                            // 218
- * @public                                                                                                            // 219
- * @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted           // 220
- *                                                                                                                    // 221
- */                                                                                                                   // 222
-FS.HTTP.unmount = function(mountPoints) {                                                                             // 223
-  // The mountPoints is optional, can be string or array if undefined then                                            // 224
-  // _existingMountPoints will be used                                                                                // 225
-  var unmountList;                                                                                                    // 226
-  // Container for the mount points to unmount                                                                        // 227
-  var unmountPoints = {};                                                                                             // 228
-                                                                                                                      // 229
-  if (typeof mountPoints === 'undefined') {                                                                           // 230
-    // Use existing mount points - unmount all                                                                        // 231
-    unmountList = _existingMountPoints;                                                                               // 232
-  } else if (mountPoints === ''+mountPoints) {                                                                        // 233
-    // Got a string                                                                                                   // 234
-    unmountList = [mountPoints];                                                                                      // 235
-  } else if (mountPoints.length) {                                                                                    // 236
-    // Got an array                                                                                                   // 237
-    unmountList = mountPoints;                                                                                        // 238
-  }                                                                                                                   // 239
-                                                                                                                      // 240
-  // If we have a list to unmount                                                                                     // 241
-  if (unmountList) {                                                                                                  // 242
-    // Iterate over each item                                                                                         // 243
-    FS.Utility.each(unmountList, function(mountPoint) {                                                               // 244
-      // Check _existingMountPoints to make sure the mount point exists in our                                        // 245
-      // context / was created by the FS.HTTP.mount                                                                   // 246
-      if (_existingMountPoints[mountPoint]) {                                                                         // 247
-        // Mark as unmount                                                                                            // 248
-        unmountPoints[mountPoint] = false;                                                                            // 249
-        // Release                                                                                                    // 250
-        delete _existingMountPoints[mountPoint];                                                                      // 251
-      }                                                                                                               // 252
-    });                                                                                                               // 253
-    FS.debug && console.log('FS.HTTP.unmount:');                                                                      // 254
-    FS.debug && console.log(unmountPoints);                                                                           // 255
-    // Complete unmount                                                                                               // 256
-    HTTP.methods(unmountPoints);                                                                                      // 257
-  }                                                                                                                   // 258
-};                                                                                                                    // 259
-                                                                                                                      // 260
-// ### FS.Collection maps on HTTP pr. default on the following restpoints:                                            // 261
-// *                                                                                                                  // 262
-//    baseUrl + '/files/:collectionName/:id/:filename',                                                               // 263
-//    baseUrl + '/files/:collectionName/:id',                                                                         // 264
-//    baseUrl + '/files/:collectionName'                                                                              // 265
-//                                                                                                                    // 266
-// Change/ replace the existing mount point by:                                                                       // 267
-// ```js                                                                                                              // 268
-//   // unmount all existing                                                                                          // 269
-//   FS.HTTP.unmount();                                                                                               // 270
-//   // Create new mount point                                                                                        // 271
-//   FS.HTTP.mount([                                                                                                  // 272
-//    '/cfs/files/:collectionName/:id/:filename',                                                                     // 273
-//    '/cfs/files/:collectionName/:id',                                                                               // 274
-//    '/cfs/files/:collectionName'                                                                                    // 275
-//  ]);                                                                                                               // 276
-//  ```                                                                                                               // 277
-//                                                                                                                    // 278
-mountUrls = function mountUrls() {                                                                                    // 279
-  // We unmount first in case we are calling this a second time                                                       // 280
-  FS.HTTP.unmount();                                                                                                  // 281
-                                                                                                                      // 282
-  FS.HTTP.mount([                                                                                                     // 283
-    baseUrl + '/files/:collectionName/:id/:filename',                                                                 // 284
-    baseUrl + '/files/:collectionName/:id',                                                                           // 285
-    baseUrl + '/files/:collectionName'                                                                                // 286
-  ]);                                                                                                                 // 287
-};                                                                                                                    // 288
-                                                                                                                      // 289
-// Returns the userId from URL token                                                                                  // 290
-var expirationAuth = function expirationAuth() {                                                                      // 291
-  var self = this;                                                                                                    // 292
-                                                                                                                      // 293
-  // Read the token from '/hello?token=base64'                                                                        // 294
-  var encodedToken = self.query.token;                                                                                // 295
-                                                                                                                      // 296
-  FS.debug && console.log("token: "+encodedToken);                                                                    // 297
-                                                                                                                      // 298
-  if (!encodedToken || !Meteor.users) return false;                                                                   // 299
-                                                                                                                      // 300
-  // Check the userToken before adding it to the db query                                                             // 301
-  // Set the this.userId                                                                                              // 302
-  var tokenString = FS.Utility.atob(encodedToken);                                                                    // 303
-                                                                                                                      // 304
-  var tokenObject;                                                                                                    // 305
-  try {                                                                                                               // 306
-    tokenObject = JSON.parse(tokenString);                                                                            // 307
-  } catch(err) {                                                                                                      // 308
-    throw new Meteor.Error(400, 'Bad Request');                                                                       // 309
-  }                                                                                                                   // 310
-                                                                                                                      // 311
-  // XXX: Do some check here of the object                                                                            // 312
-  var userToken = tokenObject.authToken;                                                                              // 313
-  if (userToken !== ''+userToken) {                                                                                   // 314
-    throw new Meteor.Error(400, 'Bad Request');                                                                       // 315
-  }                                                                                                                   // 316
-                                                                                                                      // 317
-  // If we have an expiration token we should check that it's still valid                                             // 318
-  if (tokenObject.expiration != null) {                                                                               // 319
-    // check if its too old                                                                                           // 320
-    var now = Date.now();                                                                                             // 321
-    if (tokenObject.expiration < now) {                                                                               // 322
-      FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now);                   // 323
-      throw new Meteor.Error(500, 'Expired token');                                                                   // 324
-    }                                                                                                                 // 325
-  }                                                                                                                   // 326
-                                                                                                                      // 327
-  // We are not on a secure line - so we have to look up the user...                                                  // 328
-  var user = Meteor.users.findOne({                                                                                   // 329
-    $or: [                                                                                                            // 330
-      {'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)},                               // 331
-      {'services.resume.loginTokens.token': userToken}                                                                // 332
-    ]                                                                                                                 // 333
-  });                                                                                                                 // 334
-                                                                                                                      // 335
-  // Set the userId in the scope                                                                                      // 336
-  return user && user._id;                                                                                            // 337
-};                                                                                                                    // 338
-                                                                                                                      // 339
-HTTP.methods(                                                                                                         // 340
-  {'/cfs/servertime': {                                                                                               // 341
-    get: function(data) {                                                                                             // 342
-      return Date.now().toString();                                                                                   // 343
-    }                                                                                                                 // 344
-  }                                                                                                                   // 345
-});                                                                                                                   // 346
-                                                                                                                      // 347
-// Unify client / server api                                                                                          // 348
-FS.HTTP.now = function() {                                                                                            // 349
-  return Date.now();                                                                                                  // 350
-};                                                                                                                    // 351
-                                                                                                                      // 352
-// Start up the basic mount points                                                                                    // 353
-Meteor.startup(function () {                                                                                          // 354
-  mountUrls();                                                                                                        // 355
-});                                                                                                                   // 356
-                                                                                                                      // 357
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-///////////////////////////////////////////////////////////////////////
-
-}).call(this);
-
-
-/* Exports */
-if (typeof Package === 'undefined') Package = {};
-Package['cfs:access-point'] = {};
-
-})();

+ 0 - 238
.sandstorm-meteor-1.8/export.js

@@ -1,238 +0,0 @@
-/* global JsonRoutes */
-if (Meteor.isServer) {
-  // todo XXX once we have a real API in place, move that route there
-  // todo XXX also  share the route definition between the client and the server
-  // so that we could use something like
-  // `ApiRoutes.path('boards/export', boardId)``
-  // on the client instead of copy/pasting the route path manually between the
-  // client and the server.
-  /**
-   * @operation export
-   * @tag Boards
-   *
-   * @summary This route is used to export the board.
-   *
-   * @description If user is already logged-in, pass loginToken as param
-   * "authToken": '/api/boards/:boardId/export?authToken=:token'
-   *
-   * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
-   * for detailed explanations
-   *
-   * @param {string} boardId the ID of the board we are exporting
-   * @param {string} authToken the loginToken
-   */
-  JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) {
-    const boardId = req.params.boardId;
-    let user = null;
-
-    const loginToken = req.query.authToken;
-    if (loginToken) {
-      const hashToken = Accounts._hashLoginToken(loginToken);
-      user = Meteor.users.findOne({
-        'services.resume.loginTokens.hashedToken': hashToken,
-      });
-    } else if (!Meteor.settings.public.sandstorm) {
-      Authentication.checkUserId(req.userId);
-      user = Users.findOne({ _id: req.userId, isAdmin: true });
-    }
-
-    const exporter = new Exporter(boardId);
-    if (exporter.canExport(user)) {
-      JsonRoutes.sendResult(res, {
-        code: 200,
-        data: exporter.build(),
-      });
-    } else {
-      // we could send an explicit error message, but on the other hand the only
-      // way to get there is by hacking the UI so let's keep it raw.
-      JsonRoutes.sendResult(res, 403);
-    }
-  });
-}
-
-// exporter maybe is broken since Gridfs introduced, add fs and path
-
-export class Exporter {
-  constructor(boardId) {
-    this._boardId = boardId;
-  }
-
-  build() {
-    const fs = Npm.require('fs');
-    const os = Npm.require('os');
-    const path = Npm.require('path');
-
-    const byBoard = { boardId: this._boardId };
-    const byBoardNoLinked = {
-      boardId: this._boardId,
-      linkedId: { $in: ['', null] },
-    };
-    // we do not want to retrieve boardId in related elements
-    const noBoardId = {
-      fields: {
-        boardId: 0,
-      },
-    };
-    const result = {
-      _format: 'wekan-board-1.0.0',
-    };
-    _.extend(
-      result,
-      Boards.findOne(this._boardId, {
-        fields: {
-          stars: 0,
-        },
-      }),
-    );
-    result.lists = Lists.find(byBoard, noBoardId).fetch();
-    result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
-    result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
-    result.customFields = CustomFields.find(
-      { boardIds: { $in: [this.boardId] } },
-      { fields: { boardId: 0 } },
-    ).fetch();
-    result.comments = CardComments.find(byBoard, noBoardId).fetch();
-    result.activities = Activities.find(byBoard, noBoardId).fetch();
-    result.rules = Rules.find(byBoard, noBoardId).fetch();
-    result.checklists = [];
-    result.checklistItems = [];
-    result.subtaskItems = [];
-    result.triggers = [];
-    result.actions = [];
-    result.cards.forEach(card => {
-      result.checklists.push(
-        ...Checklists.find({
-          cardId: card._id,
-        }).fetch(),
-      );
-      result.checklistItems.push(
-        ...ChecklistItems.find({
-          cardId: card._id,
-        }).fetch(),
-      );
-      result.subtaskItems.push(
-        ...Cards.find({
-          parentId: card._id,
-        }).fetch(),
-      );
-    });
-    result.rules.forEach(rule => {
-      result.triggers.push(
-        ...Triggers.find(
-          {
-            _id: rule.triggerId,
-          },
-          noBoardId,
-        ).fetch(),
-      );
-      result.actions.push(
-        ...Actions.find(
-          {
-            _id: rule.actionId,
-          },
-          noBoardId,
-        ).fetch(),
-      );
-    });
-
-    // [Old] for attachments we only export IDs and absolute url to original doc
-    // [New] Encode attachment to base64
-    const getBase64Data = function(doc, callback) {
-      let buffer = new Buffer(0);
-      // callback has the form function (err, res) {}
-      const tmpFile = path.join(
-        os.tmpdir(),
-        `tmpexport${process.pid}${Math.random()}`,
-      );
-      const tmpWriteable = fs.createWriteStream(tmpFile);
-      const readStream = doc.createReadStream();
-      readStream.on('data', function(chunk) {
-        buffer = Buffer.concat([buffer, chunk]);
-      });
-      readStream.on('error', function(err) {
-        callback(err, null);
-      });
-      readStream.on('end', function() {
-        // done
-        fs.unlink(tmpFile, () => {
-          //ignored
-        });
-        callback(null, buffer.toString('base64'));
-      });
-      readStream.pipe(tmpWriteable);
-    };
-    const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
-    result.attachments = Attachments.find(byBoard)
-      .fetch()
-      .map(attachment => {
-        return {
-          _id: attachment._id,
-          cardId: attachment.cardId,
-          // url: FlowRouter.url(attachment.url()),
-          file: getBase64DataSync(attachment),
-          name: attachment.original.name,
-          type: attachment.original.type,
-        };
-      });
-
-    // we also have to export some user data - as the other elements only
-    // include id but we have to be careful:
-    // 1- only exports users that are linked somehow to that board
-    // 2- do not export any sensitive information
-    const users = {};
-    result.members.forEach(member => {
-      users[member.userId] = true;
-    });
-    result.lists.forEach(list => {
-      users[list.userId] = true;
-    });
-    result.cards.forEach(card => {
-      users[card.userId] = true;
-      if (card.members) {
-        card.members.forEach(memberId => {
-          users[memberId] = true;
-        });
-      }
-    });
-    result.comments.forEach(comment => {
-      users[comment.userId] = true;
-    });
-    result.activities.forEach(activity => {
-      users[activity.userId] = true;
-    });
-    result.checklists.forEach(checklist => {
-      users[checklist.userId] = true;
-    });
-    const byUserIds = {
-      _id: {
-        $in: Object.getOwnPropertyNames(users),
-      },
-    };
-    // we use whitelist to be sure we do not expose inadvertently
-    // some secret fields that gets added to User later.
-    const userFields = {
-      fields: {
-        _id: 1,
-        username: 1,
-        'profile.fullname': 1,
-        'profile.initials': 1,
-        'profile.avatarUrl': 1,
-      },
-    };
-    result.users = Users.find(byUserIds, userFields)
-      .fetch()
-      .map(user => {
-        // user avatar is stored as a relative url, we export absolute
-        if ((user.profile || {}).avatarUrl) {
-          user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
-        }
-        return user;
-      });
-    return result;
-  }
-
-  canExport(user) {
-    const board = Boards.findOne(this._boardId);
-    return board && board.isVisibleBy(user);
-  }
-}

+ 0 - 640
.sandstorm-meteor-1.8/ldap.js

@@ -1,640 +0,0 @@
-import ldapjs from 'ldapjs';
-import util from 'util';
-import Bunyan from 'bunyan';
-import { log_debug, log_info, log_warn, log_error } from './logger';
-
-export default class LDAP {
-  constructor() {
-    this.ldapjs = ldapjs;
-
-    this.connected = false;
-
-    this.options = {
-      host: this.constructor.settings_get('LDAP_HOST'),
-      port: this.constructor.settings_get('LDAP_PORT'),
-      Reconnect: this.constructor.settings_get('LDAP_RECONNECT'),
-      timeout: this.constructor.settings_get('LDAP_TIMEOUT'),
-      connect_timeout: this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),
-      idle_timeout: this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),
-      encryption: this.constructor.settings_get('LDAP_ENCRYPTION'),
-      ca_cert: this.constructor.settings_get('LDAP_CA_CERT'),
-      reject_unauthorized:
-        this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') || false,
-      Authentication: this.constructor.settings_get('LDAP_AUTHENTIFICATION'),
-      Authentication_UserDN: this.constructor.settings_get(
-        'LDAP_AUTHENTIFICATION_USERDN',
-      ),
-      Authentication_Password: this.constructor.settings_get(
-        'LDAP_AUTHENTIFICATION_PASSWORD',
-      ),
-      Authentication_Fallback: this.constructor.settings_get(
-        'LDAP_LOGIN_FALLBACK',
-      ),
-      BaseDN: this.constructor.settings_get('LDAP_BASEDN'),
-      Internal_Log_Level: this.constructor.settings_get('INTERNAL_LOG_LEVEL'),
-      User_Authentication: this.constructor.settings_get(
-        'LDAP_USER_AUTHENTICATION',
-      ),
-      User_Authentication_Field: this.constructor.settings_get(
-        'LDAP_USER_AUTHENTICATION_FIELD',
-      ),
-      User_Attributes: this.constructor.settings_get('LDAP_USER_ATTRIBUTES'),
-      User_Search_Filter: this.constructor.settings_get(
-        'LDAP_USER_SEARCH_FILTER',
-      ),
-      User_Search_Scope: this.constructor.settings_get(
-        'LDAP_USER_SEARCH_SCOPE',
-      ),
-      User_Search_Field: this.constructor.settings_get(
-        'LDAP_USER_SEARCH_FIELD',
-      ),
-      Search_Page_Size: this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),
-      Search_Size_Limit: this.constructor.settings_get(
-        'LDAP_SEARCH_SIZE_LIMIT',
-      ),
-      group_filter_enabled: this.constructor.settings_get(
-        'LDAP_GROUP_FILTER_ENABLE',
-      ),
-      group_filter_object_class: this.constructor.settings_get(
-        'LDAP_GROUP_FILTER_OBJECTCLASS',
-      ),
-      group_filter_group_id_attribute: this.constructor.settings_get(
-        'LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE',
-      ),
-      group_filter_group_member_attribute: this.constructor.settings_get(
-        'LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE',
-      ),
-      group_filter_group_member_format: this.constructor.settings_get(
-        'LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT',
-      ),
-      group_filter_group_name: this.constructor.settings_get(
-        'LDAP_GROUP_FILTER_GROUP_NAME',
-      ),
-    };
-  }
-
-  static settings_get(name, ...args) {
-    let value = process.env[name];
-    if (value !== undefined) {
-      if (value === 'true' || value === 'false') {
-        value = JSON.parse(value);
-      } else if (value !== '' && !isNaN(value)) {
-        value = Number(value);
-      }
-      return value;
-    } else {
-      log_warn(`Lookup for unset variable: ${name}`);
-    }
-  }
-
-  connectSync(...args) {
-    if (!this._connectSync) {
-      this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
-    }
-    return this._connectSync(...args);
-  }
-
-  searchAllSync(...args) {
-    if (!this._searchAllSync) {
-      this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
-    }
-    return this._searchAllSync(...args);
-  }
-
-  connectAsync(callback) {
-    log_info('Init setup');
-
-    let replied = false;
-
-    const connectionOptions = {
-      url: `${this.options.host}:${this.options.port}`,
-      timeout: this.options.timeout,
-      connectTimeout: this.options.connect_timeout,
-      idleTimeout: this.options.idle_timeout,
-      reconnect: this.options.Reconnect,
-    };
-
-    if (this.options.Internal_Log_Level !== 'disabled') {
-      connectionOptions.log = new Bunyan({
-        name: 'ldapjs',
-        component: 'client',
-        stream: process.stderr,
-        level: this.options.Internal_Log_Level,
-      });
-    }
-
-    const tlsOptions = {
-      rejectUnauthorized: this.options.reject_unauthorized,
-    };
-
-    if (this.options.ca_cert && this.options.ca_cert !== '') {
-      // Split CA cert into array of strings
-      const chainLines = this.constructor
-        .settings_get('LDAP_CA_CERT')
-        .split('\n');
-      let cert = [];
-      const ca = [];
-      chainLines.forEach(line => {
-        cert.push(line);
-        if (line.match(/-END CERTIFICATE-/)) {
-          ca.push(cert.join('\n'));
-          cert = [];
-        }
-      });
-      tlsOptions.ca = ca;
-    }
-
-    if (this.options.encryption === 'ssl') {
-      connectionOptions.url = `ldaps://${connectionOptions.url}`;
-      connectionOptions.tlsOptions = tlsOptions;
-    } else {
-      connectionOptions.url = `ldap://${connectionOptions.url}`;
-    }
-
-    log_info('Connecting', connectionOptions.url);
-    log_debug(`connectionOptions${util.inspect(connectionOptions)}`);
-
-    this.client = ldapjs.createClient(connectionOptions);
-
-    this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
-
-    this.client.on('error', error => {
-      log_error('connection', error);
-      if (replied === false) {
-        replied = true;
-        callback(error, null);
-      }
-    });
-
-    this.client.on('idle', () => {
-      log_info('Idle');
-      this.disconnect();
-    });
-
-    this.client.on('close', () => {
-      log_info('Closed');
-    });
-
-    if (this.options.encryption === 'tls') {
-      // Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
-      // https://github.com/RocketChat/Rocket.Chat/issues/2035
-      // https://github.com/mcavage/node-ldapjs/issues/349
-      tlsOptions.host = this.options.host;
-
-      log_info('Starting TLS');
-      log_debug('tlsOptions', tlsOptions);
-
-      this.client.starttls(tlsOptions, null, (error, response) => {
-        if (error) {
-          log_error('TLS connection', error);
-          if (replied === false) {
-            replied = true;
-            callback(error, null);
-          }
-          return;
-        }
-
-        log_info('TLS connected');
-        this.connected = true;
-        if (replied === false) {
-          replied = true;
-          callback(null, response);
-        }
-      });
-    } else {
-      this.client.on('connect', response => {
-        log_info('LDAP connected');
-        this.connected = true;
-        if (replied === false) {
-          replied = true;
-          callback(null, response);
-        }
-      });
-    }
-
-    setTimeout(() => {
-      if (replied === false) {
-        log_error('connection time out', connectionOptions.connectTimeout);
-        replied = true;
-        callback(new Error('Timeout'));
-      }
-    }, connectionOptions.connectTimeout);
-  }
-
-  getUserFilter(username) {
-    const filter = [];
-
-    if (this.options.User_Search_Filter !== '') {
-      if (this.options.User_Search_Filter[0] === '(') {
-        filter.push(`${this.options.User_Search_Filter}`);
-      } else {
-        filter.push(`(${this.options.User_Search_Filter})`);
-      }
-    }
-
-    const usernameFilter = this.options.User_Search_Field.split(',').map(
-      item => `(${item}=${username})`,
-    );
-
-    if (usernameFilter.length === 0) {
-      log_error('LDAP_LDAP_User_Search_Field not defined');
-    } else if (usernameFilter.length === 1) {
-      filter.push(`${usernameFilter[0]}`);
-    } else {
-      filter.push(`(|${usernameFilter.join('')})`);
-    }
-
-    return `(&${filter.join('')})`;
-  }
-
-  bindUserIfNecessary(username, password) {
-    if (this.domainBinded === true) {
-      return;
-    }
-
-    if (!this.options.User_Authentication) {
-      return;
-    }
-
-    if (!this.options.BaseDN) throw new Error('BaseDN is not provided');
-
-    const userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;
-
-    this.bindSync(userDn, password);
-    this.domainBinded = true;
-  }
-
-  bindIfNecessary() {
-    if (this.domainBinded === true) {
-      return;
-    }
-
-    if (this.options.Authentication !== true) {
-      return;
-    }
-
-    log_info('Binding UserDN', this.options.Authentication_UserDN);
-
-    this.bindSync(
-      this.options.Authentication_UserDN,
-      this.options.Authentication_Password,
-    );
-    this.domainBinded = true;
-  }
-
-  searchUsersSync(username, page) {
-    this.bindIfNecessary();
-    const searchOptions = {
-      filter: this.getUserFilter(username),
-      scope: this.options.User_Search_Scope || 'sub',
-      sizeLimit: this.options.Search_Size_Limit,
-    };
-
-    if (!!this.options.User_Attributes)
-      searchOptions.attributes = this.options.User_Attributes.split(',');
-
-    if (this.options.Search_Page_Size > 0) {
-      searchOptions.paged = {
-        pageSize: this.options.Search_Page_Size,
-        pagePause: !!page,
-      };
-    }
-
-    log_info('Searching user', username);
-    log_debug('searchOptions', searchOptions);
-    log_debug('BaseDN', this.options.BaseDN);
-
-    if (page) {
-      return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
-    }
-
-    return this.searchAllSync(this.options.BaseDN, searchOptions);
-  }
-
-  getUserByIdSync(id, attribute) {
-    this.bindIfNecessary();
-
-    const Unique_Identifier_Field = this.constructor
-      .settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD')
-      .split(',');
-
-    let filter;
-
-    if (attribute) {
-      filter = new this.ldapjs.filters.EqualityFilter({
-        attribute,
-        value: new Buffer(id, 'hex'),
-      });
-    } else {
-      const filters = [];
-      Unique_Identifier_Field.forEach(item => {
-        filters.push(
-          new this.ldapjs.filters.EqualityFilter({
-            attribute: item,
-            value: new Buffer(id, 'hex'),
-          }),
-        );
-      });
-
-      filter = new this.ldapjs.filters.OrFilter({ filters });
-    }
-
-    const searchOptions = {
-      filter,
-      scope: 'sub',
-    };
-
-    log_info('Searching by id', id);
-    log_debug('search filter', searchOptions.filter.toString());
-    log_debug('BaseDN', this.options.BaseDN);
-
-    const result = this.searchAllSync(this.options.BaseDN, searchOptions);
-
-    if (!Array.isArray(result) || result.length === 0) {
-      return;
-    }
-
-    if (result.length > 1) {
-      log_error('Search by id', id, 'returned', result.length, 'records');
-    }
-
-    return result[0];
-  }
-
-  getUserByUsernameSync(username) {
-    this.bindIfNecessary();
-
-    const searchOptions = {
-      filter: this.getUserFilter(username),
-      scope: this.options.User_Search_Scope || 'sub',
-    };
-
-    log_info('Searching user', username);
-    log_debug('searchOptions', searchOptions);
-    log_debug('BaseDN', this.options.BaseDN);
-
-    const result = this.searchAllSync(this.options.BaseDN, searchOptions);
-
-    if (!Array.isArray(result) || result.length === 0) {
-      return;
-    }
-
-    if (result.length > 1) {
-      log_error(
-        'Search by username',
-        username,
-        'returned',
-        result.length,
-        'records',
-      );
-    }
-
-    return result[0];
-  }
-
-  getUserGroups(username, ldapUser) {
-    if (!this.options.group_filter_enabled) {
-      return true;
-    }
-
-    const filter = ['(&'];
-
-    if (this.options.group_filter_object_class !== '') {
-      filter.push(`(objectclass=${this.options.group_filter_object_class})`);
-    }
-
-    if (this.options.group_filter_group_member_attribute !== '') {
-      const format_value =
-        ldapUser[this.options.group_filter_group_member_format];
-      if (format_value) {
-        filter.push(
-          `(${this.options.group_filter_group_member_attribute}=${format_value})`,
-        );
-      }
-    }
-
-    filter.push(')');
-
-    const searchOptions = {
-      filter: filter.join('').replace(/#{username}/g, username),
-      scope: 'sub',
-    };
-
-    log_debug('Group list filter LDAP:', searchOptions.filter);
-
-    const result = this.searchAllSync(this.options.BaseDN, searchOptions);
-
-    if (!Array.isArray(result) || result.length === 0) {
-      return [];
-    }
-
-    const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
-    const groups = [];
-    result.map(item => {
-      groups.push(item[grp_identifier]);
-    });
-    log_debug(`Groups: ${groups.join(', ')}`);
-    return groups;
-  }
-
-  isUserInGroup(username, ldapUser) {
-    if (!this.options.group_filter_enabled) {
-      return true;
-    }
-
-    const grps = this.getUserGroups(username, ldapUser);
-
-    const filter = ['(&'];
-
-    if (this.options.group_filter_object_class !== '') {
-      filter.push(`(objectclass=${this.options.group_filter_object_class})`);
-    }
-
-    if (this.options.group_filter_group_member_attribute !== '') {
-      const format_value =
-        ldapUser[this.options.group_filter_group_member_format];
-      if (format_value) {
-        filter.push(
-          `(${this.options.group_filter_group_member_attribute}=${format_value})`,
-        );
-      }
-    }
-
-    if (this.options.group_filter_group_id_attribute !== '') {
-      filter.push(
-        `(${this.options.group_filter_group_id_attribute}=${this.options.group_filter_group_name})`,
-      );
-    }
-    filter.push(')');
-
-    const searchOptions = {
-      filter: filter.join('').replace(/#{username}/g, username),
-      scope: 'sub',
-    };
-
-    log_debug('Group filter LDAP:', searchOptions.filter);
-
-    const result = this.searchAllSync(this.options.BaseDN, searchOptions);
-
-    if (!Array.isArray(result) || result.length === 0) {
-      return false;
-    }
-    return true;
-  }
-
-  extractLdapEntryData(entry) {
-    const values = {
-      _raw: entry.raw,
-    };
-
-    Object.keys(values._raw).forEach(key => {
-      const value = values._raw[key];
-
-      if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
-        if (value instanceof Buffer) {
-          values[key] = value.toString();
-        } else {
-          values[key] = value;
-        }
-      }
-    });
-
-    return values;
-  }
-
-  searchAllPaged(BaseDN, options, page) {
-    this.bindIfNecessary();
-
-    const processPage = ({ entries, title, end, next }) => {
-      log_info(title);
-      // Force LDAP idle to wait the record processing
-      this.client._updateIdle(true);
-      page(null, entries, {
-        end,
-        next: () => {
-          // Reset idle timer
-          this.client._updateIdle();
-          next && next();
-        },
-      });
-    };
-
-    this.client.search(BaseDN, options, (error, res) => {
-      if (error) {
-        log_error(error);
-        page(error);
-        return;
-      }
-
-      res.on('error', error => {
-        log_error(error);
-        page(error);
-        return;
-      });
-
-      let entries = [];
-
-      const internalPageSize =
-        options.paged && options.paged.pageSize > 0
-          ? options.paged.pageSize * 2
-          : 500;
-
-      res.on('searchEntry', entry => {
-        entries.push(this.extractLdapEntryData(entry));
-
-        if (entries.length >= internalPageSize) {
-          processPage({
-            entries,
-            title: 'Internal Page',
-            end: false,
-          });
-          entries = [];
-        }
-      });
-
-      res.on('page', (result, next) => {
-        if (!next) {
-          this.client._updateIdle(true);
-          processPage({
-            entries,
-            title: 'Final Page',
-            end: true,
-          });
-        } else if (entries.length) {
-          log_info('Page');
-          processPage({
-            entries,
-            title: 'Page',
-            end: false,
-            next,
-          });
-          entries = [];
-        }
-      });
-
-      res.on('end', () => {
-        if (entries.length) {
-          processPage({
-            entries,
-            title: 'Final Page',
-            end: true,
-          });
-          entries = [];
-        }
-      });
-    });
-  }
-
-  searchAllAsync(BaseDN, options, callback) {
-    this.bindIfNecessary();
-
-    this.client.search(BaseDN, options, (error, res) => {
-      if (error) {
-        log_error(error);
-        callback(error);
-        return;
-      }
-
-      res.on('error', error => {
-        log_error(error);
-        callback(error);
-        return;
-      });
-
-      const entries = [];
-
-      res.on('searchEntry', entry => {
-        entries.push(this.extractLdapEntryData(entry));
-      });
-
-      res.on('end', () => {
-        log_info('Search result count', entries.length);
-        callback(null, entries);
-      });
-    });
-  }
-
-  authSync(dn, password) {
-    log_info('Authenticating', dn);
-
-    try {
-      if (password === '') {
-        throw new Error('Password is not provided');
-      }
-      this.bindSync(dn, password);
-      log_info('Authenticated', dn);
-      return true;
-    } catch (error) {
-      log_info('Not authenticated', dn);
-      log_debug('error', error);
-      return false;
-    }
-  }
-
-  disconnect() {
-    this.connected = false;
-    this.domainBinded = false;
-    log_info('Disconecting');
-    this.client.unbind();
-  }
-}

+ 0 - 163
.sandstorm-meteor-1.8/oidc_server.js

@@ -1,163 +0,0 @@
-Oidc = {};
-
-OAuth.registerService('oidc', 2, null, function(query) {
-  var debug = process.env.DEBUG || false;
-  var token = getToken(query);
-  if (debug) console.log('XXX: register token:', token);
-
-  var accessToken = token.access_token || token.id_token;
-  var expiresAt = +new Date() + 1000 * parseInt(token.expires_in, 10);
-
-  var userinfo = getUserInfo(accessToken);
-  if (debug) console.log('XXX: userinfo:', userinfo);
-
-  var serviceData = {};
-  serviceData.id = userinfo[process.env.OAUTH2_ID_MAP]; // || userinfo["id"];
-  serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP]; // || userinfo["uid"];
-  serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
-  serviceData.accessToken = accessToken;
-  serviceData.expiresAt = expiresAt;
-  serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
-
-  if (accessToken) {
-    var tokenContent = getTokenContent(accessToken);
-    var fields = _.pick(
-      tokenContent,
-      getConfiguration().idTokenWhitelistFields,
-    );
-    _.extend(serviceData, fields);
-  }
-
-  if (token.refresh_token) serviceData.refreshToken = token.refresh_token;
-  if (debug) console.log('XXX: serviceData:', serviceData);
-
-  var profile = {};
-  profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
-  profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
-  if (debug) console.log('XXX: profile:', profile);
-
-  return {
-    serviceData: serviceData,
-    options: { profile: profile },
-  };
-});
-
-var userAgent = 'Meteor';
-if (Meteor.release) {
-  userAgent += '/' + Meteor.release;
-}
-
-var getToken = function(query) {
-  var debug = process.env.DEBUG || false;
-  var config = getConfiguration();
-  if (config.tokenEndpoint.includes('https://')) {
-    var serverTokenEndpoint = config.tokenEndpoint;
-  } else {
-    var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
-  }
-  var requestPermissions = config.requestPermissions;
-  var response;
-
-  try {
-    response = HTTP.post(serverTokenEndpoint, {
-      headers: {
-        Accept: 'application/json',
-        'User-Agent': userAgent,
-      },
-      params: {
-        code: query.code,
-        client_id: config.clientId,
-        client_secret: OAuth.openSecret(config.secret),
-        redirect_uri: OAuth._redirectUri('oidc', config),
-        grant_type: 'authorization_code',
-        scope: requestPermissions,
-        state: query.state,
-      },
-    });
-  } catch (err) {
-    throw _.extend(
-      new Error(
-        'Failed to get token from OIDC ' +
-          serverTokenEndpoint +
-          ': ' +
-          err.message,
-      ),
-      { response: err.response },
-    );
-  }
-  if (response.data.error) {
-    // if the http response was a json object with an error attribute
-    throw new Error(
-      'Failed to complete handshake with OIDC ' +
-        serverTokenEndpoint +
-        ': ' +
-        response.data.error,
-    );
-  } else {
-    if (debug) console.log('XXX: getToken response: ', response.data);
-    return response.data;
-  }
-};
-
-var getUserInfo = function(accessToken) {
-  var debug = process.env.DEBUG || false;
-  var config = getConfiguration();
-  // Some userinfo endpoints use a different base URL than the authorization or token endpoints.
-  // This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
-  if (config.userinfoEndpoint.includes('https://')) {
-    var serverUserinfoEndpoint = config.userinfoEndpoint;
-  } else {
-    var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
-  }
-  var response;
-  try {
-    response = HTTP.get(serverUserinfoEndpoint, {
-      headers: {
-        'User-Agent': userAgent,
-        Authorization: 'Bearer ' + accessToken,
-      },
-    });
-  } catch (err) {
-    throw _.extend(
-      new Error(
-        'Failed to fetch userinfo from OIDC ' +
-          serverUserinfoEndpoint +
-          ': ' +
-          err.message,
-      ),
-      { response: err.response },
-    );
-  }
-  if (debug) console.log('XXX: getUserInfo response: ', response.data);
-  return response.data;
-};
-
-var getConfiguration = function() {
-  var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
-  if (!config) {
-    throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
-  }
-  return config;
-};
-
-var getTokenContent = function(token) {
-  var content = null;
-  if (token) {
-    try {
-      var parts = token.split('.');
-      var header = JSON.parse(new Buffer(parts[0], 'base64').toString());
-      content = JSON.parse(new Buffer(parts[1], 'base64').toString());
-      var signature = new Buffer(parts[2], 'base64');
-      var signed = parts[0] + '.' + parts[1];
-    } catch (err) {
-      this.content = {
-        exp: 0,
-      };
-    }
-  }
-  return content;
-};
-
-Oidc.retrieveCredential = function(credentialToken, credentialSecret) {
-  return OAuth.retrieveCredential(credentialToken, credentialSecret);
-};

+ 0 - 4361
.sandstorm-meteor-1.8/package-lock.json

@@ -1,4361 +0,0 @@
-{
-  "name": "wekan",
-  "version": "v3.90.0",
-  "lockfileVersion": 1,
-  "requires": true,
-  "dependencies": {
-    "@babel/code-frame": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
-      "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
-      "dev": true,
-      "requires": {
-        "@babel/highlight": "^7.8.3"
-      }
-    },
-    "@babel/highlight": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
-      "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
-      "dev": true,
-      "requires": {
-        "chalk": "^2.0.0",
-        "esutils": "^2.0.2",
-        "js-tokens": "^4.0.0"
-      }
-    },
-    "@babel/runtime": {
-      "version": "7.8.7",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.7.tgz",
-      "integrity": "sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==",
-      "requires": {
-        "regenerator-runtime": "^0.13.4"
-      }
-    },
-    "@samverschueren/stream-to-observable": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
-      "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==",
-      "dev": true,
-      "requires": {
-        "any-observable": "^0.3.0"
-      }
-    },
-    "@types/color-name": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
-      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
-      "dev": true
-    },
-    "@types/eslint-visitor-keys": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
-      "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
-      "dev": true
-    },
-    "@types/json-schema": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
-      "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
-      "dev": true
-    },
-    "@types/parse-json": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
-      "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
-      "dev": true
-    },
-    "@typescript-eslint/experimental-utils": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
-      "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
-      "dev": true,
-      "requires": {
-        "@types/json-schema": "^7.0.3",
-        "@typescript-eslint/typescript-estree": "1.13.0",
-        "eslint-scope": "^4.0.0"
-      },
-      "dependencies": {
-        "eslint-scope": {
-          "version": "4.0.3",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
-          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
-          "dev": true,
-          "requires": {
-            "esrecurse": "^4.1.0",
-            "estraverse": "^4.1.1"
-          }
-        }
-      }
-    },
-    "@typescript-eslint/parser": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz",
-      "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==",
-      "dev": true,
-      "requires": {
-        "@types/eslint-visitor-keys": "^1.0.0",
-        "@typescript-eslint/experimental-utils": "1.13.0",
-        "@typescript-eslint/typescript-estree": "1.13.0",
-        "eslint-visitor-keys": "^1.0.0"
-      }
-    },
-    "@typescript-eslint/typescript-estree": {
-      "version": "1.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
-      "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
-      "dev": true,
-      "requires": {
-        "lodash.unescape": "4.0.1",
-        "semver": "5.5.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
-          "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
-          "dev": true
-        }
-      }
-    },
-    "abbrev": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
-    },
-    "acorn": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
-      "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
-      "dev": true
-    },
-    "acorn-jsx": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
-      "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
-      "dev": true
-    },
-    "ajv": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-      "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
-      "requires": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      }
-    },
-    "ansi-escapes": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
-      "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
-      "dev": true,
-      "requires": {
-        "type-fest": "^0.11.0"
-      },
-      "dependencies": {
-        "type-fest": {
-          "version": "0.11.0",
-          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
-          "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
-          "dev": true
-        }
-      }
-    },
-    "ansi-regex": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
-    },
-    "ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
-      "requires": {
-        "color-convert": "^1.9.0"
-      }
-    },
-    "any-observable": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
-      "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
-      "dev": true
-    },
-    "aproba": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
-      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
-    },
-    "are-we-there-yet": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
-      "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
-      "requires": {
-        "delegates": "^1.0.0",
-        "readable-stream": "^2.0.6"
-      }
-    },
-    "argparse": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
-      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
-      "requires": {
-        "sprintf-js": "~1.0.2"
-      }
-    },
-    "array-includes": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz",
-      "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.17.0",
-        "is-string": "^1.0.5"
-      }
-    },
-    "array.prototype.flat": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz",
-      "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.17.0-next.1"
-      }
-    },
-    "asn1": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
-      "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
-    },
-    "assert-plus": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
-    },
-    "astral-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
-      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
-      "dev": true
-    },
-    "babel-runtime": {
-      "version": "6.26.0",
-      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
-      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
-      "requires": {
-        "core-js": "^2.4.0",
-        "regenerator-runtime": "^0.11.0"
-      },
-      "dependencies": {
-        "regenerator-runtime": {
-          "version": "0.11.1",
-          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
-          "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
-        }
-      }
-    },
-    "backoff": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
-      "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
-      "requires": {
-        "precond": "0.2"
-      }
-    },
-    "balanced-match": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
-    },
-    "base64-js": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
-      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
-    },
-    "bcrypt": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-4.0.1.tgz",
-      "integrity": "sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ==",
-      "requires": {
-        "node-addon-api": "^2.0.0",
-        "node-pre-gyp": "0.14.0"
-      }
-    },
-    "bl": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz",
-      "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==",
-      "requires": {
-        "readable-stream": "^2.3.5",
-        "safe-buffer": "^5.1.1"
-      }
-    },
-    "brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "requires": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
-      "requires": {
-        "fill-range": "^7.0.1"
-      }
-    },
-    "bson": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/bson/-/bson-4.0.3.tgz",
-      "integrity": "sha512-7uBjjxwOSuGLmoqGI1UXWpDGc0K2WjR7dC6iaOg4iriNZo6M2EEBb8co4dEPJ5ArYCebPMie0ecgX0TWF+ZUrQ==",
-      "requires": {
-        "buffer": "^5.1.0",
-        "long": "^4.0.0"
-      }
-    },
-    "buffer": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz",
-      "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==",
-      "requires": {
-        "base64-js": "^1.0.2",
-        "ieee754": "^1.1.4"
-      }
-    },
-    "buffer-from": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
-      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
-    },
-    "bunyan": {
-      "version": "1.8.12",
-      "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
-      "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
-      "requires": {
-        "dtrace-provider": "~0.8",
-        "moment": "^2.10.6",
-        "mv": "~2",
-        "safe-json-stringify": "~1"
-      }
-    },
-    "callsites": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true
-    },
-    "chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
-      "requires": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      }
-    },
-    "chardet": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
-      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
-      "dev": true
-    },
-    "chownr": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
-      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
-    },
-    "cli-cursor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
-      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
-      "dev": true,
-      "requires": {
-        "restore-cursor": "^3.1.0"
-      }
-    },
-    "cli-truncate": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
-      "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
-      "dev": true,
-      "requires": {
-        "slice-ansi": "0.0.4",
-        "string-width": "^1.0.1"
-      },
-      "dependencies": {
-        "slice-ansi": {
-          "version": "0.0.4",
-          "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
-          "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
-          "dev": true
-        }
-      }
-    },
-    "cli-width": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
-      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
-      "dev": true
-    },
-    "code-point-at": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
-      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
-    },
-    "color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
-      "requires": {
-        "color-name": "1.1.3"
-      }
-    },
-    "color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-      "dev": true
-    },
-    "commander": {
-      "version": "2.20.3",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
-    },
-    "common-tags": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
-      "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==",
-      "dev": true
-    },
-    "concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
-    },
-    "concat-stream": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
-      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
-      "dev": true,
-      "requires": {
-        "buffer-from": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^2.2.2",
-        "typedarray": "^0.0.6"
-      }
-    },
-    "console-control-strings": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
-      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
-    },
-    "contains-path": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz",
-      "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=",
-      "dev": true
-    },
-    "core-js": {
-      "version": "2.6.11",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
-      "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
-    },
-    "core-util-is": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
-    },
-    "cosmiconfig": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
-      "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
-      "dev": true,
-      "requires": {
-        "@types/parse-json": "^4.0.0",
-        "import-fresh": "^3.1.0",
-        "parse-json": "^5.0.0",
-        "path-type": "^4.0.0",
-        "yaml": "^1.7.2"
-      },
-      "dependencies": {
-        "parse-json": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
-          "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "error-ex": "^1.3.1",
-            "json-parse-better-errors": "^1.0.1",
-            "lines-and-columns": "^1.1.6"
-          }
-        },
-        "path-type": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-          "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-          "dev": true
-        }
-      }
-    },
-    "cross-spawn": {
-      "version": "6.0.5",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
-      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
-      "dev": true,
-      "requires": {
-        "nice-try": "^1.0.4",
-        "path-key": "^2.0.1",
-        "semver": "^5.5.0",
-        "shebang-command": "^1.2.0",
-        "which": "^1.2.9"
-      }
-    },
-    "cssfilter": {
-      "version": "0.0.10",
-      "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
-      "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
-    },
-    "dashdash": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
-      "requires": {
-        "assert-plus": "^1.0.0"
-      }
-    },
-    "date-fns": {
-      "version": "1.30.1",
-      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
-      "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
-      "dev": true
-    },
-    "debug": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-      "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-      "requires": {
-        "ms": "^2.1.1"
-      }
-    },
-    "dedent": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
-      "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
-      "dev": true
-    },
-    "deep-extend": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
-      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
-    },
-    "deep-is": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
-      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
-      "dev": true
-    },
-    "define-properties": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
-      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
-      "dev": true,
-      "requires": {
-        "object-keys": "^1.0.12"
-      }
-    },
-    "delegates": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
-      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
-    },
-    "denque": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
-      "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
-    },
-    "detect-libc": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
-      "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
-    },
-    "dlv": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
-      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
-      "dev": true
-    },
-    "doctrine": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
-      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
-      "dev": true,
-      "requires": {
-        "esutils": "^2.0.2"
-      }
-    },
-    "dtrace-provider": {
-      "version": "0.8.8",
-      "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz",
-      "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==",
-      "optional": true,
-      "requires": {
-        "nan": "^2.14.0"
-      }
-    },
-    "elegant-spinner": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
-      "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=",
-      "dev": true
-    },
-    "emoji-regex": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true
-    },
-    "end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-      "dev": true,
-      "requires": {
-        "once": "^1.4.0"
-      }
-    },
-    "error-ex": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
-      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
-      "requires": {
-        "is-arrayish": "^0.2.1"
-      }
-    },
-    "es-abstract": {
-      "version": "1.17.4",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
-      "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==",
-      "dev": true,
-      "requires": {
-        "es-to-primitive": "^1.2.1",
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.1",
-        "is-callable": "^1.1.5",
-        "is-regex": "^1.0.5",
-        "object-inspect": "^1.7.0",
-        "object-keys": "^1.1.1",
-        "object.assign": "^4.1.0",
-        "string.prototype.trimleft": "^2.1.1",
-        "string.prototype.trimright": "^2.1.1"
-      }
-    },
-    "es-to-primitive": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
-      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "dev": true,
-      "requires": {
-        "is-callable": "^1.1.4",
-        "is-date-object": "^1.0.1",
-        "is-symbol": "^1.0.2"
-      }
-    },
-    "es6-promise": {
-      "version": "4.2.8",
-      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
-      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
-    },
-    "escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
-      "dev": true
-    },
-    "eslint": {
-      "version": "6.8.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
-      "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
-      "dev": true,
-      "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "ajv": "^6.10.0",
-        "chalk": "^2.1.0",
-        "cross-spawn": "^6.0.5",
-        "debug": "^4.0.1",
-        "doctrine": "^3.0.0",
-        "eslint-scope": "^5.0.0",
-        "eslint-utils": "^1.4.3",
-        "eslint-visitor-keys": "^1.1.0",
-        "espree": "^6.1.2",
-        "esquery": "^1.0.1",
-        "esutils": "^2.0.2",
-        "file-entry-cache": "^5.0.1",
-        "functional-red-black-tree": "^1.0.1",
-        "glob-parent": "^5.0.0",
-        "globals": "^12.1.0",
-        "ignore": "^4.0.6",
-        "import-fresh": "^3.0.0",
-        "imurmurhash": "^0.1.4",
-        "inquirer": "^7.0.0",
-        "is-glob": "^4.0.0",
-        "js-yaml": "^3.13.1",
-        "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.3.0",
-        "lodash": "^4.17.14",
-        "minimatch": "^3.0.4",
-        "mkdirp": "^0.5.1",
-        "natural-compare": "^1.4.0",
-        "optionator": "^0.8.3",
-        "progress": "^2.0.0",
-        "regexpp": "^2.0.1",
-        "semver": "^6.1.2",
-        "strip-ansi": "^5.2.0",
-        "strip-json-comments": "^3.0.1",
-        "table": "^5.2.3",
-        "text-table": "^0.2.0",
-        "v8-compile-cache": "^2.0.3"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^3.1.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-          "dev": true
-        },
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "dev": true,
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "fast-deep-equal": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
-        },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        },
-        "strip-json-comments": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
-          "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
-          "dev": true
-        }
-      }
-    },
-    "eslint-config-meteor": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/eslint-config-meteor/-/eslint-config-meteor-0.1.1.tgz",
-      "integrity": "sha1-rbauIL5wOFdUV5MCuqinpk5PChM=",
-      "dev": true
-    },
-    "eslint-config-prettier": {
-      "version": "6.10.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz",
-      "integrity": "sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==",
-      "dev": true,
-      "requires": {
-        "get-stdin": "^6.0.0"
-      }
-    },
-    "eslint-import-resolver-meteor": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/eslint-import-resolver-meteor/-/eslint-import-resolver-meteor-0.4.0.tgz",
-      "integrity": "sha1-yGhjhAghIIz4EzxczlGQnCamFWk=",
-      "dev": true,
-      "requires": {
-        "object-assign": "^4.0.1",
-        "resolve": "^1.1.6"
-      }
-    },
-    "eslint-import-resolver-node": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz",
-      "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==",
-      "dev": true,
-      "requires": {
-        "debug": "^2.6.9",
-        "resolve": "^1.13.1"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "dev": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
-          "dev": true
-        }
-      }
-    },
-    "eslint-module-utils": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz",
-      "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==",
-      "dev": true,
-      "requires": {
-        "debug": "^2.6.9",
-        "pkg-dir": "^2.0.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "dev": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
-          "dev": true
-        }
-      }
-    },
-    "eslint-plugin-import": {
-      "version": "2.20.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz",
-      "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==",
-      "dev": true,
-      "requires": {
-        "array-includes": "^3.0.3",
-        "array.prototype.flat": "^1.2.1",
-        "contains-path": "^0.1.0",
-        "debug": "^2.6.9",
-        "doctrine": "1.5.0",
-        "eslint-import-resolver-node": "^0.3.2",
-        "eslint-module-utils": "^2.4.1",
-        "has": "^1.0.3",
-        "minimatch": "^3.0.4",
-        "object.values": "^1.1.0",
-        "read-pkg-up": "^2.0.0",
-        "resolve": "^1.12.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "dev": true,
-          "requires": {
-            "ms": "2.0.0"
-          }
-        },
-        "doctrine": {
-          "version": "1.5.0",
-          "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
-          "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=",
-          "dev": true,
-          "requires": {
-            "esutils": "^2.0.2",
-            "isarray": "^1.0.0"
-          }
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
-          "dev": true
-        }
-      }
-    },
-    "eslint-plugin-meteor": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-meteor/-/eslint-plugin-meteor-6.0.0.tgz",
-      "integrity": "sha512-2sEW3Ow1QJMLeJPHnTJbqD3ASAyRUzgU24SKTaj2NyYC4CWYl7WmEMUl99HVlDS3qigrSnSUNMix9+3vn9TmkQ==",
-      "dev": true,
-      "requires": {
-        "invariant": "2.2.4"
-      }
-    },
-    "eslint-plugin-prettier": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz",
-      "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==",
-      "dev": true,
-      "requires": {
-        "prettier-linter-helpers": "^1.0.0"
-      }
-    },
-    "eslint-scope": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
-      "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
-      "dev": true,
-      "requires": {
-        "esrecurse": "^4.1.0",
-        "estraverse": "^4.1.1"
-      }
-    },
-    "eslint-utils": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
-      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
-      "dev": true,
-      "requires": {
-        "eslint-visitor-keys": "^1.1.0"
-      }
-    },
-    "eslint-visitor-keys": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
-      "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
-      "dev": true
-    },
-    "espree": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
-      "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
-      "dev": true,
-      "requires": {
-        "acorn": "^7.1.1",
-        "acorn-jsx": "^5.2.0",
-        "eslint-visitor-keys": "^1.1.0"
-      }
-    },
-    "esprima": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true
-    },
-    "esquery": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz",
-      "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==",
-      "dev": true,
-      "requires": {
-        "estraverse": "^4.0.0"
-      }
-    },
-    "esrecurse": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
-      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
-      "dev": true,
-      "requires": {
-        "estraverse": "^4.1.0"
-      }
-    },
-    "estraverse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
-      "dev": true
-    },
-    "esutils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
-      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-      "dev": true
-    },
-    "execa": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz",
-      "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==",
-      "dev": true,
-      "requires": {
-        "cross-spawn": "^7.0.0",
-        "get-stream": "^5.0.0",
-        "human-signals": "^1.1.1",
-        "is-stream": "^2.0.0",
-        "merge-stream": "^2.0.0",
-        "npm-run-path": "^4.0.0",
-        "onetime": "^5.1.0",
-        "p-finally": "^2.0.0",
-        "signal-exit": "^3.0.2",
-        "strip-final-newline": "^2.0.0"
-      },
-      "dependencies": {
-        "cross-spawn": {
-          "version": "7.0.1",
-          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
-          "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
-          "dev": true,
-          "requires": {
-            "path-key": "^3.1.0",
-            "shebang-command": "^2.0.0",
-            "which": "^2.0.1"
-          }
-        },
-        "path-key": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-          "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-          "dev": true
-        },
-        "shebang-command": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
-          "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-          "dev": true,
-          "requires": {
-            "shebang-regex": "^3.0.0"
-          }
-        },
-        "shebang-regex": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
-          "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-          "dev": true
-        },
-        "which": {
-          "version": "2.0.2",
-          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-          "dev": true,
-          "requires": {
-            "isexe": "^2.0.0"
-          }
-        }
-      }
-    },
-    "external-editor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
-      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
-      "dev": true,
-      "requires": {
-        "chardet": "^0.7.0",
-        "iconv-lite": "^0.4.24",
-        "tmp": "^0.0.33"
-      }
-    },
-    "extsprintf": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
-      "integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
-    },
-    "fast-deep-equal": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-      "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
-    },
-    "fast-diff": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
-      "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
-      "dev": true
-    },
-    "fast-json-stable-stringify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
-      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
-    },
-    "fast-levenshtein": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
-      "dev": true
-    },
-    "figures": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
-      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
-      "dev": true,
-      "requires": {
-        "escape-string-regexp": "^1.0.5"
-      }
-    },
-    "file-entry-cache": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
-      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
-      "dev": true,
-      "requires": {
-        "flat-cache": "^2.0.1"
-      }
-    },
-    "fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "dev": true,
-      "requires": {
-        "to-regex-range": "^5.0.1"
-      }
-    },
-    "find-up": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
-      "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
-      "dev": true,
-      "requires": {
-        "locate-path": "^2.0.0"
-      }
-    },
-    "flat-cache": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
-      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
-      "dev": true,
-      "requires": {
-        "flatted": "^2.0.0",
-        "rimraf": "2.6.3",
-        "write": "1.0.3"
-      }
-    },
-    "flatted": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
-      "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
-      "dev": true
-    },
-    "flushwritable": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/flushwritable/-/flushwritable-1.0.0.tgz",
-      "integrity": "sha1-PjKNj95BKtR+c44751C00pAENJg="
-    },
-    "fs-minipass": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
-      "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
-      "requires": {
-        "minipass": "^2.6.0"
-      }
-    },
-    "fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
-    },
-    "function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
-    },
-    "functional-red-black-tree": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
-      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
-      "dev": true
-    },
-    "gauge": {
-      "version": "2.7.4",
-      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
-      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
-      "requires": {
-        "aproba": "^1.0.3",
-        "console-control-strings": "^1.0.0",
-        "has-unicode": "^2.0.0",
-        "object-assign": "^4.1.0",
-        "signal-exit": "^3.0.0",
-        "string-width": "^1.0.1",
-        "strip-ansi": "^3.0.1",
-        "wide-align": "^1.1.0"
-      }
-    },
-    "get-own-enumerable-property-symbols": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
-      "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
-      "dev": true
-    },
-    "get-stdin": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
-      "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
-      "dev": true
-    },
-    "get-stream": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
-      "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
-      "dev": true,
-      "requires": {
-        "pump": "^3.0.0"
-      }
-    },
-    "glob": {
-      "version": "7.1.4",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
-      "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
-      "requires": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.0.4",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      }
-    },
-    "glob-parent": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
-      "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
-      "dev": true,
-      "requires": {
-        "is-glob": "^4.0.1"
-      }
-    },
-    "globals": {
-      "version": "12.4.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
-      "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
-      "dev": true,
-      "requires": {
-        "type-fest": "^0.8.1"
-      }
-    },
-    "graceful-fs": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
-      "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
-      "dev": true
-    },
-    "gridfs-stream": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/gridfs-stream/-/gridfs-stream-1.1.1.tgz",
-      "integrity": "sha1-PdOhAOwgIaGBKC9utGcJY2B034k=",
-      "requires": {
-        "flushwritable": "^1.0.0"
-      }
-    },
-    "has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
-      "requires": {
-        "function-bind": "^1.1.1"
-      }
-    },
-    "has-ansi": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
-      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
-      "dev": true,
-      "requires": {
-        "ansi-regex": "^2.0.0"
-      }
-    },
-    "has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
-      "dev": true
-    },
-    "has-symbols": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
-      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
-      "dev": true
-    },
-    "has-unicode": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
-      "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
-    },
-    "hosted-git-info": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
-      "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
-      "dev": true
-    },
-    "human-signals": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
-      "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
-      "dev": true
-    },
-    "iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-      "requires": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      }
-    },
-    "ieee754": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
-      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
-    },
-    "ignore": {
-      "version": "4.0.6",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
-      "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
-      "dev": true
-    },
-    "ignore-walk": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
-      "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
-      "requires": {
-        "minimatch": "^3.0.4"
-      }
-    },
-    "import-fresh": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
-      "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
-      "dev": true,
-      "requires": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      },
-      "dependencies": {
-        "resolve-from": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-          "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-          "dev": true
-        }
-      }
-    },
-    "imurmurhash": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
-      "dev": true
-    },
-    "indent-string": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
-      "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=",
-      "dev": true
-    },
-    "inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "requires": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
-    },
-    "ini": {
-      "version": "1.3.5",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
-      "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
-    },
-    "inquirer": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
-      "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
-      "dev": true,
-      "requires": {
-        "ansi-escapes": "^4.2.1",
-        "chalk": "^3.0.0",
-        "cli-cursor": "^3.1.0",
-        "cli-width": "^2.0.0",
-        "external-editor": "^3.0.3",
-        "figures": "^3.0.0",
-        "lodash": "^4.17.15",
-        "mute-stream": "0.0.8",
-        "run-async": "^2.4.0",
-        "rxjs": "^6.5.3",
-        "string-width": "^4.1.0",
-        "strip-ansi": "^6.0.0",
-        "through": "^2.3.6"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
-          "dev": true
-        },
-        "ansi-styles": {
-          "version": "4.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
-          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
-          "dev": true,
-          "requires": {
-            "@types/color-name": "^1.1.1",
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
-          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-          "dev": true
-        },
-        "string-width": {
-          "version": "4.2.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
-          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
-          "dev": true,
-          "requires": {
-            "emoji-regex": "^8.0.0",
-            "is-fullwidth-code-point": "^3.0.0",
-            "strip-ansi": "^6.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^5.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
-          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
-    "invariant": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
-      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
-      "dev": true,
-      "requires": {
-        "loose-envify": "^1.0.0"
-      }
-    },
-    "is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
-      "dev": true
-    },
-    "is-callable": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
-      "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
-      "dev": true
-    },
-    "is-date-object": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
-      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
-      "dev": true
-    },
-    "is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "dev": true
-    },
-    "is-fullwidth-code-point": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
-      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
-      "requires": {
-        "number-is-nan": "^1.0.0"
-      }
-    },
-    "is-glob": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
-      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
-      "dev": true,
-      "requires": {
-        "is-extglob": "^2.1.1"
-      }
-    },
-    "is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true
-    },
-    "is-obj": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
-      "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
-      "dev": true
-    },
-    "is-observable": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz",
-      "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==",
-      "dev": true,
-      "requires": {
-        "symbol-observable": "^1.1.0"
-      }
-    },
-    "is-promise": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
-      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
-      "dev": true
-    },
-    "is-regex": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
-      "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
-      "dev": true,
-      "requires": {
-        "has": "^1.0.3"
-      }
-    },
-    "is-regexp": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
-      "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
-      "dev": true
-    },
-    "is-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
-      "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
-      "dev": true
-    },
-    "is-string": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
-      "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
-      "dev": true
-    },
-    "is-symbol": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
-      "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
-      "dev": true,
-      "requires": {
-        "has-symbols": "^1.0.1"
-      }
-    },
-    "isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
-    },
-    "isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
-    },
-    "js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true
-    },
-    "js-yaml": {
-      "version": "3.13.1",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
-      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
-      "dev": true,
-      "requires": {
-        "argparse": "^1.0.7",
-        "esprima": "^4.0.0"
-      }
-    },
-    "json-parse-better-errors": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
-      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
-      "dev": true
-    },
-    "json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
-    },
-    "json-stable-stringify-without-jsonify": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
-      "dev": true
-    },
-    "ldap-filter": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
-      "integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
-      "requires": {
-        "assert-plus": "0.1.5"
-      },
-      "dependencies": {
-        "assert-plus": {
-          "version": "0.1.5",
-          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
-          "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
-        }
-      }
-    },
-    "ldapjs": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
-      "integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
-      "requires": {
-        "asn1": "0.2.3",
-        "assert-plus": "^1.0.0",
-        "backoff": "^2.5.0",
-        "bunyan": "^1.8.3",
-        "dashdash": "^1.14.0",
-        "dtrace-provider": "~0.8",
-        "ldap-filter": "0.2.2",
-        "once": "^1.4.0",
-        "vasync": "^1.6.4",
-        "verror": "^1.8.1"
-      }
-    },
-    "levn": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
-      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
-      "dev": true,
-      "requires": {
-        "prelude-ls": "~1.1.2",
-        "type-check": "~0.3.2"
-      }
-    },
-    "lines-and-columns": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
-      "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
-      "dev": true
-    },
-    "lint-staged": {
-      "version": "10.0.8",
-      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.0.8.tgz",
-      "integrity": "sha512-Oa9eS4DJqvQMVdywXfEor6F4vP+21fPHF8LUXgBbVWUSWBddjqsvO6Bv1LwMChmgQZZqwUvgJSHlu8HFHAPZmA==",
-      "dev": true,
-      "requires": {
-        "chalk": "^3.0.0",
-        "commander": "^4.0.1",
-        "cosmiconfig": "^6.0.0",
-        "debug": "^4.1.1",
-        "dedent": "^0.7.0",
-        "execa": "^3.4.0",
-        "listr": "^0.14.3",
-        "log-symbols": "^3.0.0",
-        "micromatch": "^4.0.2",
-        "normalize-path": "^3.0.0",
-        "please-upgrade-node": "^3.2.0",
-        "string-argv": "0.3.1",
-        "stringify-object": "^3.3.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
-          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
-          "dev": true,
-          "requires": {
-            "@types/color-name": "^1.1.1",
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
-          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-          "dev": true
-        },
-        "commander": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
-          "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
-          "dev": true
-        },
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "dev": true,
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
-          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
-    "listr": {
-      "version": "0.14.3",
-      "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz",
-      "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==",
-      "dev": true,
-      "requires": {
-        "@samverschueren/stream-to-observable": "^0.3.0",
-        "is-observable": "^1.1.0",
-        "is-promise": "^2.1.0",
-        "is-stream": "^1.1.0",
-        "listr-silent-renderer": "^1.1.1",
-        "listr-update-renderer": "^0.5.0",
-        "listr-verbose-renderer": "^0.5.0",
-        "p-map": "^2.0.0",
-        "rxjs": "^6.3.3"
-      },
-      "dependencies": {
-        "is-stream": {
-          "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
-          "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-          "dev": true
-        }
-      }
-    },
-    "listr-silent-renderer": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
-      "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=",
-      "dev": true
-    },
-    "listr-update-renderer": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz",
-      "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==",
-      "dev": true,
-      "requires": {
-        "chalk": "^1.1.3",
-        "cli-truncate": "^0.2.1",
-        "elegant-spinner": "^1.0.1",
-        "figures": "^1.7.0",
-        "indent-string": "^3.0.0",
-        "log-symbols": "^1.0.2",
-        "log-update": "^2.3.0",
-        "strip-ansi": "^3.0.1"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "2.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
-          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
-          "dev": true
-        },
-        "chalk": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
-          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^2.2.1",
-            "escape-string-regexp": "^1.0.2",
-            "has-ansi": "^2.0.0",
-            "strip-ansi": "^3.0.0",
-            "supports-color": "^2.0.0"
-          }
-        },
-        "figures": {
-          "version": "1.7.0",
-          "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
-          "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
-          "dev": true,
-          "requires": {
-            "escape-string-regexp": "^1.0.5",
-            "object-assign": "^4.1.0"
-          }
-        },
-        "log-symbols": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
-          "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
-          "dev": true,
-          "requires": {
-            "chalk": "^1.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
-          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
-          "dev": true
-        }
-      }
-    },
-    "listr-verbose-renderer": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz",
-      "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==",
-      "dev": true,
-      "requires": {
-        "chalk": "^2.4.1",
-        "cli-cursor": "^2.1.0",
-        "date-fns": "^1.27.2",
-        "figures": "^2.0.0"
-      },
-      "dependencies": {
-        "cli-cursor": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
-          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
-          "dev": true,
-          "requires": {
-            "restore-cursor": "^2.0.0"
-          }
-        },
-        "figures": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
-          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
-          "dev": true,
-          "requires": {
-            "escape-string-regexp": "^1.0.5"
-          }
-        },
-        "mimic-fn": {
-          "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
-          "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
-          "dev": true
-        },
-        "onetime": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
-          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
-          "dev": true,
-          "requires": {
-            "mimic-fn": "^1.0.0"
-          }
-        },
-        "restore-cursor": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
-          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
-          "dev": true,
-          "requires": {
-            "onetime": "^2.0.0",
-            "signal-exit": "^3.0.2"
-          }
-        }
-      }
-    },
-    "load-json-file": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
-      "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.1.2",
-        "parse-json": "^2.2.0",
-        "pify": "^2.0.0",
-        "strip-bom": "^3.0.0"
-      }
-    },
-    "locate-path": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
-      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
-      "dev": true,
-      "requires": {
-        "p-locate": "^2.0.0",
-        "path-exists": "^3.0.0"
-      }
-    },
-    "lodash": {
-      "version": "4.17.15",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
-      "dev": true
-    },
-    "lodash.merge": {
-      "version": "4.6.2",
-      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
-      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
-      "dev": true
-    },
-    "lodash.unescape": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
-      "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
-      "dev": true
-    },
-    "log-symbols": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
-      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
-      "dev": true,
-      "requires": {
-        "chalk": "^2.4.2"
-      }
-    },
-    "log-update": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz",
-      "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=",
-      "dev": true,
-      "requires": {
-        "ansi-escapes": "^3.0.0",
-        "cli-cursor": "^2.0.0",
-        "wrap-ansi": "^3.0.1"
-      },
-      "dependencies": {
-        "ansi-escapes": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
-          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
-          "dev": true
-        },
-        "cli-cursor": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
-          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
-          "dev": true,
-          "requires": {
-            "restore-cursor": "^2.0.0"
-          }
-        },
-        "mimic-fn": {
-          "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
-          "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
-          "dev": true
-        },
-        "onetime": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
-          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
-          "dev": true,
-          "requires": {
-            "mimic-fn": "^1.0.0"
-          }
-        },
-        "restore-cursor": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
-          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
-          "dev": true,
-          "requires": {
-            "onetime": "^2.0.0",
-            "signal-exit": "^3.0.2"
-          }
-        }
-      }
-    },
-    "loglevel": {
-      "version": "1.6.7",
-      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz",
-      "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==",
-      "dev": true
-    },
-    "loglevel-colored-level-prefix": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz",
-      "integrity": "sha1-akAhj9x64V/HbD0PPmdsRlOIYD4=",
-      "dev": true,
-      "requires": {
-        "chalk": "^1.1.3",
-        "loglevel": "^1.4.1"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "2.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
-          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
-          "dev": true
-        },
-        "chalk": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
-          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^2.2.1",
-            "escape-string-regexp": "^1.0.2",
-            "has-ansi": "^2.0.0",
-            "strip-ansi": "^3.0.0",
-            "supports-color": "^2.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
-          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
-          "dev": true
-        }
-      }
-    },
-    "long": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
-      "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
-    },
-    "loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "dev": true,
-      "requires": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      }
-    },
-    "lru-cache": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
-      "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
-      "dev": true,
-      "requires": {
-        "pseudomap": "^1.0.2",
-        "yallist": "^2.1.2"
-      },
-      "dependencies": {
-        "yallist": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
-          "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
-          "dev": true
-        }
-      }
-    },
-    "memory-pager": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
-      "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
-      "optional": true
-    },
-    "merge-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-      "dev": true
-    },
-    "meteor-node-stubs": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.0.0.tgz",
-      "integrity": "sha512-QJwyv23wyXD3uEMzk5Xr/y5ezoVlCbHvBbrgdkVadn84dmifLRbs0PtD6EeNw5NLIk+SQSfxld7IMdEsneGz5w==",
-      "requires": {
-        "assert": "^1.4.1",
-        "browserify-zlib": "^0.2.0",
-        "buffer": "^5.2.1",
-        "console-browserify": "^1.1.0",
-        "constants-browserify": "^1.0.0",
-        "crypto-browserify": "^3.12.0",
-        "domain-browser": "^1.2.0",
-        "events": "^3.0.0",
-        "https-browserify": "^1.0.0",
-        "os-browserify": "^0.3.0",
-        "path-browserify": "^1.0.0",
-        "process": "^0.11.10",
-        "punycode": "^2.1.1",
-        "querystring-es3": "^0.2.1",
-        "readable-stream": "^3.3.0",
-        "stream-browserify": "^2.0.2",
-        "stream-http": "^3.0.0",
-        "string_decoder": "^1.2.0",
-        "timers-browserify": "^2.0.10",
-        "tty-browserify": "0.0.1",
-        "url": "^0.11.0",
-        "util": "^0.11.1",
-        "vm-browserify": "^1.1.0"
-      },
-      "dependencies": {
-        "asn1.js": {
-          "version": "4.10.1",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.0.0",
-            "inherits": "^2.0.1",
-            "minimalistic-assert": "^1.0.0"
-          }
-        },
-        "assert": {
-          "version": "1.4.1",
-          "bundled": true,
-          "requires": {
-            "util": "0.10.3"
-          },
-          "dependencies": {
-            "util": {
-              "version": "0.10.3",
-              "bundled": true,
-              "requires": {
-                "inherits": "2.0.1"
-              }
-            }
-          }
-        },
-        "base64-js": {
-          "version": "1.3.0",
-          "bundled": true
-        },
-        "bn.js": {
-          "version": "4.11.8",
-          "bundled": true
-        },
-        "brorand": {
-          "version": "1.1.0",
-          "bundled": true
-        },
-        "browserify-aes": {
-          "version": "1.2.0",
-          "bundled": true,
-          "requires": {
-            "buffer-xor": "^1.0.3",
-            "cipher-base": "^1.0.0",
-            "create-hash": "^1.1.0",
-            "evp_bytestokey": "^1.0.3",
-            "inherits": "^2.0.1",
-            "safe-buffer": "^5.0.1"
-          }
-        },
-        "browserify-cipher": {
-          "version": "1.0.1",
-          "bundled": true,
-          "requires": {
-            "browserify-aes": "^1.0.4",
-            "browserify-des": "^1.0.0",
-            "evp_bytestokey": "^1.0.0"
-          }
-        },
-        "browserify-des": {
-          "version": "1.0.2",
-          "bundled": true,
-          "requires": {
-            "cipher-base": "^1.0.1",
-            "des.js": "^1.0.0",
-            "inherits": "^2.0.1",
-            "safe-buffer": "^5.1.2"
-          }
-        },
-        "browserify-rsa": {
-          "version": "4.0.1",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.1.0",
-            "randombytes": "^2.0.1"
-          }
-        },
-        "browserify-sign": {
-          "version": "4.0.4",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.1.1",
-            "browserify-rsa": "^4.0.0",
-            "create-hash": "^1.1.0",
-            "create-hmac": "^1.1.2",
-            "elliptic": "^6.0.0",
-            "inherits": "^2.0.1",
-            "parse-asn1": "^5.0.0"
-          }
-        },
-        "browserify-zlib": {
-          "version": "0.2.0",
-          "bundled": true,
-          "requires": {
-            "pako": "~1.0.5"
-          }
-        },
-        "buffer": {
-          "version": "5.2.1",
-          "bundled": true,
-          "requires": {
-            "base64-js": "^1.0.2",
-            "ieee754": "^1.1.4"
-          }
-        },
-        "buffer-xor": {
-          "version": "1.0.3",
-          "bundled": true
-        },
-        "builtin-status-codes": {
-          "version": "3.0.0",
-          "bundled": true
-        },
-        "cipher-base": {
-          "version": "1.0.4",
-          "bundled": true,
-          "requires": {
-            "inherits": "^2.0.1",
-            "safe-buffer": "^5.0.1"
-          }
-        },
-        "console-browserify": {
-          "version": "1.1.0",
-          "bundled": true,
-          "requires": {
-            "date-now": "^0.1.4"
-          }
-        },
-        "constants-browserify": {
-          "version": "1.0.0",
-          "bundled": true
-        },
-        "core-util-is": {
-          "version": "1.0.2",
-          "bundled": true
-        },
-        "create-ecdh": {
-          "version": "4.0.3",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.1.0",
-            "elliptic": "^6.0.0"
-          }
-        },
-        "create-hash": {
-          "version": "1.2.0",
-          "bundled": true,
-          "requires": {
-            "cipher-base": "^1.0.1",
-            "inherits": "^2.0.1",
-            "md5.js": "^1.3.4",
-            "ripemd160": "^2.0.1",
-            "sha.js": "^2.4.0"
-          }
-        },
-        "create-hmac": {
-          "version": "1.1.7",
-          "bundled": true,
-          "requires": {
-            "cipher-base": "^1.0.3",
-            "create-hash": "^1.1.0",
-            "inherits": "^2.0.1",
-            "ripemd160": "^2.0.0",
-            "safe-buffer": "^5.0.1",
-            "sha.js": "^2.4.8"
-          }
-        },
-        "crypto-browserify": {
-          "version": "3.12.0",
-          "bundled": true,
-          "requires": {
-            "browserify-cipher": "^1.0.0",
-            "browserify-sign": "^4.0.0",
-            "create-ecdh": "^4.0.0",
-            "create-hash": "^1.1.0",
-            "create-hmac": "^1.1.0",
-            "diffie-hellman": "^5.0.0",
-            "inherits": "^2.0.1",
-            "pbkdf2": "^3.0.3",
-            "public-encrypt": "^4.0.0",
-            "randombytes": "^2.0.0",
-            "randomfill": "^1.0.3"
-          }
-        },
-        "date-now": {
-          "version": "0.1.4",
-          "bundled": true
-        },
-        "des.js": {
-          "version": "1.0.0",
-          "bundled": true,
-          "requires": {
-            "inherits": "^2.0.1",
-            "minimalistic-assert": "^1.0.0"
-          }
-        },
-        "diffie-hellman": {
-          "version": "5.0.3",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.1.0",
-            "miller-rabin": "^4.0.0",
-            "randombytes": "^2.0.0"
-          }
-        },
-        "domain-browser": {
-          "version": "1.2.0",
-          "bundled": true
-        },
-        "elliptic": {
-          "version": "6.4.1",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.4.0",
-            "brorand": "^1.0.1",
-            "hash.js": "^1.0.0",
-            "hmac-drbg": "^1.0.0",
-            "inherits": "^2.0.1",
-            "minimalistic-assert": "^1.0.0",
-            "minimalistic-crypto-utils": "^1.0.0"
-          }
-        },
-        "events": {
-          "version": "3.0.0",
-          "bundled": true
-        },
-        "evp_bytestokey": {
-          "version": "1.0.3",
-          "bundled": true,
-          "requires": {
-            "md5.js": "^1.3.4",
-            "safe-buffer": "^5.1.1"
-          }
-        },
-        "hash-base": {
-          "version": "3.0.4",
-          "bundled": true,
-          "requires": {
-            "inherits": "^2.0.1",
-            "safe-buffer": "^5.0.1"
-          }
-        },
-        "hash.js": {
-          "version": "1.1.7",
-          "bundled": true,
-          "requires": {
-            "inherits": "^2.0.3",
-            "minimalistic-assert": "^1.0.1"
-          },
-          "dependencies": {
-            "inherits": {
-              "version": "2.0.3",
-              "bundled": true
-            }
-          }
-        },
-        "hmac-drbg": {
-          "version": "1.0.1",
-          "bundled": true,
-          "requires": {
-            "hash.js": "^1.0.3",
-            "minimalistic-assert": "^1.0.0",
-            "minimalistic-crypto-utils": "^1.0.1"
-          }
-        },
-        "https-browserify": {
-          "version": "1.0.0",
-          "bundled": true
-        },
-        "ieee754": {
-          "version": "1.1.13",
-          "bundled": true
-        },
-        "inherits": {
-          "version": "2.0.1",
-          "bundled": true
-        },
-        "isarray": {
-          "version": "1.0.0",
-          "bundled": true
-        },
-        "md5.js": {
-          "version": "1.3.5",
-          "bundled": true,
-          "requires": {
-            "hash-base": "^3.0.0",
-            "inherits": "^2.0.1",
-            "safe-buffer": "^5.1.2"
-          }
-        },
-        "miller-rabin": {
-          "version": "4.0.1",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.0.0",
-            "brorand": "^1.0.1"
-          }
-        },
-        "minimalistic-assert": {
-          "version": "1.0.1",
-          "bundled": true
-        },
-        "minimalistic-crypto-utils": {
-          "version": "1.0.1",
-          "bundled": true
-        },
-        "os-browserify": {
-          "version": "0.3.0",
-          "bundled": true
-        },
-        "pako": {
-          "version": "1.0.10",
-          "bundled": true
-        },
-        "parse-asn1": {
-          "version": "5.1.4",
-          "bundled": true,
-          "requires": {
-            "asn1.js": "^4.0.0",
-            "browserify-aes": "^1.0.0",
-            "create-hash": "^1.1.0",
-            "evp_bytestokey": "^1.0.0",
-            "pbkdf2": "^3.0.3",
-            "safe-buffer": "^5.1.1"
-          }
-        },
-        "path-browserify": {
-          "version": "1.0.0",
-          "bundled": true
-        },
-        "pbkdf2": {
-          "version": "3.0.17",
-          "bundled": true,
-          "requires": {
-            "create-hash": "^1.1.2",
-            "create-hmac": "^1.1.4",
-            "ripemd160": "^2.0.1",
-            "safe-buffer": "^5.0.1",
-            "sha.js": "^2.4.8"
-          }
-        },
-        "process": {
-          "version": "0.11.10",
-          "bundled": true
-        },
-        "process-nextick-args": {
-          "version": "2.0.0",
-          "bundled": true
-        },
-        "public-encrypt": {
-          "version": "4.0.3",
-          "bundled": true,
-          "requires": {
-            "bn.js": "^4.1.0",
-            "browserify-rsa": "^4.0.0",
-            "create-hash": "^1.1.0",
-            "parse-asn1": "^5.0.0",
-            "randombytes": "^2.0.1",
-            "safe-buffer": "^5.1.2"
-          }
-        },
-        "punycode": {
-          "version": "2.1.1",
-          "bundled": true
-        },
-        "querystring": {
-          "version": "0.2.0",
-          "bundled": true
-        },
-        "querystring-es3": {
-          "version": "0.2.1",
-          "bundled": true
-        },
-        "randombytes": {
-          "version": "2.1.0",
-          "bundled": true,
-          "requires": {
-            "safe-buffer": "^5.1.0"
-          }
-        },
-        "randomfill": {
-          "version": "1.0.4",
-          "bundled": true,
-          "requires": {
-            "randombytes": "^2.0.5",
-            "safe-buffer": "^5.1.0"
-          }
-        },
-        "readable-stream": {
-          "version": "3.3.0",
-          "bundled": true,
-          "requires": {
-            "inherits": "^2.0.3",
-            "string_decoder": "^1.1.1",
-            "util-deprecate": "^1.0.1"
-          },
-          "dependencies": {
-            "inherits": {
-              "version": "2.0.3",
-              "bundled": true
-            }
-          }
-        },
-        "ripemd160": {
-          "version": "2.0.2",
-          "bundled": true,
-          "requires": {
-            "hash-base": "^3.0.0",
-            "inherits": "^2.0.1"
-          }
-        },
-        "safe-buffer": {
-          "version": "5.1.2",
-          "bundled": true
-        },
-        "setimmediate": {
-          "version": "1.0.5",
-          "bundled": true
-        },
-        "sha.js": {
-          "version": "2.4.11",
-          "bundled": true,
-          "requires": {
-            "inherits": "^2.0.1",
-            "safe-buffer": "^5.0.1"
-          }
-        },
-        "stream-browserify": {
-          "version": "2.0.2",
-          "bundled": true,
-          "requires": {
-            "inherits": "~2.0.1",
-            "readable-stream": "^2.0.2"
-          },
-          "dependencies": {
-            "readable-stream": {
-              "version": "2.3.6",
-              "bundled": true,
-              "requires": {
-                "core-util-is": "~1.0.0",
-                "inherits": "~2.0.3",
-                "isarray": "~1.0.0",
-                "process-nextick-args": "~2.0.0",
-                "safe-buffer": "~5.1.1",
-                "string_decoder": "~1.1.1",
-                "util-deprecate": "~1.0.1"
-              },
-              "dependencies": {
-                "inherits": {
-                  "version": "2.0.3",
-                  "bundled": true
-                }
-              }
-            },
-            "string_decoder": {
-              "version": "1.1.1",
-              "bundled": true,
-              "requires": {
-                "safe-buffer": "~5.1.0"
-              }
-            }
-          }
-        },
-        "stream-http": {
-          "version": "3.0.0",
-          "bundled": true,
-          "requires": {
-            "builtin-status-codes": "^3.0.0",
-            "inherits": "^2.0.1",
-            "readable-stream": "^3.0.6",
-            "xtend": "^4.0.0"
-          }
-        },
-        "string_decoder": {
-          "version": "1.2.0",
-          "bundled": true,
-          "requires": {
-            "safe-buffer": "~5.1.0"
-          }
-        },
-        "timers-browserify": {
-          "version": "2.0.10",
-          "bundled": true,
-          "requires": {
-            "setimmediate": "^1.0.4"
-          }
-        },
-        "tty-browserify": {
-          "version": "0.0.1",
-          "bundled": true
-        },
-        "url": {
-          "version": "0.11.0",
-          "bundled": true,
-          "requires": {
-            "punycode": "1.3.2",
-            "querystring": "0.2.0"
-          },
-          "dependencies": {
-            "punycode": {
-              "version": "1.3.2",
-              "bundled": true
-            }
-          }
-        },
-        "util": {
-          "version": "0.11.1",
-          "bundled": true,
-          "requires": {
-            "inherits": "2.0.3"
-          },
-          "dependencies": {
-            "inherits": {
-              "version": "2.0.3",
-              "bundled": true
-            }
-          }
-        },
-        "util-deprecate": {
-          "version": "1.0.2",
-          "bundled": true
-        },
-        "vm-browserify": {
-          "version": "1.1.0",
-          "bundled": true
-        },
-        "xtend": {
-          "version": "4.0.1",
-          "bundled": true
-        }
-      }
-    },
-    "micromatch": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
-      "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
-      "dev": true,
-      "requires": {
-        "braces": "^3.0.1",
-        "picomatch": "^2.0.5"
-      }
-    },
-    "mimic-fn": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
-      "dev": true
-    },
-    "minimatch": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "requires": {
-        "brace-expansion": "^1.1.7"
-      }
-    },
-    "minimist": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
-    },
-    "minipass": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
-      "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
-      "requires": {
-        "safe-buffer": "^5.1.2",
-        "yallist": "^3.0.0"
-      }
-    },
-    "minizlib": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
-      "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
-      "requires": {
-        "minipass": "^2.9.0"
-      }
-    },
-    "mkdirp": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
-      "requires": {
-        "minimist": "0.0.8"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "0.0.8",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
-          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
-        }
-      }
-    },
-    "moment": {
-      "version": "2.24.0",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
-      "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
-      "optional": true
-    },
-    "mongodb": {
-      "version": "3.5.5",
-      "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.5.tgz",
-      "integrity": "sha512-GCjDxR3UOltDq00Zcpzql6dQo1sVry60OXJY3TDmFc2SWFY6c8Gn1Ardidc5jDirvJrx2GC3knGOImKphbSL3A==",
-      "requires": {
-        "bl": "^2.2.0",
-        "bson": "^1.1.1",
-        "denque": "^1.4.1",
-        "require_optional": "^1.0.1",
-        "safe-buffer": "^5.1.2",
-        "saslprep": "^1.0.0"
-      },
-      "dependencies": {
-        "bson": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.3.tgz",
-          "integrity": "sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg=="
-        }
-      }
-    },
-    "ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
-    },
-    "mute-stream": {
-      "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
-      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
-      "dev": true
-    },
-    "mv": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
-      "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
-      "optional": true,
-      "requires": {
-        "mkdirp": "~0.5.1",
-        "ncp": "~2.0.0",
-        "rimraf": "~2.4.0"
-      },
-      "dependencies": {
-        "glob": {
-          "version": "6.0.4",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
-          "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
-          "optional": true,
-          "requires": {
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "2 || 3",
-            "once": "^1.3.0",
-            "path-is-absolute": "^1.0.0"
-          }
-        },
-        "rimraf": {
-          "version": "2.4.5",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
-          "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
-          "optional": true,
-          "requires": {
-            "glob": "^6.0.1"
-          }
-        }
-      }
-    },
-    "nan": {
-      "version": "2.14.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
-      "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
-      "optional": true
-    },
-    "natural-compare": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
-      "dev": true
-    },
-    "ncp": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
-      "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
-      "optional": true
-    },
-    "needle": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.3.tgz",
-      "integrity": "sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==",
-      "requires": {
-        "debug": "^3.2.6",
-        "iconv-lite": "^0.4.4",
-        "sax": "^1.2.4"
-      }
-    },
-    "nice-try": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
-      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
-      "dev": true
-    },
-    "node-addon-api": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
-      "integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
-    },
-    "node-pre-gyp": {
-      "version": "0.14.0",
-      "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz",
-      "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==",
-      "requires": {
-        "detect-libc": "^1.0.2",
-        "mkdirp": "^0.5.1",
-        "needle": "^2.2.1",
-        "nopt": "^4.0.1",
-        "npm-packlist": "^1.1.6",
-        "npmlog": "^4.0.2",
-        "rc": "^1.2.7",
-        "rimraf": "^2.6.1",
-        "semver": "^5.3.0",
-        "tar": "^4.4.2"
-      }
-    },
-    "nopt": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
-      "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
-      "requires": {
-        "abbrev": "1",
-        "osenv": "^0.1.4"
-      }
-    },
-    "normalize-package-data": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
-      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
-      "dev": true,
-      "requires": {
-        "hosted-git-info": "^2.1.4",
-        "resolve": "^1.10.0",
-        "semver": "2 || 3 || 4 || 5",
-        "validate-npm-package-license": "^3.0.1"
-      }
-    },
-    "normalize-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true
-    },
-    "npm-bundled": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
-      "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
-      "requires": {
-        "npm-normalize-package-bin": "^1.0.1"
-      }
-    },
-    "npm-normalize-package-bin": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
-      "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
-    },
-    "npm-packlist": {
-      "version": "1.4.8",
-      "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
-      "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
-      "requires": {
-        "ignore-walk": "^3.0.1",
-        "npm-bundled": "^1.0.1",
-        "npm-normalize-package-bin": "^1.0.1"
-      }
-    },
-    "npm-run-path": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
-      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
-      "dev": true,
-      "requires": {
-        "path-key": "^3.0.0"
-      },
-      "dependencies": {
-        "path-key": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-          "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-          "dev": true
-        }
-      }
-    },
-    "npmlog": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
-      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
-      "requires": {
-        "are-we-there-yet": "~1.1.2",
-        "console-control-strings": "~1.1.0",
-        "gauge": "~2.7.3",
-        "set-blocking": "~2.0.0"
-      }
-    },
-    "number-is-nan": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
-      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
-    },
-    "object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
-    },
-    "object-inspect": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
-      "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
-      "dev": true
-    },
-    "object-keys": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true
-    },
-    "object.assign": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
-      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.2",
-        "function-bind": "^1.1.1",
-        "has-symbols": "^1.0.0",
-        "object-keys": "^1.0.11"
-      }
-    },
-    "object.values": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
-      "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.17.0-next.1",
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3"
-      }
-    },
-    "once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "requires": {
-        "wrappy": "1"
-      }
-    },
-    "onetime": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
-      "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
-      "dev": true,
-      "requires": {
-        "mimic-fn": "^2.1.0"
-      }
-    },
-    "optionator": {
-      "version": "0.8.3",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
-      "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
-      "dev": true,
-      "requires": {
-        "deep-is": "~0.1.3",
-        "fast-levenshtein": "~2.0.6",
-        "levn": "~0.3.0",
-        "prelude-ls": "~1.1.2",
-        "type-check": "~0.3.2",
-        "word-wrap": "~1.2.3"
-      }
-    },
-    "os": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/os/-/os-0.1.1.tgz",
-      "integrity": "sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M="
-    },
-    "os-homedir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
-      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
-    },
-    "os-shim": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz",
-      "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=",
-      "dev": true
-    },
-    "os-tmpdir": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
-      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
-    },
-    "osenv": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
-      "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
-      "requires": {
-        "os-homedir": "^1.0.0",
-        "os-tmpdir": "^1.0.0"
-      }
-    },
-    "p-finally": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
-      "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==",
-      "dev": true
-    },
-    "p-limit": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
-      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
-      "dev": true,
-      "requires": {
-        "p-try": "^1.0.0"
-      }
-    },
-    "p-locate": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
-      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
-      "dev": true,
-      "requires": {
-        "p-limit": "^1.1.0"
-      }
-    },
-    "p-map": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
-      "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
-      "dev": true
-    },
-    "p-try": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
-      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
-      "dev": true
-    },
-    "page": {
-      "version": "1.11.5",
-      "resolved": "https://registry.npmjs.org/page/-/page-1.11.5.tgz",
-      "integrity": "sha512-0JXUHc7Y8p1cPJQbhZSwaKO3p+bU3Rgny+OM5gJMKHWHvJKan/fsE5RUzEjRQolv9DzPOSVWfSOHz0lLxK19eA==",
-      "requires": {
-        "path-to-regexp": "~1.2.1"
-      }
-    },
-    "parent-module": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
-      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dev": true,
-      "requires": {
-        "callsites": "^3.0.0"
-      }
-    },
-    "parse-json": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
-      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
-      "dev": true,
-      "requires": {
-        "error-ex": "^1.2.0"
-      }
-    },
-    "path-exists": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
-      "dev": true
-    },
-    "path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
-    },
-    "path-is-inside": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
-      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
-      "dev": true
-    },
-    "path-key": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
-      "dev": true
-    },
-    "path-parse": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
-      "dev": true
-    },
-    "path-to-regexp": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.2.1.tgz",
-      "integrity": "sha1-szcFwUAjTYc8hyHHuf2LVB7Tr/k=",
-      "requires": {
-        "isarray": "0.0.1"
-      },
-      "dependencies": {
-        "isarray": {
-          "version": "0.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
-          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
-        }
-      }
-    },
-    "path-type": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
-      "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
-      "dev": true,
-      "requires": {
-        "pify": "^2.0.0"
-      }
-    },
-    "picomatch": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
-      "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
-      "dev": true
-    },
-    "pify": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
-      "dev": true
-    },
-    "pkg-dir": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
-      "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
-      "dev": true,
-      "requires": {
-        "find-up": "^2.1.0"
-      }
-    },
-    "please-upgrade-node": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
-      "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
-      "dev": true,
-      "requires": {
-        "semver-compare": "^1.0.0"
-      }
-    },
-    "pre-commit": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz",
-      "integrity": "sha1-287g7p3nI15X95xW186UZBpp7sY=",
-      "dev": true,
-      "requires": {
-        "cross-spawn": "^5.0.1",
-        "spawn-sync": "^1.0.15",
-        "which": "1.2.x"
-      },
-      "dependencies": {
-        "cross-spawn": {
-          "version": "5.1.0",
-          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
-          "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
-          "dev": true,
-          "requires": {
-            "lru-cache": "^4.0.1",
-            "shebang-command": "^1.2.0",
-            "which": "^1.2.9"
-          }
-        },
-        "which": {
-          "version": "1.2.14",
-          "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
-          "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=",
-          "dev": true,
-          "requires": {
-            "isexe": "^2.0.0"
-          }
-        }
-      }
-    },
-    "precond": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
-      "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
-    },
-    "prelude-ls": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
-      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
-      "dev": true
-    },
-    "prettier": {
-      "version": "1.19.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
-      "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
-      "dev": true
-    },
-    "prettier-eslint": {
-      "version": "9.0.1",
-      "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-9.0.1.tgz",
-      "integrity": "sha512-KZT65QTosSAqBBqmrC+RpXbsMRe7Os2YSR9cAfFbDlyPAopzA/S5bioiZ3rpziNQNSJaOxmtXSx07EQ+o2Dlug==",
-      "dev": true,
-      "requires": {
-        "@typescript-eslint/parser": "^1.10.2",
-        "common-tags": "^1.4.0",
-        "core-js": "^3.1.4",
-        "dlv": "^1.1.0",
-        "eslint": "^5.0.0",
-        "indent-string": "^4.0.0",
-        "lodash.merge": "^4.6.0",
-        "loglevel-colored-level-prefix": "^1.0.0",
-        "prettier": "^1.7.0",
-        "pretty-format": "^23.0.1",
-        "require-relative": "^0.8.7",
-        "typescript": "^3.2.1",
-        "vue-eslint-parser": "^2.0.2"
-      },
-      "dependencies": {
-        "acorn": {
-          "version": "6.4.1",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
-          "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
-          "dev": true
-        },
-        "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^3.1.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "ansi-escapes": {
-          "version": "3.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
-          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
-          "dev": true
-        },
-        "ansi-regex": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
-          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
-          "dev": true
-        },
-        "cli-cursor": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
-          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
-          "dev": true,
-          "requires": {
-            "restore-cursor": "^2.0.0"
-          }
-        },
-        "core-js": {
-          "version": "3.6.4",
-          "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
-          "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==",
-          "dev": true
-        },
-        "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-          "dev": true,
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "eslint": {
-          "version": "5.16.0",
-          "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
-          "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "ajv": "^6.9.1",
-            "chalk": "^2.1.0",
-            "cross-spawn": "^6.0.5",
-            "debug": "^4.0.1",
-            "doctrine": "^3.0.0",
-            "eslint-scope": "^4.0.3",
-            "eslint-utils": "^1.3.1",
-            "eslint-visitor-keys": "^1.0.0",
-            "espree": "^5.0.1",
-            "esquery": "^1.0.1",
-            "esutils": "^2.0.2",
-            "file-entry-cache": "^5.0.1",
-            "functional-red-black-tree": "^1.0.1",
-            "glob": "^7.1.2",
-            "globals": "^11.7.0",
-            "ignore": "^4.0.6",
-            "import-fresh": "^3.0.0",
-            "imurmurhash": "^0.1.4",
-            "inquirer": "^6.2.2",
-            "js-yaml": "^3.13.0",
-            "json-stable-stringify-without-jsonify": "^1.0.1",
-            "levn": "^0.3.0",
-            "lodash": "^4.17.11",
-            "minimatch": "^3.0.4",
-            "mkdirp": "^0.5.1",
-            "natural-compare": "^1.4.0",
-            "optionator": "^0.8.2",
-            "path-is-inside": "^1.0.2",
-            "progress": "^2.0.0",
-            "regexpp": "^2.0.1",
-            "semver": "^5.5.1",
-            "strip-ansi": "^4.0.0",
-            "strip-json-comments": "^2.0.1",
-            "table": "^5.2.3",
-            "text-table": "^0.2.0"
-          }
-        },
-        "eslint-scope": {
-          "version": "4.0.3",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
-          "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
-          "dev": true,
-          "requires": {
-            "esrecurse": "^4.1.0",
-            "estraverse": "^4.1.1"
-          }
-        },
-        "espree": {
-          "version": "5.0.1",
-          "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
-          "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
-          "dev": true,
-          "requires": {
-            "acorn": "^6.0.7",
-            "acorn-jsx": "^5.0.0",
-            "eslint-visitor-keys": "^1.0.0"
-          }
-        },
-        "fast-deep-equal": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
-          "dev": true
-        },
-        "figures": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
-          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
-          "dev": true,
-          "requires": {
-            "escape-string-regexp": "^1.0.5"
-          }
-        },
-        "globals": {
-          "version": "11.12.0",
-          "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-          "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
-          "dev": true
-        },
-        "indent-string": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-          "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
-          "dev": true
-        },
-        "inquirer": {
-          "version": "6.5.2",
-          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
-          "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
-          "dev": true,
-          "requires": {
-            "ansi-escapes": "^3.2.0",
-            "chalk": "^2.4.2",
-            "cli-cursor": "^2.1.0",
-            "cli-width": "^2.0.0",
-            "external-editor": "^3.0.3",
-            "figures": "^2.0.0",
-            "lodash": "^4.17.12",
-            "mute-stream": "0.0.7",
-            "run-async": "^2.2.0",
-            "rxjs": "^6.4.0",
-            "string-width": "^2.1.0",
-            "strip-ansi": "^5.1.0",
-            "through": "^2.3.6"
-          },
-          "dependencies": {
-            "ansi-regex": {
-              "version": "4.1.0",
-              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-              "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-              "dev": true
-            },
-            "strip-ansi": {
-              "version": "5.2.0",
-              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-              "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-              "dev": true,
-              "requires": {
-                "ansi-regex": "^4.1.0"
-              }
-            }
-          }
-        },
-        "is-fullwidth-code-point": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
-        "mimic-fn": {
-          "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
-          "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
-          "dev": true
-        },
-        "mute-stream": {
-          "version": "0.0.7",
-          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
-          "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
-          "dev": true
-        },
-        "onetime": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
-          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
-          "dev": true,
-          "requires": {
-            "mimic-fn": "^1.0.0"
-          }
-        },
-        "restore-cursor": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
-          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
-          "dev": true,
-          "requires": {
-            "onetime": "^2.0.0",
-            "signal-exit": "^3.0.2"
-          }
-        },
-        "string-width": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
-          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
-          "dev": true,
-          "requires": {
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^4.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
-          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^3.0.0"
-          }
-        }
-      }
-    },
-    "prettier-linter-helpers": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
-      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
-      "dev": true,
-      "requires": {
-        "fast-diff": "^1.1.2"
-      }
-    },
-    "pretty-format": {
-      "version": "23.6.0",
-      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz",
-      "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==",
-      "dev": true,
-      "requires": {
-        "ansi-regex": "^3.0.0",
-        "ansi-styles": "^3.2.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
-          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
-          "dev": true
-        }
-      }
-    },
-    "process-nextick-args": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
-      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
-    },
-    "progress": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
-      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
-      "dev": true
-    },
-    "pseudomap": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
-      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
-      "dev": true
-    },
-    "pump": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-      "dev": true,
-      "requires": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "punycode": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
-    },
-    "qs": {
-      "version": "6.9.1",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz",
-      "integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA=="
-    },
-    "rc": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
-      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
-      "requires": {
-        "deep-extend": "^0.6.0",
-        "ini": "~1.3.0",
-        "minimist": "^1.2.0",
-        "strip-json-comments": "~2.0.1"
-      }
-    },
-    "read-pkg": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
-      "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
-      "dev": true,
-      "requires": {
-        "load-json-file": "^2.0.0",
-        "normalize-package-data": "^2.3.2",
-        "path-type": "^2.0.0"
-      }
-    },
-    "read-pkg-up": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
-      "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
-      "dev": true,
-      "requires": {
-        "find-up": "^2.0.0",
-        "read-pkg": "^2.0.0"
-      }
-    },
-    "readable-stream": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
-      "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
-      "requires": {
-        "core-util-is": "~1.0.0",
-        "inherits": "~2.0.3",
-        "isarray": "~1.0.0",
-        "process-nextick-args": "~2.0.0",
-        "safe-buffer": "~5.1.1",
-        "string_decoder": "~1.1.1",
-        "util-deprecate": "~1.0.1"
-      }
-    },
-    "regenerator-runtime": {
-      "version": "0.13.5",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
-      "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
-    },
-    "regexpp": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
-      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
-      "dev": true
-    },
-    "require-relative": {
-      "version": "0.8.7",
-      "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
-      "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
-      "dev": true
-    },
-    "require_optional": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
-      "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
-      "requires": {
-        "resolve-from": "^2.0.0",
-        "semver": "^5.1.0"
-      }
-    },
-    "resolve": {
-      "version": "1.15.1",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
-      "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
-      "dev": true,
-      "requires": {
-        "path-parse": "^1.0.6"
-      }
-    },
-    "resolve-from": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
-      "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
-    },
-    "restore-cursor": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
-      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
-      "dev": true,
-      "requires": {
-        "onetime": "^5.1.0",
-        "signal-exit": "^3.0.2"
-      }
-    },
-    "rimraf": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
-      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
-      "requires": {
-        "glob": "^7.1.3"
-      }
-    },
-    "run-async": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz",
-      "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==",
-      "dev": true,
-      "requires": {
-        "is-promise": "^2.1.0"
-      }
-    },
-    "rxjs": {
-      "version": "6.5.4",
-      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
-      "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
-      "dev": true,
-      "requires": {
-        "tslib": "^1.9.0"
-      }
-    },
-    "safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
-    },
-    "safe-json-stringify": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
-      "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
-      "optional": true
-    },
-    "safer-buffer": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
-    },
-    "saslprep": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
-      "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
-      "optional": true,
-      "requires": {
-        "sparse-bitfield": "^3.0.3"
-      }
-    },
-    "sax": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
-    },
-    "semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-    },
-    "semver-compare": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
-      "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
-      "dev": true
-    },
-    "set-blocking": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
-    },
-    "shebang-command": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
-      "dev": true,
-      "requires": {
-        "shebang-regex": "^1.0.0"
-      }
-    },
-    "shebang-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
-      "dev": true
-    },
-    "signal-exit": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
-      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
-    },
-    "slice-ansi": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
-      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
-      "dev": true,
-      "requires": {
-        "ansi-styles": "^3.2.0",
-        "astral-regex": "^1.0.0",
-        "is-fullwidth-code-point": "^2.0.0"
-      },
-      "dependencies": {
-        "is-fullwidth-code-point": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-          "dev": true
-        }
-      }
-    },
-    "source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
-    },
-    "source-map-support": {
-      "version": "0.5.16",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
-      "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
-      "requires": {
-        "buffer-from": "^1.0.0",
-        "source-map": "^0.6.0"
-      }
-    },
-    "sparse-bitfield": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
-      "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
-      "optional": true,
-      "requires": {
-        "memory-pager": "^1.0.2"
-      }
-    },
-    "spawn-sync": {
-      "version": "1.0.15",
-      "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz",
-      "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=",
-      "dev": true,
-      "requires": {
-        "concat-stream": "^1.4.7",
-        "os-shim": "^0.1.2"
-      }
-    },
-    "spdx-correct": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
-      "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
-      "dev": true,
-      "requires": {
-        "spdx-expression-parse": "^3.0.0",
-        "spdx-license-ids": "^3.0.0"
-      }
-    },
-    "spdx-exceptions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
-      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
-      "dev": true
-    },
-    "spdx-expression-parse": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
-      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
-      "dev": true,
-      "requires": {
-        "spdx-exceptions": "^2.1.0",
-        "spdx-license-ids": "^3.0.0"
-      }
-    },
-    "spdx-license-ids": {
-      "version": "3.0.5",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
-      "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
-      "dev": true
-    },
-    "sprintf-js": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
-      "dev": true
-    },
-    "string-argv": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
-      "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
-      "dev": true
-    },
-    "string-width": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
-      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
-      "requires": {
-        "code-point-at": "^1.0.0",
-        "is-fullwidth-code-point": "^1.0.0",
-        "strip-ansi": "^3.0.0"
-      }
-    },
-    "string.prototype.trimleft": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
-      "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.3",
-        "function-bind": "^1.1.1"
-      }
-    },
-    "string.prototype.trimright": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
-      "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
-      "dev": true,
-      "requires": {
-        "define-properties": "^1.1.3",
-        "function-bind": "^1.1.1"
-      }
-    },
-    "string_decoder": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
-      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-      "requires": {
-        "safe-buffer": "~5.1.0"
-      }
-    },
-    "stringify-object": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
-      "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
-      "dev": true,
-      "requires": {
-        "get-own-enumerable-property-symbols": "^3.0.0",
-        "is-obj": "^1.0.1",
-        "is-regexp": "^1.0.0"
-      }
-    },
-    "strip-ansi": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
-      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
-      "requires": {
-        "ansi-regex": "^2.0.0"
-      }
-    },
-    "strip-bom": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
-      "dev": true
-    },
-    "strip-final-newline": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
-      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
-      "dev": true
-    },
-    "strip-json-comments": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
-      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
-    },
-    "supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
-      "requires": {
-        "has-flag": "^3.0.0"
-      }
-    },
-    "symbol-observable": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
-      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
-      "dev": true
-    },
-    "table": {
-      "version": "5.4.6",
-      "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
-      "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
-      "dev": true,
-      "requires": {
-        "ajv": "^6.10.2",
-        "lodash": "^4.17.14",
-        "slice-ansi": "^2.1.0",
-        "string-width": "^3.0.0"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
-          "dev": true,
-          "requires": {
-            "fast-deep-equal": "^3.1.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "ansi-regex": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
-          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
-          "dev": true
-        },
-        "emoji-regex": {
-          "version": "7.0.3",
-          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
-          "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
-          "dev": true
-        },
-        "fast-deep-equal": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-          "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
-          "dev": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-          "dev": true
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-          "dev": true
-        },
-        "string-width": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
-          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-          "dev": true,
-          "requires": {
-            "emoji-regex": "^7.0.1",
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^5.1.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^4.1.0"
-          }
-        }
-      }
-    },
-    "tar": {
-      "version": "4.4.13",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
-      "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
-      "requires": {
-        "chownr": "^1.1.1",
-        "fs-minipass": "^1.2.5",
-        "minipass": "^2.8.6",
-        "minizlib": "^1.2.1",
-        "mkdirp": "^0.5.0",
-        "safe-buffer": "^5.1.2",
-        "yallist": "^3.0.3"
-      }
-    },
-    "text-table": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
-      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
-      "dev": true
-    },
-    "through": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
-      "dev": true
-    },
-    "tmp": {
-      "version": "0.0.33",
-      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
-      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
-      "dev": true,
-      "requires": {
-        "os-tmpdir": "~1.0.2"
-      }
-    },
-    "to-regex-range": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
-      "requires": {
-        "is-number": "^7.0.0"
-      }
-    },
-    "tslib": {
-      "version": "1.11.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
-      "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==",
-      "dev": true
-    },
-    "type-check": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
-      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
-      "dev": true,
-      "requires": {
-        "prelude-ls": "~1.1.2"
-      }
-    },
-    "type-fest": {
-      "version": "0.8.1",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
-      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
-      "dev": true
-    },
-    "typedarray": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
-      "dev": true
-    },
-    "typescript": {
-      "version": "3.8.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
-      "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
-      "dev": true
-    },
-    "uri-js": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
-      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
-      "requires": {
-        "punycode": "^2.1.0"
-      }
-    },
-    "util-deprecate": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
-    },
-    "v8-compile-cache": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
-      "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
-      "dev": true
-    },
-    "validate-npm-package-license": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
-      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
-      "dev": true,
-      "requires": {
-        "spdx-correct": "^3.0.0",
-        "spdx-expression-parse": "^3.0.0"
-      }
-    },
-    "vasync": {
-      "version": "1.6.4",
-      "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
-      "integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
-      "requires": {
-        "verror": "1.6.0"
-      },
-      "dependencies": {
-        "verror": {
-          "version": "1.6.0",
-          "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
-          "integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
-          "requires": {
-            "extsprintf": "1.2.0"
-          }
-        }
-      }
-    },
-    "verror": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
-      "requires": {
-        "assert-plus": "^1.0.0",
-        "core-util-is": "1.0.2",
-        "extsprintf": "^1.2.0"
-      }
-    },
-    "vue-eslint-parser": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz",
-      "integrity": "sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw==",
-      "dev": true,
-      "requires": {
-        "debug": "^3.1.0",
-        "eslint-scope": "^3.7.1",
-        "eslint-visitor-keys": "^1.0.0",
-        "espree": "^3.5.2",
-        "esquery": "^1.0.0",
-        "lodash": "^4.17.4"
-      },
-      "dependencies": {
-        "acorn": {
-          "version": "5.7.4",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
-          "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==",
-          "dev": true
-        },
-        "acorn-jsx": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
-          "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
-          "dev": true,
-          "requires": {
-            "acorn": "^3.0.4"
-          },
-          "dependencies": {
-            "acorn": {
-              "version": "3.3.0",
-              "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
-              "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
-              "dev": true
-            }
-          }
-        },
-        "eslint-scope": {
-          "version": "3.7.3",
-          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
-          "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
-          "dev": true,
-          "requires": {
-            "esrecurse": "^4.1.0",
-            "estraverse": "^4.1.1"
-          }
-        },
-        "espree": {
-          "version": "3.5.4",
-          "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
-          "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
-          "dev": true,
-          "requires": {
-            "acorn": "^5.5.0",
-            "acorn-jsx": "^3.0.0"
-          }
-        }
-      }
-    },
-    "which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
-      "requires": {
-        "isexe": "^2.0.0"
-      }
-    },
-    "wide-align": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
-      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
-      "requires": {
-        "string-width": "^1.0.2 || 2"
-      }
-    },
-    "word-wrap": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
-      "dev": true
-    },
-    "wrap-ansi": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz",
-      "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=",
-      "dev": true,
-      "requires": {
-        "string-width": "^2.1.1",
-        "strip-ansi": "^4.0.0"
-      },
-      "dependencies": {
-        "ansi-regex": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
-          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
-          "dev": true
-        },
-        "is-fullwidth-code-point": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-          "dev": true
-        },
-        "string-width": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
-          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
-          "dev": true,
-          "requires": {
-            "is-fullwidth-code-point": "^2.0.0",
-            "strip-ansi": "^4.0.0"
-          }
-        },
-        "strip-ansi": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
-          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
-          "dev": true,
-          "requires": {
-            "ansi-regex": "^3.0.0"
-          }
-        }
-      }
-    },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
-    },
-    "write": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
-      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
-      "dev": true,
-      "requires": {
-        "mkdirp": "^0.5.1"
-      }
-    },
-    "xss": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.6.tgz",
-      "integrity": "sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A==",
-      "requires": {
-        "commander": "^2.9.0",
-        "cssfilter": "0.0.10"
-      }
-    },
-    "yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
-    },
-    "yaml": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.2.tgz",
-      "integrity": "sha512-omakb0d7FjMo3R1D2EbTKVIk6dAVLRxFXdLZMEUToeAvuqgG/YuHMuQOZ5fgk+vQ8cx+cnGKwyg+8g8PNT0xQg==",
-      "dev": true,
-      "requires": {
-        "@babel/runtime": "^7.8.7"
-      },
-      "dependencies": {
-        "@babel/runtime": {
-          "version": "7.8.7",
-          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.7.tgz",
-          "integrity": "sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==",
-          "dev": true,
-          "requires": {
-            "regenerator-runtime": "^0.13.4"
-          }
-        },
-        "regenerator-runtime": {
-          "version": "0.13.5",
-          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
-          "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
-          "dev": true
-        }
-      }
-    }
-  }
-}

+ 0 - 73
.sandstorm-meteor-1.8/package.json

@@ -1,73 +0,0 @@
-{
-  "name": "wekan",
-  "version": "v3.90.0",
-  "description": "Open-Source kanban",
-  "private": true,
-  "scripts": {
-    "lint": "eslint --cache --ext .js --ignore-path .eslintignore .",
-    "lint:eslint:fix": "eslint --ext .js --ignore-path .eslintignore --fix .",
-    "lint:staged": "lint-staged",
-    "prettify": "prettier --write '**/*.js' '**/*.jsx'",
-    "test": "npm run lint"
-  },
-  "lint-staged": {
-    "*.js": [
-      "meteor npm run prettify",
-      "meteor npm run lint:eslint:fix",
-      "git add --force"
-    ],
-    "*.jsx": [
-      "meteor npm run prettify",
-      "meteor npm run lint:eslint:fix",
-      "git add --force"
-    ],
-    "*.json": [
-      "prettier --write",
-      "git add --force"
-    ]
-  },
-  "pre-commit": "lint:staged",
-  "eslintConfig": {
-    "extends": "@meteorjs/eslint-config-meteor"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/wekan/wekan.git"
-  },
-  "license": "MIT",
-  "bugs": {
-    "url": "https://github.com/wekan/wekan/issues"
-  },
-  "homepage": "https://wekan.github.io",
-  "devDependencies": {
-    "eslint": "^6.8.0",
-    "eslint-config-meteor": "^0.1.1",
-    "eslint-config-prettier": "^6.10.0",
-    "eslint-import-resolver-meteor": "^0.4.0",
-    "eslint-plugin-import": "^2.20.1",
-    "eslint-plugin-meteor": "^6.0.0",
-    "eslint-plugin-prettier": "^3.1.2",
-    "lint-staged": "^10.0.8",
-    "pre-commit": "^1.2.2",
-    "prettier": "^1.19.1",
-    "prettier-eslint": "^9.0.1"
-  },
-  "dependencies": {
-    "@babel/runtime": "^7.8.7",
-    "ajv": "^6.12.0",
-    "babel-runtime": "^6.26.0",
-    "bcrypt": "^4.0.1",
-    "bson": "^4.0.3",
-    "bunyan": "^1.8.12",
-    "es6-promise": "^4.2.8",
-    "gridfs-stream": "^1.1.1",
-    "ldapjs": "^1.0.2",
-    "meteor-node-stubs": "^1.0.0",
-    "mongodb": "^3.5.5",
-    "os": "^0.1.1",
-    "page": "^1.11.5",
-    "qs": "^6.9.1",
-    "source-map-support": "^0.5.16",
-    "xss": "^1.0.6"
-  }
-}

+ 0 - 853
.sandstorm-meteor-1.8/wekanCreator.js

@@ -1,853 +0,0 @@
-const DateString = Match.Where(function(dateAsString) {
-  check(dateAsString, String);
-  return moment(dateAsString, moment.ISO_8601).isValid();
-});
-
-export class WekanCreator {
-  constructor(data) {
-    // we log current date, to use the same timestamp for all our actions.
-    // this helps to retrieve all elements performed by the same import.
-    this._nowDate = new Date();
-    // The object creation dates, indexed by Wekan id
-    // (so we only parse actions once!)
-    this.createdAt = {
-      board: null,
-      cards: {},
-      lists: {},
-      swimlanes: {},
-    };
-    // The object creator Wekan Id, indexed by the object Wekan id
-    // (so we only parse actions once!)
-    this.createdBy = {
-      cards: {}, // only cards have a field for that
-    };
-
-    // Map of labels Wekan ID => Wekan ID
-    this.labels = {};
-    // Map of swimlanes Wekan ID => Wekan ID
-    this.swimlanes = {};
-    // Map of lists Wekan ID => Wekan ID
-    this.lists = {};
-    // Map of cards Wekan ID => Wekan ID
-    this.cards = {};
-    // Map of comments Wekan ID => Wekan ID
-    this.commentIds = {};
-    // Map of attachments Wekan ID => Wekan ID
-    this.attachmentIds = {};
-    // Map of checklists Wekan ID => Wekan ID
-    this.checklists = {};
-    // Map of checklistItems Wekan ID => Wekan ID
-    this.checklistItems = {};
-    // The comments, indexed by Wekan card id (to map when importing cards)
-    this.comments = {};
-    // Map of rules Wekan ID => Wekan ID
-    this.rules = {};
-    // the members, indexed by Wekan member id => Wekan user ID
-    this.members = data.membersMapping ? data.membersMapping : {};
-    // Map of triggers Wekan ID => Wekan ID
-    this.triggers = {};
-    // Map of actions Wekan ID => Wekan ID
-    this.actions = {};
-
-    // maps a wekanCardId to an array of wekanAttachments
-    this.attachments = {};
-  }
-
-  /**
-   * If dateString is provided,
-   * return the Date it represents.
-   * If not, will return the date when it was first called.
-   * This is useful for us, as we want all import operations to
-   * have the exact same date for easier later retrieval.
-   *
-   * @param {String} dateString a properly formatted Date
-   */
-  _now(dateString) {
-    if (dateString) {
-      return new Date(dateString);
-    }
-    if (!this._nowDate) {
-      this._nowDate = new Date();
-    }
-    return this._nowDate;
-  }
-
-  /**
-   * if wekanUserId is provided and we have a mapping,
-   * return it.
-   * Otherwise return current logged user.
-   * @param wekanUserId
-   * @private
-   */
-  _user(wekanUserId) {
-    if (wekanUserId && this.members[wekanUserId]) {
-      return this.members[wekanUserId];
-    }
-    return Meteor.userId();
-  }
-
-  checkActivities(wekanActivities) {
-    check(wekanActivities, [
-      Match.ObjectIncluding({
-        activityType: String,
-        createdAt: DateString,
-      }),
-    ]);
-    // XXX we could perform more thorough checks based on action type
-  }
-
-  checkBoard(wekanBoard) {
-    check(
-      wekanBoard,
-      Match.ObjectIncluding({
-        archived: Boolean,
-        title: String,
-        // XXX refine control by validating 'color' against a list of
-        // allowed values (is it worth the maintenance?)
-        color: String,
-        permission: Match.Where(value => {
-          return ['private', 'public'].indexOf(value) >= 0;
-        }),
-      }),
-    );
-  }
-
-  checkCards(wekanCards) {
-    check(wekanCards, [
-      Match.ObjectIncluding({
-        archived: Boolean,
-        dateLastActivity: DateString,
-        labelIds: [String],
-        title: String,
-        sort: Number,
-      }),
-    ]);
-  }
-
-  checkLabels(wekanLabels) {
-    check(wekanLabels, [
-      Match.ObjectIncluding({
-        // XXX refine control by validating 'color' against a list of allowed
-        // values (is it worth the maintenance?)
-        color: String,
-      }),
-    ]);
-  }
-
-  checkLists(wekanLists) {
-    check(wekanLists, [
-      Match.ObjectIncluding({
-        archived: Boolean,
-        title: String,
-      }),
-    ]);
-  }
-
-  checkSwimlanes(wekanSwimlanes) {
-    check(wekanSwimlanes, [
-      Match.ObjectIncluding({
-        archived: Boolean,
-        title: String,
-      }),
-    ]);
-  }
-
-  checkChecklists(wekanChecklists) {
-    check(wekanChecklists, [
-      Match.ObjectIncluding({
-        cardId: String,
-        title: String,
-      }),
-    ]);
-  }
-
-  checkChecklistItems(wekanChecklistItems) {
-    check(wekanChecklistItems, [
-      Match.ObjectIncluding({
-        cardId: String,
-        title: String,
-      }),
-    ]);
-  }
-
-  checkRules(wekanRules) {
-    check(wekanRules, [
-      Match.ObjectIncluding({
-        triggerId: String,
-        actionId: String,
-        title: String,
-      }),
-    ]);
-  }
-
-  checkTriggers(wekanTriggers) {
-    // XXX More check based on trigger type
-    check(wekanTriggers, [
-      Match.ObjectIncluding({
-        activityType: String,
-        desc: String,
-      }),
-    ]);
-  }
-
-  getMembersToMap(data) {
-    // we will work on the list itself (an ordered array of objects) when a
-    // mapping is done, we add a 'wekan' field to the object representing the
-    // imported member
-    const membersToMap = data.members;
-    const users = data.users;
-    // auto-map based on username
-    membersToMap.forEach(importedMember => {
-      importedMember.id = importedMember.userId;
-      delete importedMember.userId;
-      const user = users.filter(user => {
-        return user._id === importedMember.id;
-      })[0];
-      if (user.profile && user.profile.fullname) {
-        importedMember.fullName = user.profile.fullname;
-      }
-      importedMember.username = user.username;
-      const wekanUser = Users.findOne({ username: importedMember.username });
-      if (wekanUser) {
-        importedMember.wekanId = wekanUser._id;
-      }
-    });
-    return membersToMap;
-  }
-
-  checkActions(wekanActions) {
-    // XXX More check based on action type
-    check(wekanActions, [
-      Match.ObjectIncluding({
-        actionType: String,
-        desc: String,
-      }),
-    ]);
-  }
-
-  // You must call parseActions before calling this one.
-  createBoardAndLabels(boardToImport) {
-    const boardToCreate = {
-      archived: boardToImport.archived,
-      color: boardToImport.color,
-      // very old boards won't have a creation activity so no creation date
-      createdAt: this._now(boardToImport.createdAt),
-      labels: [],
-      members: [
-        {
-          userId: Meteor.userId(),
-          wekanId: Meteor.userId(),
-          isActive: true,
-          isAdmin: true,
-          isNoComments: false,
-          isCommentOnly: false,
-          swimlaneId: false,
-        },
-      ],
-      // Standalone Export has modifiedAt missing, adding modifiedAt to fix it
-      modifiedAt: this._now(boardToImport.modifiedAt),
-      permission: boardToImport.permission,
-      slug: getSlug(boardToImport.title) || 'board',
-      stars: 0,
-      title: boardToImport.title,
-    };
-    // now add other members
-    if (boardToImport.members) {
-      boardToImport.members.forEach(wekanMember => {
-        // do we already have it in our list?
-        if (
-          !boardToCreate.members.some(
-            member => member.wekanId === wekanMember.wekanId,
-          )
-        )
-          boardToCreate.members.push({
-            ...wekanMember,
-            userId: wekanMember.wekanId,
-          });
-      });
-    }
-    boardToImport.labels.forEach(label => {
-      const labelToCreate = {
-        _id: Random.id(6),
-        color: label.color,
-        name: label.name,
-      };
-      // We need to remember them by Wekan ID, as this is the only ref we have
-      // when importing cards.
-      this.labels[label._id] = labelToCreate._id;
-      boardToCreate.labels.push(labelToCreate);
-    });
-    const boardId = Boards.direct.insert(boardToCreate);
-    Boards.direct.update(boardId, {
-      $set: {
-        modifiedAt: this._now(),
-      },
-    });
-    // log activity
-    Activities.direct.insert({
-      activityType: 'importBoard',
-      boardId,
-      createdAt: this._now(),
-      source: {
-        id: boardToImport.id,
-        system: 'Wekan',
-      },
-      // We attribute the import to current user,
-      // not the author from the original object.
-      userId: this._user(),
-    });
-    return boardId;
-  }
-
-  /**
-   * Create the Wekan cards corresponding to the supplied Wekan cards,
-   * as well as all linked data: activities, comments, and attachments
-   * @param wekanCards
-   * @param boardId
-   * @returns {Array}
-   */
-  createCards(wekanCards, boardId) {
-    const result = [];
-    wekanCards.forEach(card => {
-      const cardToCreate = {
-        archived: card.archived,
-        boardId,
-        // very old boards won't have a creation activity so no creation date
-        createdAt: this._now(this.createdAt.cards[card._id]),
-        dateLastActivity: this._now(),
-        description: card.description,
-        listId: this.lists[card.listId],
-        swimlaneId: this.swimlanes[card.swimlaneId],
-        sort: card.sort,
-        title: card.title,
-        // we attribute the card to its creator if available
-        userId: this._user(this.createdBy.cards[card._id]),
-        isOvertime: card.isOvertime || false,
-        startAt: card.startAt ? this._now(card.startAt) : null,
-        dueAt: card.dueAt ? this._now(card.dueAt) : null,
-        spentTime: card.spentTime || null,
-      };
-      // add labels
-      if (card.labelIds) {
-        cardToCreate.labelIds = card.labelIds.map(wekanId => {
-          return this.labels[wekanId];
-        });
-      }
-      // add members {
-      if (card.members) {
-        const wekanMembers = [];
-        // we can't just map, as some members may not have been mapped
-        card.members.forEach(sourceMemberId => {
-          if (this.members[sourceMemberId]) {
-            const wekanId = this.members[sourceMemberId];
-            // we may map multiple Wekan members to the same wekan user
-            // in which case we risk adding the same user multiple times
-            if (!wekanMembers.find(wId => wId === wekanId)) {
-              wekanMembers.push(wekanId);
-            }
-          }
-          return true;
-        });
-        if (wekanMembers.length > 0) {
-          cardToCreate.members = wekanMembers;
-        }
-      }
-      // set color
-      if (card.color) {
-        cardToCreate.color = card.color;
-      }
-      // insert card
-      const cardId = Cards.direct.insert(cardToCreate);
-      // keep track of Wekan id => Wekan id
-      this.cards[card._id] = cardId;
-      // // log activity
-      // Activities.direct.insert({
-      //   activityType: 'importCard',
-      //   boardId,
-      //   cardId,
-      //   createdAt: this._now(),
-      //   listId: cardToCreate.listId,
-      //   source: {
-      //     id: card._id,
-      //     system: 'Wekan',
-      //   },
-      //   // we attribute the import to current user,
-      //   // not the author of the original card
-      //   userId: this._user(),
-      // });
-      // add comments
-      const comments = this.comments[card._id];
-      if (comments) {
-        comments.forEach(comment => {
-          const commentToCreate = {
-            boardId,
-            cardId,
-            createdAt: this._now(comment.createdAt),
-            text: comment.text,
-            // we attribute the comment to the original author, default to current user
-            userId: this._user(comment.userId),
-          };
-          // dateLastActivity will be set from activity insert, no need to
-          // update it ourselves
-          const commentId = CardComments.direct.insert(commentToCreate);
-          this.commentIds[comment._id] = commentId;
-          // Activities.direct.insert({
-          //   activityType: 'addComment',
-          //   boardId: commentToCreate.boardId,
-          //   cardId: commentToCreate.cardId,
-          //   commentId,
-          //   createdAt: this._now(commentToCreate.createdAt),
-          //   // we attribute the addComment (not the import)
-          //   // to the original author - it is needed by some UI elements.
-          //   userId: commentToCreate.userId,
-          // });
-        });
-      }
-      const attachments = this.attachments[card._id];
-      const wekanCoverId = card.coverId;
-      if (attachments) {
-        attachments.forEach(att => {
-          const file = new FS.File();
-          // Simulating file.attachData on the client generates multiple errors
-          // - HEAD returns null, which causes exception down the line
-          // - the template then tries to display the url to the attachment which causes other errors
-          // so we make it server only, and let UI catch up once it is done, forget about latency comp.
-          const self = this;
-          if (Meteor.isServer) {
-            if (att.url) {
-              file.attachData(att.url, function(error) {
-                file.boardId = boardId;
-                file.cardId = cardId;
-                file.userId = self._user(att.userId);
-                // The field source will only be used to prevent adding
-                // attachments' related activities automatically
-                file.source = 'import';
-                if (error) {
-                  throw error;
-                } else {
-                  const wekanAtt = Attachments.insert(file, () => {
-                    // we do nothing
-                  });
-                  self.attachmentIds[att._id] = wekanAtt._id;
-                  //
-                  if (wekanCoverId === att._id) {
-                    Cards.direct.update(cardId, {
-                      $set: {
-                        coverId: wekanAtt._id,
-                      },
-                    });
-                  }
-                }
-              });
-            } else if (att.file) {
-              file.attachData(
-                new Buffer(att.file, 'base64'),
-                {
-                  type: att.type,
-                },
-                error => {
-                  file.name(att.name);
-                  file.boardId = boardId;
-                  file.cardId = cardId;
-                  file.userId = self._user(att.userId);
-                  // The field source will only be used to prevent adding
-                  // attachments' related activities automatically
-                  file.source = 'import';
-                  if (error) {
-                    throw error;
-                  } else {
-                    const wekanAtt = Attachments.insert(file, () => {
-                      // we do nothing
-                    });
-                    this.attachmentIds[att._id] = wekanAtt._id;
-                    //
-                    if (wekanCoverId === att._id) {
-                      Cards.direct.update(cardId, {
-                        $set: {
-                          coverId: wekanAtt._id,
-                        },
-                      });
-                    }
-                  }
-                },
-              );
-            }
-          }
-          // todo XXX set cover - if need be
-        });
-      }
-      result.push(cardId);
-    });
-    return result;
-  }
-
-  // Create labels if they do not exist and load this.labels.
-  createLabels(wekanLabels, board) {
-    wekanLabels.forEach(label => {
-      const color = label.color;
-      const name = label.name;
-      const existingLabel = board.getLabel(name, color);
-      if (existingLabel) {
-        this.labels[label.id] = existingLabel._id;
-      } else {
-        const idLabelCreated = board.pushLabel(name, color);
-        this.labels[label.id] = idLabelCreated;
-      }
-    });
-  }
-
-  createLists(wekanLists, boardId) {
-    wekanLists.forEach((list, listIndex) => {
-      const listToCreate = {
-        archived: list.archived,
-        boardId,
-        // We are being defensing here by providing a default date (now) if the
-        // creation date wasn't found on the action log. This happen on old
-        // Wekan boards (eg from 2013) that didn't log the 'createList' action
-        // we require.
-        createdAt: this._now(this.createdAt.lists[list.id]),
-        title: list.title,
-        sort: list.sort ? list.sort : listIndex,
-      };
-      const listId = Lists.direct.insert(listToCreate);
-      Lists.direct.update(listId, {
-        $set: {
-          updatedAt: this._now(),
-        },
-      });
-      this.lists[list._id] = listId;
-      // // log activity
-      // Activities.direct.insert({
-      //   activityType: 'importList',
-      //   boardId,
-      //   createdAt: this._now(),
-      //   listId,
-      //   source: {
-      //     id: list._id,
-      //     system: 'Wekan',
-      //   },
-      //   // We attribute the import to current user,
-      //   // not the creator of the original object
-      //   userId: this._user(),
-      // });
-    });
-  }
-
-  createSwimlanes(wekanSwimlanes, boardId) {
-    wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
-      const swimlaneToCreate = {
-        archived: swimlane.archived,
-        boardId,
-        // We are being defensing here by providing a default date (now) if the
-        // creation date wasn't found on the action log. This happen on old
-        // Wekan boards (eg from 2013) that didn't log the 'createList' action
-        // we require.
-        createdAt: this._now(this.createdAt.swimlanes[swimlane._id]),
-        title: swimlane.title,
-        sort: swimlane.sort ? swimlane.sort : swimlaneIndex,
-      };
-      // set color
-      if (swimlane.color) {
-        swimlaneToCreate.color = swimlane.color;
-      }
-      const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
-      Swimlanes.direct.update(swimlaneId, {
-        $set: {
-          updatedAt: this._now(),
-        },
-      });
-      this.swimlanes[swimlane._id] = swimlaneId;
-    });
-  }
-
-  createChecklists(wekanChecklists) {
-    const result = [];
-    wekanChecklists.forEach((checklist, checklistIndex) => {
-      // Create the checklist
-      const checklistToCreate = {
-        cardId: this.cards[checklist.cardId],
-        title: checklist.title,
-        createdAt: checklist.createdAt,
-        sort: checklist.sort ? checklist.sort : checklistIndex,
-      };
-      const checklistId = Checklists.direct.insert(checklistToCreate);
-      this.checklists[checklist._id] = checklistId;
-      result.push(checklistId);
-    });
-    return result;
-  }
-
-  createTriggers(wekanTriggers, boardId) {
-    wekanTriggers.forEach(trigger => {
-      if (trigger.hasOwnProperty('labelId')) {
-        trigger.labelId = this.labels[trigger.labelId];
-      }
-      if (trigger.hasOwnProperty('memberId')) {
-        trigger.memberId = this.members[trigger.memberId];
-      }
-      trigger.boardId = boardId;
-      const oldId = trigger._id;
-      delete trigger._id;
-      this.triggers[oldId] = Triggers.direct.insert(trigger);
-    });
-  }
-
-  createActions(wekanActions, boardId) {
-    wekanActions.forEach(action => {
-      if (action.hasOwnProperty('labelId')) {
-        action.labelId = this.labels[action.labelId];
-      }
-      if (action.hasOwnProperty('memberId')) {
-        action.memberId = this.members[action.memberId];
-      }
-      action.boardId = boardId;
-      const oldId = action._id;
-      delete action._id;
-      this.actions[oldId] = Actions.direct.insert(action);
-    });
-  }
-
-  createRules(wekanRules, boardId) {
-    wekanRules.forEach(rule => {
-      // Create the rule
-      rule.boardId = boardId;
-      rule.triggerId = this.triggers[rule.triggerId];
-      rule.actionId = this.actions[rule.actionId];
-      delete rule._id;
-      Rules.direct.insert(rule);
-    });
-  }
-
-  createChecklistItems(wekanChecklistItems) {
-    wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => {
-      // Create the checklistItem
-      const checklistItemTocreate = {
-        title: checklistitem.title,
-        checklistId: this.checklists[checklistitem.checklistId],
-        cardId: this.cards[checklistitem.cardId],
-        sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex,
-        isFinished: checklistitem.isFinished,
-      };
-      const checklistItemId = ChecklistItems.direct.insert(
-        checklistItemTocreate,
-      );
-      this.checklistItems[checklistitem._id] = checklistItemId;
-    });
-  }
-
-  parseActivities(wekanBoard) {
-    wekanBoard.activities.forEach(activity => {
-      switch (activity.activityType) {
-        case 'addAttachment': {
-          // We have to be cautious, because the attachment could have been removed later.
-          // In that case Wekan still reports its addition, but removes its 'url' field.
-          // So we test for that
-          const wekanAttachment = wekanBoard.attachments.filter(attachment => {
-            return attachment._id === activity.attachmentId;
-          })[0];
-
-          if (typeof wekanAttachment !== 'undefined' && wekanAttachment) {
-            if (wekanAttachment.url || wekanAttachment.file) {
-              // we cannot actually create the Wekan attachment, because we don't yet
-              // have the cards to attach it to, so we store it in the instance variable.
-              const wekanCardId = activity.cardId;
-              if (!this.attachments[wekanCardId]) {
-                this.attachments[wekanCardId] = [];
-              }
-              this.attachments[wekanCardId].push(wekanAttachment);
-            }
-          }
-          break;
-        }
-        case 'addComment': {
-          const wekanComment = wekanBoard.comments.filter(comment => {
-            return comment._id === activity.commentId;
-          })[0];
-          const id = activity.cardId;
-          if (!this.comments[id]) {
-            this.comments[id] = [];
-          }
-          this.comments[id].push(wekanComment);
-          break;
-        }
-        case 'createBoard': {
-          this.createdAt.board = activity.createdAt;
-          break;
-        }
-        case 'createCard': {
-          const cardId = activity.cardId;
-          this.createdAt.cards[cardId] = activity.createdAt;
-          this.createdBy.cards[cardId] = activity.userId;
-          break;
-        }
-        case 'createList': {
-          const listId = activity.listId;
-          this.createdAt.lists[listId] = activity.createdAt;
-          break;
-        }
-        case 'createSwimlane': {
-          const swimlaneId = activity.swimlaneId;
-          this.createdAt.swimlanes[swimlaneId] = activity.createdAt;
-          break;
-        }
-      }
-    });
-  }
-
-  importActivities(activities, boardId) {
-    activities.forEach(activity => {
-      switch (activity.activityType) {
-        // Board related activities
-        // TODO: addBoardMember, removeBoardMember
-        case 'createBoard': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            type: 'board',
-            activityTypeId: boardId,
-            activityType: activity.activityType,
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-        // List related activities
-        // TODO: removeList, archivedList
-        case 'createList': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            type: 'list',
-            activityType: activity.activityType,
-            listId: this.lists[activity.listId],
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-        // Card related activities
-        // TODO: archivedCard, restoredCard, joinMember, unjoinMember
-        case 'createCard': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            activityType: activity.activityType,
-            listId: this.lists[activity.listId],
-            cardId: this.cards[activity.cardId],
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-        case 'moveCard': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            oldListId: this.lists[activity.oldListId],
-            activityType: activity.activityType,
-            listId: this.lists[activity.listId],
-            cardId: this.cards[activity.cardId],
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-        // Comment related activities
-        case 'addComment': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            activityType: activity.activityType,
-            cardId: this.cards[activity.cardId],
-            commentId: this.commentIds[activity.commentId],
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-        // Attachment related activities
-        case 'addAttachment': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            type: 'card',
-            activityType: activity.activityType,
-            attachmentId: this.attachmentIds[activity.attachmentId],
-            cardId: this.cards[activity.cardId],
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-        // Checklist related activities
-        case 'addChecklist': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            activityType: activity.activityType,
-            cardId: this.cards[activity.cardId],
-            checklistId: this.checklists[activity.checklistId],
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-        case 'addChecklistItem': {
-          Activities.direct.insert({
-            userId: this._user(activity.userId),
-            activityType: activity.activityType,
-            cardId: this.cards[activity.cardId],
-            checklistId: this.checklists[activity.checklistId],
-            checklistItemId: activity.checklistItemId.replace(
-              activity.checklistId,
-              this.checklists[activity.checklistId],
-            ),
-            boardId,
-            createdAt: this._now(activity.createdAt),
-          });
-          break;
-        }
-      }
-    });
-  }
-
-  //check(board) {
-  check() {
-    //try {
-    // check(data, {
-    //   membersMapping: Match.Optional(Object),
-    // });
-    // this.checkActivities(board.activities);
-    // this.checkBoard(board);
-    // this.checkLabels(board.labels);
-    // this.checkLists(board.lists);
-    // this.checkSwimlanes(board.swimlanes);
-    // this.checkCards(board.cards);
-    //this.checkChecklists(board.checklists);
-    // this.checkRules(board.rules);
-    // this.checkActions(board.actions);
-    //this.checkTriggers(board.triggers);
-    //this.checkChecklistItems(board.checklistItems);
-    //} catch (e) {
-    //  throw new Meteor.Error('error-json-schema');
-    // }
-  }
-
-  create(board, currentBoardId) {
-    // TODO : Make isSandstorm variable global
-    const isSandstorm =
-      Meteor.settings &&
-      Meteor.settings.public &&
-      Meteor.settings.public.sandstorm;
-    if (isSandstorm && currentBoardId) {
-      const currentBoard = Boards.findOne(currentBoardId);
-      currentBoard.archive();
-    }
-    this.parseActivities(board);
-    const boardId = this.createBoardAndLabels(board);
-    this.createLists(board.lists, boardId);
-    this.createSwimlanes(board.swimlanes, boardId);
-    this.createCards(board.cards, boardId);
-    this.createChecklists(board.checklists);
-    this.createChecklistItems(board.checklistItems);
-    this.importActivities(board.activities, boardId);
-    this.createTriggers(board.triggers, boardId);
-    this.createActions(board.actions, boardId);
-    this.createRules(board.rules, boardId);
-    // XXX add members
-    return boardId;
-  }
-}

+ 2 - 2
.travis.yml

@@ -1,9 +1,9 @@
-dist: eoan
+dist: focal
 sudo: required
 sudo: required
 
 
 env:
 env:
   TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
   TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
-  TRAVIS_NODE_VERSION: 12.15.0
+  TRAVIS_NODE_VERSION: 12.22.3
   TRAVIS_NPM_VERSION: latest
   TRAVIS_NPM_VERSION: latest
 
 
 before_install:
 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
 # tap:i18n requires us to use `-` separator in the language identifiers whereas
 # Transifex uses a `_` separator, without an option to customize it on one side
 # Transifex uses a `_` separator, without an option to customize it on one side
 # or the other, so we need to do a Manual mapping.
 # or the other, so we need to do a Manual mapping.
-lang_map = bg_BG:bg, en_GB:en-GB, es_AR:es-AR, el_GR:el, fi_FI:fi, hu_HU:hu, id_ID:id, mn_MN:mn, no:nb, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, 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_CH:de-CH, 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
 
 
 [wekan.application]
 [wekan.application]
 file_filter = i18n/<lang>.i18n.json
 file_filter = i18n/<lang>.i18n.json

文件差异内容过多而无法显示
+ 2910 - 0
CHANGELOG.md


+ 37 - 7
Dockerfile

@@ -1,13 +1,19 @@
-FROM ubuntu:rolling
+FROM quay.io/wekan/ubuntu:groovy-20210115
 LABEL maintainer="wekan"
 LABEL maintainer="wekan"
 
 
+# 2020-12-03:
+# - Above Ubuntu base image copied from Docker Hub ubuntu:groovy-20201125.2
+#   to Quay to avoid Docker Hub rate limits.
+
 # Set the environment variables (defaults where required)
 # Set the environment variables (defaults where required)
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # ENV BUILD_DEPS="paxctl"
 # ENV BUILD_DEPS="paxctl"
+ARG DEBIAN_FRONTEND=noninteractive
+
 ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
 ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
     DEBUG=false \
     DEBUG=false \
-    NODE_VERSION=v12.16.1 \
-    METEOR_RELEASE=1.10-rc.2 \
+    NODE_VERSION=v12.22.3 \
+    METEOR_RELEASE=1.10.2 \
     USE_EDGE=false \
     USE_EDGE=false \
     METEOR_EDGE=1.5-beta.17 \
     METEOR_EDGE=1.5-beta.17 \
     NPM_VERSION=latest \
     NPM_VERSION=latest \
@@ -15,6 +21,7 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     ARCHITECTURE=linux-x64 \
     ARCHITECTURE=linux-x64 \
     SRC_PATH=./ \
     SRC_PATH=./ \
     WITH_API=true \
     WITH_API=true \
+    RESULTS_PER_PAGE="" \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
@@ -26,6 +33,7 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     ATTACHMENTS_STORE_PATH="" \
     ATTACHMENTS_STORE_PATH="" \
     MAX_IMAGE_PIXEL="" \
     MAX_IMAGE_PIXEL="" \
     IMAGE_COMPRESS_RATIO="" \
     IMAGE_COMPRESS_RATIO="" \
+    NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
     BIGEVENTS_PATTERN=NONE \
     BIGEVENTS_PATTERN=NONE \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     NOTIFY_DUE_AT_HOUR_OF_DAY="" \
     NOTIFY_DUE_AT_HOUR_OF_DAY="" \
@@ -38,6 +46,8 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     TRUSTED_URL="" \
     TRUSTED_URL="" \
     WEBHOOKS_ATTRIBUTES="" \
     WEBHOOKS_ATTRIBUTES="" \
     OAUTH2_ENABLED=false \
     OAUTH2_ENABLED=false \
+    OAUTH2_CA_CERT="" \
+    OAUTH2_ADFS_ENABLED=false \
     OAUTH2_LOGIN_STYLE=redirect \
     OAUTH2_LOGIN_STYLE=redirect \
     OAUTH2_CLIENT_ID="" \
     OAUTH2_CLIENT_ID="" \
     OAUTH2_SECRET="" \
     OAUTH2_SECRET="" \
@@ -111,8 +121,24 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     CORS_ALLOW_HEADERS="" \
     CORS_ALLOW_HEADERS="" \
     CORS_EXPOSE_HEADERS="" \
     CORS_EXPOSE_HEADERS="" \
     DEFAULT_AUTHENTICATION_METHOD="" \
     DEFAULT_AUTHENTICATION_METHOD="" \
-    SCROLLINERTIA="0" \
-    SCROLLAMOUNT="auto"
+    PASSWORD_LOGIN_ENABLED=true \
+    CAS_ENABLED=false \
+    CAS_BASE_URL="" \
+    CAS_LOGIN_URL="" \
+    CAS_VALIDATE_URL="" \
+    SAML_ENABLED=false \
+    SAML_PROVIDER="" \
+    SAML_ENTRYPOINT="" \
+    SAML_ISSUER="" \
+    SAML_CERT="" \
+    SAML_IDPSLO_REDIRECTURL="" \
+    SAML_PRIVATE_KEYFILE="" \
+    SAML_PUBLIC_CERTFILE="" \
+    SAML_IDENTIFIER_FORMAT="" \
+    SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE="" \
+    SAML_ATTRIBUTES="" \
+    ORACLE_OIM_ENABLED=false \
+    WAIT_SPINNER=""
 
 
 # Copy the app to the image
 # Copy the app to the image
 COPY ${SRC_PATH} /home/wekan/app
 COPY ${SRC_PATH} /home/wekan/app
@@ -250,11 +276,12 @@ RUN \
     mkdir -p /home/wekan/.npm && \
     mkdir -p /home/wekan/.npm && \
     chown wekan --recursive /home/wekan/.npm /home/wekan/.config /home/wekan/.meteor && \
     chown wekan --recursive /home/wekan/.npm /home/wekan/.config /home/wekan/.meteor && \
     #gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
     #gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
+    chmod u+w *.json && \
     gosu wekan:wekan npm install && \
     gosu wekan:wekan npm install && \
     gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
     gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
-    cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
+    #cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
     #rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
     #rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
-    chown wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
+    #chown wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
     #Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
     #Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
     #https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
     #https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
     #https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
     #https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
@@ -267,8 +294,11 @@ RUN \
     #find . -name "*phantomjs*" | xargs rm -rf && \
     #find . -name "*phantomjs*" | xargs rm -rf && \
     #
     #
     cd /home/wekan/app_build/bundle/programs/server/ && \
     cd /home/wekan/app_build/bundle/programs/server/ && \
+    chmod u+w *.json && \
     gosu wekan:wekan npm install && \
     gosu wekan:wekan npm install && \
     #gosu wekan:wekan npm install bcrypt && \
     #gosu wekan:wekan npm install bcrypt && \
+    # Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
+    rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy && \
     mv /home/wekan/app_build/bundle /build && \
     mv /home/wekan/app_build/bundle /build && \
     \
     \
     # Put back the original tar
     # Put back the original tar

+ 77 - 0
Dockerfile.arm64v8

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

+ 15 - 10
README.md

@@ -1,3 +1,5 @@
+[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/wekan/wekan)
+
 # Wekan - Open Source kanban
 # Wekan - Open Source kanban
 
 
 [![Contributors](https://img.shields.io/github/contributors/wekan/wekan.svg "Contributors")](https://github.com/wekan/wekan/graphs/contributors)
 [![Contributors](https://img.shields.io/github/contributors/wekan/wekan.svg "Contributors")](https://github.com/wekan/wekan/graphs/contributors)
@@ -10,6 +12,7 @@
 [![Project Dependencies](https://david-dm.org/wekan/wekan.svg "Project Dependencies")](https://david-dm.org/wekan/wekan)
 [![Project Dependencies](https://david-dm.org/wekan/wekan.svg "Project Dependencies")](https://david-dm.org/wekan/wekan)
 [![Code analysis at Open Hub](https://img.shields.io/badge/code%20analysis-at%20Open%20Hub-brightgreen.svg "Code analysis at Open Hub")](https://www.openhub.net/p/wekan)
 [![Code analysis at Open Hub](https://img.shields.io/badge/code%20analysis-at%20Open%20Hub-brightgreen.svg "Code analysis at Open Hub")](https://www.openhub.net/p/wekan)
 [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fwekan%2Fwekan.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_shield)
 [![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)
 
 
@@ -18,21 +21,25 @@ New English strings of new features can be added as PRs to edge branch file weka
 
 
 ## [Wekan feature requests and bugs](https://github.com/wekan/wekan/issues)
 ## [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.
 It's better than at chat where details get lost when chat scrolls up.
 
 
 ## Chat
 ## Chat
 
 
-[![Wekan Chat][vanila_badge]][wekan_chat] - Most Wekan community and developers are here. Works on webbrowser
-and PWA app that can be added as icon on Android and bookmark on iOS, used like native app.
+[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)
 [Wekan IRC FAQ](https://github.com/wekan/wekan/wiki/IRC-FAQ)
 
 
+## Docker: Please only use Docker release tags
+
+Note: With Docker, please don't use latest tag. Only use release tags.
+See https://github.com/wekan/wekan/issues/3874
+
 ## FAQ
 ## FAQ
 
 
-**NOTE**: 
+**NOTE**:
 - Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
 - Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
-- Please don't feed the trolls and spammers that are mentioned in the FAQ :)
+- Please don't feed the [trolls](https://github.com/wekan/wekan/wiki/FAQ#why-am-i-called-a-troll) and [spammers](https://github.com/wekan/wekan/wiki/FAQ#why-am-i-called-a-spammer) that are mentioned in the FAQ :)
 
 
 ## About Wekan
 ## About Wekan
 
 
@@ -50,7 +57,7 @@ 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 largest user has 13k users using Wekan in their company.
-- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 50 languages.
+- Wekan has been [translated](https://transifex.com/wekan/wekan) to about 63 languages.
 - [Features][features]: Wekan has real-time user interface.
 - [Features][features]: Wekan has real-time user interface.
 - [Platforms][platforms]: Wekan supports many platforms.
 - [Platforms][platforms]: Wekan supports many platforms.
   Wekan is critical part of new platforms Wekan is currently being integrated to.
   Wekan is critical part of new platforms Wekan is currently being integrated to.
@@ -62,7 +69,7 @@ that by providing one-click installation on various platforms.
   [More Platforms](https://github.com/wekan/wekan/wiki/Platforms), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
   [More Platforms](https://github.com/wekan/wekan/wiki/Platforms), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
 - 1 GB RAM minimum free for Wekan. Production server should have minimum total 4 GB RAM.
 - 1 GB RAM minimum free for Wekan. Production server should have minimum total 4 GB RAM.
   For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/master/docker-compose.yml): 3 frontend servers,
   For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/master/docker-compose.yml): 3 frontend servers,
-  each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.  
+  each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
 - Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
 - Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
 - SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
 - SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
   Old versions have security issues because of old versions Node.js etc. Only newest Wekan is supported.
   Old versions have security issues because of old versions Node.js etc. Only newest Wekan is supported.
@@ -112,8 +119,6 @@ with [Meteor](https://www.meteor.com).
 [translate_wekan]: https://www.transifex.com/wekan/wekan/
 [translate_wekan]: https://www.transifex.com/wekan/wekan/
 [open_source]: https://en.wikipedia.org/wiki/Open-source_software
 [open_source]: https://en.wikipedia.org/wiki/Open-source_software
 [free_software]: https://en.wikipedia.org/wiki/Free_software
 [free_software]: https://en.wikipedia.org/wiki/Free_software
-[vanila_badge]: https://vanila.io/img/join-chat-button2.png
-[wekan_chat]: https://community.vanila.io/wekan
-
+[discussions]: https://github.com/wekan/wekan/discussions
 
 
 [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fwekan%2Fwekan.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_large)
 [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fwekan%2Fwekan.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_large)

+ 3 - 3
SECURITY.md

@@ -1,10 +1,10 @@
 Security is very important to us. If you discover any issue regarding security, please disclose
 Security is very important to us. If you discover any issue regarding security, please disclose
-the information responsibly by sending an email to security (at) wekan.team and not by
+the information responsibly by sending an email to support (at) wekan.team using
+[this PGP public key](support-at-wekan.team_pgp-publickey.asc) and not by
 creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
 creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
 
 
 We thank you with a place at our hall of fame page, that is
 We thank you with a place at our hall of fame page, that is
-at https://wekan.github.io/hall-of-fame . Others have just posted public GitHub issue,
-so they are not at that hall-of-fame page.
+at https://wekan.github.io/hall-of-fame
 
 
 ## How should reports be formatted?
 ## How should reports be formatted?
 
 

+ 1 - 1
Stackerfile.yml

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

+ 291 - 0
api.py

@@ -0,0 +1,291 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vi:ts=4:et
+
+# Wekan API Python CLI, originally from here, where is more details:
+# https://github.com/wekan/wekan/wiki/New-card-with-Python3-and-REST-API
+
+try:
+    # python 3
+    from urllib.parse import urlencode
+except ImportError:
+    # python 2
+    from urllib import urlencode
+
+import json
+import requests
+import sys
+
+arguments = len(sys.argv) - 1
+
+if arguments == 0:
+    print("=== Wekan API Python CLI: Shows IDs for addcard ===")
+    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 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")
+    print("  python3 api.py listattachments BOARDID # List attachments")
+# TODO:
+#   print("  python3 api.py attachmentjson BOARDID ATTACHMENTID # One attachment as JSON base64")
+#   print("  python3 api.py attachmentbinary BOARDID ATTACHMENTID # One attachment as binary file")
+#   print("  python3 api.py attachmentdownload BOARDID ATTACHMENTID # One attachment as file")
+#   print("  python3 api.py attachmentsdownload BOARDID # All attachments as files")
+    exit
+
+# ------- SETTINGS START -------------
+
+# Username is your Wekan username or email address.
+# OIDC/OAuth2 etc uses email address as username.
+
+username = 'testtest'
+
+password = 'testtest'
+
+wekanurl = 'http://localhost:4000/'
+
+# ------- SETTINGS END -------------
+
+"""
+EXAMPLE:
+
+python3 api.py
+
+OR:
+chmod +x api.py
+./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 createlist BOARDID LISTTITLE # Create list
+  python3 api.py addcard AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION
+  python3 api.py editcard BOARDID LISTID CARDID NEWCARDTITLE NEWCARDDESCRIPTION
+  python3 api.py listattachments BOARDID # List attachments
+  python3 api.py attachmentjson BOARDID ATTACHMENTID # One attachment as JSON base64
+  python3 api.py attachmentbinary BOARDID ATTACHMENTID # One attachment as binary file
+
+=== USERS ===
+
+python3 api.py users
+
+=> abcd1234
+
+=== BOARDS ===
+
+python3 api.py boards abcd1234
+
+
+=== SWIMLANES ===
+
+python3 api.py swimlanes dYZ
+
+[{"_id":"Jiv","title":"Default"}
+]
+
+=== LISTS ===
+
+python3 api.py lists dYZ
+
+[]
+
+There is no lists, so create a list:
+
+=== CREATE LIST ===
+
+python3 api.py createlist dYZ 'Test'
+
+{"_id":"7Kp"}
+
+#  python3 api.py addcard AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION
+
+python3 api.py addcard ppg dYZ Jiv 7Kp 'Test card' 'Test description'
+
+=== LIST ATTACHMENTS WITH DOWNLOAD URLs ====
+
+python3 api.py listattachments BOARDID
+
+"""
+
+# ------- API URL GENERATION START -----------
+
+loginurl = 'users/login'
+wekanloginurl = wekanurl + loginurl
+apiboards = 'api/boards/'
+apiattachments = 'api/attachments/'
+apiusers = 'api/users'
+e = 'export'
+s = '/'
+l = 'lists'
+sw = 'swimlane'
+sws = 'swimlanes'
+cs = 'cards'
+bs = 'boards'
+atl = 'attachmentslist'
+at = 'attachment'
+ats = 'attachments'
+users = wekanurl + apiusers
+
+# ------- API URL GENERATION END -----------
+
+# ------- LOGIN TOKEN START -----------
+
+data = {"username": username, "password": password}
+body = requests.post(wekanloginurl, data=data)
+d = body.json()
+apikey = d['token']
+
+# ------- LOGIN TOKEN END -----------
+
+if arguments == 7:
+
+    if sys.argv[1] == 'addcard':
+        # ------- WRITE TO CARD START -----------
+        authorid = sys.argv[2]
+        boardid = sys.argv[3]
+        swimlaneid = sys.argv[4]
+        listid = sys.argv[5]
+        cardtitle = sys.argv[6]
+        carddescription = sys.argv[7]
+        cardtolist = wekanurl + apiboards + boardid + s + l + s + listid + s + cs
+        # Write to 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 -----------
+
+if arguments == 6:
+
+    if sys.argv[1] == 'editcard':
+
+        # ------- LIST OF BOARD START -----------
+        boardid = sys.argv[2]
+        listid = sys.argv[3]
+        cardid = sys.argv[4]
+        newcardtitle = sys.argv[5]
+        newcarddescription = sys.argv[6]
+        edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
+        print(edcard)
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        put_data = {'title': '{}'.format(newcardtitle), 'description': '{}'.format(newcarddescription)}
+        body = requests.put(edcard, data=put_data, headers=headers)
+        print("=== EDIT CARD ===\n")
+        body = requests.get(edcard, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- LISTS OF BOARD END -----------
+
+if arguments == 3:
+
+    if sys.argv[1] == 'createlist':
+
+        # ------- CREATE LIST START -----------
+        boardid = sys.argv[2]
+        listtitle = sys.argv[3]
+        list = wekanurl + apiboards + boardid + s + l
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        post_data = {'title': '{}'.format(listtitle)}
+        body = requests.post(list, data=post_data, headers=headers)
+        print("=== CREATE LIST ===\n")
+        print(body.text)
+        # ------- CREATE LIST END -----------
+
+    if sys.argv[1] == 'list':
+
+        # ------- LIST OF BOARD START -----------
+        boardid = sys.argv[2]
+        listid = sys.argv[3]
+        listone = wekanurl + apiboards + boardid + s + l + s + listid
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        print("=== INFO OF ONE LIST ===\n")
+        body = requests.get(listone, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- LISTS OF BOARD END -----------
+
+if arguments == 2:
+
+    # ------- BOARDS LIST START -----------
+    userid = sys.argv[2]
+    boards = users + s + userid + s + bs
+    if sys.argv[1] == 'boards':
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        #post_data = {'userId': '{}'.format(userid)}
+        body = requests.get(boards, headers=headers)
+        print("=== BOARDS ===\n")
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+    # ------- BOARDS LIST END -----------
+    if sys.argv[1] == 'board':
+
+        # ------- BOARD INFO START -----------
+        boardid = sys.argv[2]
+        board = wekanurl + apiboards + boardid
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        body = requests.get(board, headers=headers)
+        print("=== BOARD ===\n")
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- BOARD INFO END -----------
+
+    if sys.argv[1] == 'swimlanes':
+        boardid = sys.argv[2]
+        swimlanes = wekanurl + apiboards + boardid + s + sws
+        # ------- SWIMLANES OF BOARD START -----------
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        print("=== SWIMLANES ===\n")
+        body = requests.get(swimlanes, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- SWIMLANES OF BOARD END -----------
+
+    if sys.argv[1] == 'lists':
+
+        # ------- LISTS OF BOARD START -----------
+        boardid = sys.argv[2]
+        lists = wekanurl + apiboards + boardid + s + l
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        print("=== LISTS ===\n")
+        body = requests.get(lists, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- LISTS OF BOARD END -----------
+
+    if sys.argv[1] == 'listattachments':
+
+        # ------- LISTS OF ATTACHMENTS START -----------
+        boardid = sys.argv[2]
+        listattachments = wekanurl + apiboards + boardid + s + ats
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        print("=== LIST OF ATTACHMENTS ===\n")
+        body = requests.get(listattachments, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- LISTS OF ATTACHMENTS END -----------
+
+if arguments == 1:
+
+    if sys.argv[1] == 'users':
+
+        # ------- LIST OF USERS START -----------
+        headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
+        print(users)
+        print("=== USERS ===\n")
+        body = requests.get(users, headers=headers)
+        data2 = body.text.replace('}',"}\n")
+        print(data2)
+        # ------- LIST OF USERS END -----------

+ 6 - 0
client/00-startup.js

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

+ 70 - 37
client/components/activities/activities.jade

@@ -15,11 +15,18 @@ template(name="cardActivities")
   each activityData in currentCard.activities
   each activityData in currentCard.activities
     +activity(activity=activityData card=card mode=mode)
     +activity(activity=activityData card=card mode=mode)
 
 
+template(name="editOrDeleteComment")
+  = ' - '
+  a.js-open-inlined-form {{_ "edit"}}
+  = ' - '
+  a.js-delete-comment {{_ "delete"}}
+
 template(name="activity")
 template(name="activity")
   .activity
   .activity
     +userAvatar(userId=activity.user._id)
     +userAvatar(userId=activity.user._id)
     p.activity-desc
     p.activity-desc
-      +memberName(user=activity.user)
+      span.activity-member
+        +memberName(user=activity.user)
 
 
       //- attachment activity -------------------------------------------------
       //- attachment activity -------------------------------------------------
       if($eq activity.activityType 'deleteAttachment')
       if($eq activity.activityType 'deleteAttachment')
@@ -34,38 +41,38 @@ template(name="activity")
       //- board activity ------------------------------------------------------
       //- board activity ------------------------------------------------------
       if($eq mode 'board')
       if($eq mode 'board')
         if($eq activity.activityType 'createBoard')
         if($eq activity.activityType 'createBoard')
-          | {{_ 'activity-created' boardLabel}}.
+          | {{{_ 'activity-created' boardLabelLink}}}.
 
 
         if($eq activity.activityType 'importBoard')
         if($eq activity.activityType 'importBoard')
-          | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
+          | {{{_ 'activity-imported-board' boardLabelLink sourceLink}}}.
 
 
         if($eq activity.activityType 'addBoardMember')
         if($eq activity.activityType 'addBoardMember')
-          | {{{_ 'activity-added' memberLink boardLabel}}}.
+          | {{{_ 'activity-added' memberLink boardLabelLink}}}.
 
 
         if($eq activity.activityType 'removeBoardMember')
         if($eq activity.activityType 'removeBoardMember')
-          | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+          | {{{_ 'activity-excluded' memberLink boardLabelLink}}}.
 
 
       //- card activity -------------------------------------------------------
       //- card activity -------------------------------------------------------
       if($eq activity.activityType 'createCard')
       if($eq activity.activityType 'createCard')
         if($eq mode 'card')
         if($eq mode 'card')
-          | {{{_ 'activity-added' cardLabel activity.listName}}}.
+          | {{{_ 'activity-added' cardLabelLink (sanitize activity.listName)}}}.
         else
         else
-          | {{{_ 'activity-added' cardLabel boardLabel}}}.
+          | {{{_ 'activity-added' cardLabelLink boardLabelLink}}}.
 
 
       if($eq activity.activityType 'importCard')
       if($eq activity.activityType 'importCard')
-        | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
+        | {{{_ 'activity-imported' cardLink boardLabelLink sourceLink}}}.
 
 
       if($eq activity.activityType 'moveCard')
       if($eq activity.activityType 'moveCard')
-        | {{{_ 'activity-moved' cardLabel activity.oldList.title activity.list.title}}}.
+        | {{{_ 'activity-moved' cardLabelLink (sanitize activity.oldList.title) (sanitize activity.list.title)}}}.
 
 
       if($eq activity.activityType 'moveCardBoard')
       if($eq activity.activityType 'moveCardBoard')
-        | {{{_ 'activity-moved' cardLink activity.oldBoardName activity.boardName}}}.
+        | {{{_ 'activity-moved' cardLink (sanitize activity.oldBoardName) (sanitize activity.boardName)}}}.
 
 
       if($eq activity.activityType 'archivedCard')
       if($eq activity.activityType 'archivedCard')
         | {{{_ 'activity-archived' cardLink}}}.
         | {{{_ 'activity-archived' cardLink}}}.
 
 
       if($eq activity.activityType 'restoredCard')
       if($eq activity.activityType 'restoredCard')
-        | {{{_ 'activity-sent' cardLink boardLabel}}}.
+        | {{{_ 'activity-sent' cardLink boardLabelLink}}}.
 
 
       //- checklist activity --------------------------------------------------
       //- checklist activity --------------------------------------------------
       if($eq activity.activityType 'addChecklist')
       if($eq activity.activityType 'addChecklist')
@@ -75,7 +82,7 @@ template(name="activity")
             +viewer
             +viewer
               = activity.checklist.title
               = activity.checklist.title
         else
         else
-          a.activity-checklist(href="{{ activity.card.absoluteUrl }}")
+          a.activity-checklist(href="{{ activity.card.originRelativeUrl }}")
             +viewer
             +viewer
               = activity.checklist.title
               = activity.checklist.title
 
 
@@ -83,25 +90,25 @@ template(name="activity")
         | {{{_ 'activity-checklist-removed' cardLink}}}.
         | {{{_ 'activity-checklist-removed' cardLink}}}.
 
 
       if($eq activity.activityType 'completeChecklist')
       if($eq activity.activityType 'completeChecklist')
-        | {{{_ 'activity-checklist-completed' activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-checklist-completed' (sanitize activity.checklist.title) cardLink}}}.
 
 
       if($eq activity.activityType 'uncompleteChecklist')
       if($eq activity.activityType 'uncompleteChecklist')
-        | {{{_ 'activity-checklist-uncompleted' activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-checklist-uncompleted' (sanitize activity.checklist.title) cardLink}}}.
 
 
       if($eq activity.activityType 'checkedItem')
       if($eq activity.activityType 'checkedItem')
-        | {{{_ 'activity-checked-item' checkItem activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-checked-item' (sanitize checkItem) (sanitize activity.checklist.title) cardLink}}}.
 
 
       if($eq activity.activityType 'uncheckedItem')
       if($eq activity.activityType 'uncheckedItem')
-        | {{{_ 'activity-unchecked-item' checkItem activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-unchecked-item' (sanitize checkItem) (sanitize activity.checklist.title) cardLink}}}.
 
 
       if($eq activity.activityType 'addChecklistItem')
       if($eq activity.activityType 'addChecklistItem')
-        | {{{_ 'activity-checklist-item-added' activity.checklist.title cardLink}}}.
-        .activity-checklist(href="{{ activity.card.absoluteUrl }}")
+        | {{{_ 'activity-checklist-item-added' (sanitize activity.checklist.title) cardLink}}}.
+        .activity-checklist(href="{{ activity.card.originRelativeUrl }}")
           +viewer
           +viewer
             = activity.checklistItem.title
             = activity.checklistItem.title
 
 
       if($eq activity.activityType 'removedChecklistItem')
       if($eq activity.activityType 'removedChecklistItem')
-        | {{{_ 'activity-checklist-item-removed' activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-checklist-item-removed' (sanitize activity.checklist.title) cardLink}}}.
 
 
       //- comment activity ----------------------------------------------------
       //- comment activity ----------------------------------------------------
       if($eq mode 'card')
       if($eq mode 'card')
@@ -118,11 +125,10 @@ template(name="activity")
               +viewer
               +viewer
                 = activity.comment.text
                 = activity.comment.text
             span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
             span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
-              if ($eq currentUser._id activity.comment.userId)
-                = ' - '
-                a.js-open-inlined-form {{_ "edit"}}
-                = ' - '
-                a.js-delete-comment {{_ "delete"}}
+              if($eq currentUser._id activity.comment.userId)
+                +editOrDeleteComment
+              else if currentUser.isBoardAdmin
+                +editOrDeleteComment
 
 
         if($eq activity.activityType 'deleteComment')
         if($eq activity.activityType 'deleteComment')
           | {{{_ 'activity-deleteComment' currentData.commentId}}}.
           | {{{_ 'activity-deleteComment' currentData.commentId}}}.
@@ -133,41 +139,68 @@ template(name="activity")
         //- if we are not in card mode we only display a summary of the comment
         //- if we are not in card mode we only display a summary of the comment
         if($eq activity.activityType 'addComment')
         if($eq activity.activityType 'addComment')
           | {{{_ 'activity-on' cardLink}}}
           | {{{_ 'activity-on' cardLink}}}
-          a.activity-comment(href="{{ activity.card.absoluteUrl }}")
+          a.activity-comment(href="{{ activity.card.originRelativeUrl }}")
             +viewer
             +viewer
               = activity.comment.text
               = activity.comment.text
 
 
+      //- date activity ------------------------------------------------
+      if($eq mode 'card')
+        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}}}.
+
+        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}}}.
+
+        if($eq activity.activityType 'a-endAt')
+          | {{{_ 'activity-endDate' (sanitize endDate) cardLink}}}.
+
       //- customField activity ------------------------------------------------
       //- customField activity ------------------------------------------------
       if($eq mode 'board')
       if($eq mode 'board')
         if($eq activity.activityType 'createCustomField')
         if($eq activity.activityType 'createCustomField')
           | {{_ 'activity-customfield-created' customField}}.
           | {{_ 'activity-customfield-created' customField}}.
 
 
         if($eq activity.activityType 'setCustomField')
         if($eq activity.activityType 'setCustomField')
-          | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
+          | {{{_ 'activity-set-customfield' (sanitize lastCustomField) (sanitize lastCustomFieldValue) cardLink}}}.
 
 
         if($eq activity.activityType 'unsetCustomField')
         if($eq activity.activityType 'unsetCustomField')
-          | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
+          | {{{_ 'activity-unset-customfield' (sanitize lastCustomField) cardLink}}}.
 
 
       //- label activity ------------------------------------------------------
       //- label activity ------------------------------------------------------
       if($eq activity.activityType 'addedLabel')
       if($eq activity.activityType 'addedLabel')
-        | {{{_ 'activity-added-label' lastLabel cardLink}}}.
+        | {{{_ 'activity-added-label' (sanitize lastLabel) cardLink}}}.
 
 
       if($eq activity.activityType 'removedLabel')
       if($eq activity.activityType 'removedLabel')
-        | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
+        | {{{_ 'activity-removed-label' (sanitize lastLabel) cardLink}}}.
 
 
       //- list activity -------------------------------------------------------
       //- list activity -------------------------------------------------------
       if($neq mode 'card')
       if($neq mode 'card')
         if($eq activity.activityType 'createList')
         if($eq activity.activityType 'createList')
-          | {{{_ 'activity-added' listLabel boardLabel}}}.
+          | {{{_ 'activity-added' (sanitize listLabel) boardLabelLink}}}.
 
 
         if($eq activity.activityType 'importList')
         if($eq activity.activityType 'importList')
-          | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
+          | {{{_ 'activity-imported' (sanitize listLabel) boardLabelLink sourceLink}}}.
 
 
         if($eq activity.activityType 'removeList')
         if($eq activity.activityType 'removeList')
-          | {{{_ 'activity-removed' activity.title boardLabel}}}.
+          | {{{_ 'activity-removed' (sanitize activity.title) boardLabelLink}}}.
 
 
         if($eq activity.activityType 'archivedList')
         if($eq activity.activityType 'archivedList')
-          | {{_ 'activity-archived' listLabel}}.
+          | {{_ 'activity-archived' (sanitize listLabel)}}.
 
 
       //- member activity ----------------------------------------------------
       //- member activity ----------------------------------------------------
       if($eq activity.activityType 'joinMember')
       if($eq activity.activityType 'joinMember')
@@ -185,15 +218,15 @@ template(name="activity")
       //- swimlane activity --------------------------------------------------
       //- swimlane activity --------------------------------------------------
       if($neq mode 'card')
       if($neq mode 'card')
         if($eq activity.activityType 'createSwimlane')
         if($eq activity.activityType 'createSwimlane')
-          | {{{_ 'activity-added' activity.swimlane.title boardLabel}}}.
+          | {{_ 'activity-added' (sanitize activity.swimlane.title) boardLabelLink}}.
 
 
         if($eq activity.activityType 'archivedSwimlane')
         if($eq activity.activityType 'archivedSwimlane')
-          | {{_ 'activity-archived' activity.swimlane.title}}.
+          | {{_ 'activity-archived' (sanitize activity.swimlane.title)}}.
 
 
 
 
       //- I don't understand this part ----------------------------------------
       //- I don't understand this part ----------------------------------------
       if(currentData.timeKey)
       if(currentData.timeKey)
-        | {{{_ activity.activityType }}}
+        | {{_ activity.activityType }}
         = ' '
         = ' '
         i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
         i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
         if (currentData.timeOldValue)
         if (currentData.timeOldValue)
@@ -203,6 +236,6 @@ template(name="activity")
             i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
             i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
         = ' @'
         = ' @'
       else if(currentData.timeValue)
       else if(currentData.timeValue)
-        | {{{_ activity.activityType currentData.timeValue}}}
+        | {{_ activity.activityType currentData.timeValue}}
 
 
       span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
       span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}

+ 58 - 19
client/components/activities/activities.js

@@ -1,12 +1,15 @@
-const activitiesPerPage = 20;
+import DOMPurify from 'dompurify';
+
+const activitiesPerPage = 500;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     // XXX Should we use ReactiveNumber?
     // XXX Should we use ReactiveNumber?
     this.page = new ReactiveVar(1);
     this.page = new ReactiveVar(1);
     this.loadNextPageLocked = false;
     this.loadNextPageLocked = false;
-    const sidebar = this.parentComponent(); // XXX for some reason not working
-    sidebar.callFirstWith(null, 'resetNextPeak');
+    // TODO is sidebar always available? E.g. on small screens/mobile devices
+    const sidebar = Sidebar;
+    sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
     this.autorun(() => {
     this.autorun(() => {
       let mode = this.data().mode;
       let mode = this.data().mode;
       const capitalizedMode = Utils.capitalize(mode);
       const capitalizedMode = Utils.capitalize(mode);
@@ -27,6 +30,8 @@ BlazeComponent.extendComponent({
       this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
       this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
         this.loadNextPageLocked = false;
         this.loadNextPageLocked = false;
 
 
+        // TODO the guard can be removed as soon as the TODO above is resolved
+        if (!sidebar) return;
         // If the sibear peak hasn't increased, that mean that there are no more
         // If the sibear peak hasn't increased, that mean that there are no more
         // activities, and we can stop calling new subscriptions.
         // activities, and we can stop calling new subscriptions.
         // XXX This is hacky! We need to know excatly and reactively how many
         // XXX This is hacky! We need to know excatly and reactively how many
@@ -41,23 +46,22 @@ BlazeComponent.extendComponent({
       });
       });
     });
     });
   },
   },
-}).register('activities');
-
-BlazeComponent.extendComponent({
   loadNextPage() {
   loadNextPage() {
     if (this.loadNextPageLocked === false) {
     if (this.loadNextPageLocked === false) {
       this.page.set(this.page.get() + 1);
       this.page.set(this.page.get() + 1);
       this.loadNextPageLocked = true;
       this.loadNextPageLocked = true;
     }
     }
   },
   },
+}).register('activities');
 
 
+BlazeComponent.extendComponent({
   checkItem() {
   checkItem() {
     const checkItemId = this.currentData().activity.checklistItemId;
     const checkItemId = this.currentData().activity.checklistItemId;
     const checkItem = ChecklistItems.findOne({ _id: checkItemId });
     const checkItem = ChecklistItems.findOne({ _id: checkItemId });
     return checkItem && checkItem.title;
     return checkItem && checkItem.title;
   },
   },
 
 
-  boardLabel() {
+  boardLabelLink() {
     const data = this.currentData();
     const data = this.currentData();
     if (data.mode !== 'board') {
     if (data.mode !== 'board') {
       return createBoardLink(data.activity.board(), data.activity.listName);
       return createBoardLink(data.activity.board(), data.activity.listName);
@@ -65,10 +69,10 @@ BlazeComponent.extendComponent({
     return TAPi18n.__('this-board');
     return TAPi18n.__('this-board');
   },
   },
 
 
-  cardLabel() {
+  cardLabelLink() {
     const data = this.currentData();
     const data = this.currentData();
     if (data.mode !== 'card') {
     if (data.mode !== 'card') {
-      return createCardLink(this.currentData().activity.card());
+      return createCardLink(data.activity.card());
     }
     }
     return TAPi18n.__('this-card');
     return TAPi18n.__('this-card');
   },
   },
@@ -77,6 +81,30 @@ BlazeComponent.extendComponent({
     return createCardLink(this.currentData().activity.card());
     return createCardLink(this.currentData().activity.card());
   },
   },
 
 
+  receivedDate() {
+    const receivedDate = this.currentData().activity.card();
+    if (!receivedDate) return null;
+    return receivedDate.receivedAt;
+  },
+
+  startDate() {
+    const startDate = this.currentData().activity.card();
+    if (!startDate) return null;
+    return startDate.startAt;
+  },
+
+  dueDate() {
+    const dueDate = this.currentData().activity.card();
+    if (!dueDate) return null;
+    return dueDate.dueAt;
+  },
+
+  endDate() {
+    const endDate = this.currentData().activity.card();
+    if (!endDate) return null;
+    return endDate.endAt;
+  },
+
   lastLabel() {
   lastLabel() {
     const lastLabelId = this.currentData().activity.labelId;
     const lastLabelId = this.currentData().activity.labelId;
     if (!lastLabelId) return null;
     if (!lastLabelId) return null;
@@ -134,11 +162,15 @@ BlazeComponent.extendComponent({
             {
             {
               href: source.url,
               href: source.url,
             },
             },
-            source.system,
+            DOMPurify.sanitize(source.system, {
+              ALLOW_UNKNOWN_PROTOCOLS: true,
+            }),
           ),
           ),
         );
         );
       } else {
       } else {
-        return source.system;
+        return DOMPurify.sanitize(source.system, {
+          ALLOW_UNKNOWN_PROTOCOLS: true,
+        });
       }
       }
     }
     }
     return null;
     return null;
@@ -162,10 +194,10 @@ BlazeComponent.extendComponent({
               href: attachment.url({ download: true }),
               href: attachment.url({ download: true }),
               target: '_blank',
               target: '_blank',
             },
             },
-            attachment.name(),
+            DOMPurify.sanitize(attachment.name()),
           ),
           ),
         )) ||
         )) ||
-      this.currentData().activity.attachmentName
+      DOMPurify.sanitize(this.currentData().activity.attachmentName)
     );
     );
   },
   },
 
 
@@ -180,7 +212,7 @@ BlazeComponent.extendComponent({
       {
       {
         // XXX We should use Popup.afterConfirmation here
         // XXX We should use Popup.afterConfirmation here
         'click .js-delete-comment'() {
         'click .js-delete-comment'() {
-          const commentId = this.currentData().commentId;
+          const commentId = this.currentData().activity.commentId;
           CardComments.remove(commentId);
           CardComments.remove(commentId);
         },
         },
         'submit .js-edit-comment'(evt) {
         'submit .js-edit-comment'(evt) {
@@ -188,7 +220,7 @@ BlazeComponent.extendComponent({
           const commentText = this.currentComponent()
           const commentText = this.currentComponent()
             .getValue()
             .getValue()
             .trim();
             .trim();
-          const commentId = Template.parentData().commentId;
+          const commentId = Template.parentData().activity.commentId;
           if (commentText) {
           if (commentText) {
             CardComments.update(commentId, {
             CardComments.update(commentId, {
               $set: {
               $set: {
@@ -202,16 +234,23 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('activity');
 }).register('activity');
 
 
+Template.activity.helpers({
+  sanitize(value) {
+    return DOMPurify.sanitize(value, { ALLOW_UNKNOWN_PROTOCOLS: true });
+  },
+});
+
 function createCardLink(card) {
 function createCardLink(card) {
+  if (!card) return '';
   return (
   return (
     card &&
     card &&
     Blaze.toHTML(
     Blaze.toHTML(
       HTML.A(
       HTML.A(
         {
         {
-          href: card.absoluteUrl(),
+          href: card.originRelativeUrl(),
           class: 'action-card',
           class: 'action-card',
         },
         },
-        card.title,
+        DOMPurify.sanitize(card.title, { ALLOW_UNKNOWN_PROTOCOLS: true }),
       ),
       ),
     )
     )
   );
   );
@@ -225,10 +264,10 @@ function createBoardLink(board, list) {
     Blaze.toHTML(
     Blaze.toHTML(
       HTML.A(
       HTML.A(
         {
         {
-          href: board.absoluteUrl(),
+          href: board.originRelativeUrl(),
           class: 'action-board',
           class: 'action-board',
         },
         },
-        text,
+        DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
       ),
       ),
     )
     )
   );
   );

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

@@ -10,12 +10,16 @@
 
 
   .activity
   .activity
     margin: 0.5px 0
     margin: 0.5px 0
+    padding: 6px 0;
     display: flex
     display: flex
 
 
     .member
     .member
-      width: 24px
+      width: 32px
       height: @width
       height: @width
 
 
+    .activity-member
+      font-weight: 700      
+
     .activity-desc
     .activity-desc
       word-wrap: break-word
       word-wrap: break-word
       overflow: hidden
       overflow: hidden

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

@@ -1,7 +1,7 @@
 template(name="commentForm")
 template(name="commentForm")
   .new-comment.js-new-comment(
   .new-comment.js-new-comment(
     class="{{#if commentFormIsOpen}}is-open{{/if}}")
     class="{{#if commentFormIsOpen}}is-open{{/if}}")
-    +userAvatar(userId=currentUser._id)
+    +userAvatar(userId=currentUser._id noRemove=true)
     form.js-new-comment-form
     form.js-new-comment-form
       +editor(class="js-new-comment-input")
       +editor(class="js-new-comment-input")
         | {{getUnsavedValue 'cardComment' currentCard._id}}
         | {{getUnsavedValue 'cardComment' currentCard._id}}

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

@@ -3,6 +3,7 @@ const commentFormIsOpen = new ReactiveVar(false);
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onDestroyed() {
   onDestroyed() {
     commentFormIsOpen.set(false);
     commentFormIsOpen.set(false);
+    $('.note-popover').hide();
   },
   },
 
 
   commentFormIsOpen() {
   commentFormIsOpen() {

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

@@ -3,11 +3,15 @@ BlazeComponent.extendComponent({
     this.subscribe('archivedBoards');
     this.subscribe('archivedBoards');
   },
   },
 
 
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   archivedBoards() {
   archivedBoards() {
     return Boards.find(
     return Boards.find(
       { archived: true },
       { archived: true },
       {
       {
-        sort: ['title'],
+        sort: { archivedAt: -1, modifiedAt: -1 },
       },
       },
     );
     );
   },
   },

+ 1 - 1
client/components/boards/boardBody.jade

@@ -15,7 +15,7 @@ template(name="board")
 template(name="boardBody")
 template(name="boardBody")
   .board-wrapper(class=currentBoard.colorClass)
   .board-wrapper(class=currentBoard.colorClass)
     +sidebar
     +sidebar
-    .board-canvas.js-swimlanes.js-perfect-scrollbar(
+    .board-canvas.js-swimlanes(
       class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
       class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
       class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
       class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
       class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
       class="{{#if draggingActive.get}}is-dragging-active{{/if}}")

+ 18 - 17
client/components/boards/boardBody.js

@@ -1,7 +1,5 @@
-import { Cookies } from 'meteor/ostrio:cookies';
-const cookies = new Cookies();
 const subManager = new SubsManager();
 const subManager = new SubsManager();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
 const swimlaneWhileSortingHeight = 150;
 const swimlaneWhileSortingHeight = 150;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
@@ -191,21 +189,18 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.js-swimlane:not(.placeholder)');
-
     this.autorun(() => {
     this.autorun(() => {
       let showDesktopDragHandles = false;
       let showDesktopDragHandles = false;
       currentUser = Meteor.user();
       currentUser = Meteor.user();
       if (currentUser) {
       if (currentUser) {
         showDesktopDragHandles = (currentUser.profile || {})
         showDesktopDragHandles = (currentUser.profile || {})
           .showDesktopDragHandles;
           .showDesktopDragHandles;
-      } else if (cookies.has('showDesktopDragHandles')) {
+      } else if (window.localStorage.getItem('showDesktopDragHandles')) {
         showDesktopDragHandles = true;
         showDesktopDragHandles = true;
       } else {
       } else {
         showDesktopDragHandles = false;
         showDesktopDragHandles = false;
       }
       }
-      if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+      if (Utils.isMiniScreen() || showDesktopDragHandles) {
         $swimlanesDom.sortable({
         $swimlanesDom.sortable({
           handle: '.js-swimlane-header-handle',
           handle: '.js-swimlane-header-handle',
         });
         });
@@ -215,9 +210,13 @@ BlazeComponent.extendComponent({
         });
         });
       }
       }
 
 
-      // Disable drag-dropping if the current user is not a board member or is miniscreen
-      $swimlanesDom.sortable('option', 'disabled', !userIsMember());
-      $swimlanesDom.sortable('option', 'disabled', Utils.isMiniScreen());
+      // Disable drag-dropping if the current user is not a board member
+      //$swimlanesDom.sortable('option', 'disabled', !userIsMember());
+      $swimlanesDom.sortable(
+        'option',
+        'disabled',
+        !Meteor.user().isBoardAdmin(),
+      );
     });
     });
 
 
     function userIsMember() {
     function userIsMember() {
@@ -241,7 +240,9 @@ BlazeComponent.extendComponent({
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
       return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
     } else {
     } else {
-      return cookies.get('boardView') === 'board-view-swimlanes';
+      return (
+        window.localStorage.getItem('boardView') === 'board-view-swimlanes'
+      );
     }
     }
   },
   },
 
 
@@ -250,7 +251,7 @@ BlazeComponent.extendComponent({
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).boardView === 'board-view-lists';
       return (currentUser.profile || {}).boardView === 'board-view-lists';
     } else {
     } else {
-      return cookies.get('boardView') === 'board-view-lists';
+      return window.localStorage.getItem('boardView') === 'board-view-lists';
     }
     }
   },
   },
 
 
@@ -259,7 +260,7 @@ BlazeComponent.extendComponent({
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).boardView === 'board-view-cal';
       return (currentUser.profile || {}).boardView === 'board-view-cal';
     } else {
     } else {
-      return cookies.get('boardView') === 'board-view-cal';
+      return window.localStorage.getItem('boardView') === 'board-view-cal';
     }
     }
   },
   },
 
 
@@ -327,7 +328,7 @@ BlazeComponent.extendComponent({
       header: {
       header: {
         left: 'title   today prev,next',
         left: 'title   today prev,next',
         center:
         center:
-          'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,timelineMonth timelineYear',
+          'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,listMonth',
         right: '',
         right: '',
       },
       },
       // height: 'parent', nope, doesn't work as the parent might be small
       // height: 'parent', nope, doesn't work as the parent might be small
@@ -359,7 +360,7 @@ BlazeComponent.extendComponent({
             end: end || card.endAt,
             end: end || card.endAt,
             allDay:
             allDay:
               Math.abs(end.getTime() - start.getTime()) / 1000 === 24 * 3600,
               Math.abs(end.getTime() - start.getTime()) / 1000 === 24 * 3600,
-            url: FlowRouter.url('card', {
+            url: FlowRouter.path('card', {
               boardId: currentBoard._id,
               boardId: currentBoard._id,
               slug: currentBoard.slug,
               slug: currentBoard.slug,
               cardId: card._id,
               cardId: card._id,
@@ -421,7 +422,7 @@ BlazeComponent.extendComponent({
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).boardView === 'board-view-cal';
       return (currentUser.profile || {}).boardView === 'board-view-cal';
     } else {
     } else {
-      return cookies.get('boardView') === 'board-view-cal';
+      return window.localStorage.getItem('boardView') === 'board-view-cal';
     }
     }
   },
   },
 }).register('calendarView');
 }).register('calendarView');

+ 769 - 1
client/components/boards/boardColors.styl

@@ -15,7 +15,8 @@ setBoardColor(color)
   .is-selected .minicard
   .is-selected .minicard
     border-left: 3px solid color
     border-left: 3px solid color
 
 
-  button[type=submit].primary, input[type=submit].primary
+  button[type=submit].primary, input[type=submit].primary,
+  .sidebar .sidebar-content .sidebar-btn
     background-color: darken(color, 20%)
     background-color: darken(color, 20%)
 
 
   &.pop-over .pop-over-list li a:not(.disabled):hover,
   &.pop-over .pop-over-list li a:not(.disabled):hover,
@@ -293,3 +294,770 @@ setBoardColor(color)
 
 
   //.header-quick-access
   //.header-quick-access
   //  backgroud-color: #568ba2
   //  backgroud-color: #568ba2
+
+
+/*
+  Alternate "Clear" Styling
+*/
+setBoardClear(color1,color2)
+  //color1: The quick access color
+  //color2: The main bar color
+
+  &.sk-spinner div,
+  .board-backgrounds-list &.background-box,
+  .board-list & a
+    background: linear-gradient(180deg, color1 0%, color2 100%)
+    //background: linear-gradient(180deg, rgb(73, 155, 234) 0%, rgb(0, 174, 204) 100%)
+
+  .is-selected .minicard
+    border-left: 3px solid color1
+
+  &.pop-over .pop-over-list li a:not(.disabled):hover,
+  .sidebar .sidebar-content .sidebar-btn:hover,
+  .sidebar-list li a:hover
+    background-color: lighten(color1, 10%)
+
+  &#header ul li.current, &#header-quick-access ul li.current
+    border-bottom: 4px solid lighten(color2, 10%)
+
+  &#header-quick-access
+    background: darken(color1, 10%)
+    //background: rgba(66,137,204,1)
+    color: #FFF
+
+  &#header-quick-access #header-new-board-icon,
+  &#header-quick-access #header-user-bar,
+  &#header-quick-access ul li
+    color: rgba(255,255,255,0.5)
+
+  // The background-color value here is not seen,
+  // its covered by the background of #header-main-bar
+  // it's just to aid transitions between boards
+  &#header
+    background-color: color2
+    border-bottom: 1px solid darken(color2, 20%)
+    border-top: 1px solid darken(color2, 40%)
+
+  // Since the theme uses a gradient for the header
+  // and gradients break transitions, it has to be set here
+  &#header #header-main-bar
+    background: linear-gradient(180deg, color1 0%, color2 100%)
+
+  &#header #header-main-bar p
+    margin-bottom: 6px
+
+  &#header #header-main-bar .board-header-btn.emphasis
+    background: lighten(color2, 10%)
+
+    &:hover,
+    .board-header-btn-close
+      background: rgba(0,0,0,0.2)
+
+    &:hover .board-header-btn-close
+      background: rgba(0,0,0,0.2)
+
+  .materialCheckBox.is-checked
+    border-bottom: 2px solid color1
+    border-right: 2px solid color1
+
+  .is-multiselection-active .multi-selection-checkbox
+    &.is-checked + .minicard
+      background: lighten(color2, 90%)
+
+    &:not(.is-checked) + .minicard:hover:not(.minicard-composer)
+      background: lighten(color2, 97%)
+
+  .toggle-switch:checked ~ .toggle-label
+    background-color: lighten(color1, 20%)
+
+    &:after
+      background-color: darken(color1, 20%)
+
+  .board-canvas
+    background: linear-gradient(135deg, color1 0%, color2 100%)
+
+  .swimlane
+    background: none
+
+  .list:first-child
+    margin-left: 15px
+
+  .list
+    background: rgba(255,255,255,0.35)
+    margin: 10px
+    border: 0
+    border-radius: 14px
+
+  .list.list-composer
+    background: rgba(255,255,255,0.1)
+    height: min-content
+    flex: unset
+    width: 270px
+    padding-bottom: 16px
+
+  .list.list-composer .open-list-composer
+    border-radius: 7px
+    color: rgba(0,0,0,0.3)
+    padding: 7px 10px
+    display: block
+
+  .list.list-composer .open-list-composer:hover
+    box-shadow: 0 1px 2px rgba(0,0,0,.2)
+    background: rgba(255,255,255,0.7)
+    color: rgba(0,0,0,0.6)
+
+  .list-header
+    background-color: rgba(255,255,255,0.25)
+    border-radius: 14px 14px 0 0
+
+  .list-header:not([class*="list-header-"])
+    border-bottom: 6px solid rgba(255,255,255,0)
+
+  .list-header .list-header-name
+    color: rgba(0,0,0,0.6)
+
+  .list-body
+    padding: 11px
+
+  .minicard
+    border-radius: 7px
+    padding: 10px 10px 4px 10px
+    box-shadow: 2px 2px 4px 0px rgba(0,0,0,0.15)
+    color: #222
+
+  .card-details
+    border-radius: 0 0 14px 14px
+    box-shadow: 0 0 7px 0 rgba(0,0,0,0.5)
+    margin-left: -10px
+
+  .list-body .open-minicard-composer
+    border-radius: 7px
+    color: rgba(0,0,0,.3)
+    margin-bottom: 11px
+
+  .list-body .open-minicard-composer:hover
+    background: rgba(255,255,255,0.7)
+    color: rgba(0,0,0,0.6)
+
+  button[type=submit].primary, input[type=submit].primary
+    box-shadow: none
+    background-color: rgba(255,255,255,0.5)
+    color: rgba(0,0,0,0.55)
+    border-radius: 7px
+    border: 0
+
+  button[type="submit"].primary:hover, input[type="submit"].primary:hover
+    background-color: rgba(255,255,255,0.7)
+    color: rgba(0,0,0,0.8)
+    box-shadow: 0 1px 2px rgba(0,0,0,.2)
+
+  .quiet, .quiet a
+    color: rgba(0,0,0,0.4)
+
+  .list-header .list-header-watch-icon
+    color: rgba(0,0,0,0.5)
+    position: absolute
+    margin-top: -34px
+    margin-let: -11px
+
+  a.fa, a i.fa
+    color: rgba(0,0,0,0.3)
+
+  a:not(.disabled).is-active.fa, a:not(.disabled).is-active i.fa, a:not(.disabled):hover.fa, a:not(.disabled):hover i.fa
+    color: rgba(0,0,0,0.6)
+
+  input[type="email"], input[type="password"], input[type="text"]
+    border: 0
+    border-radius: 7px
+
+  .sidebar-shadow
+    box-shadow: none
+    border-left: 9px solid color2
+
+  .is-open .sidebar-shadow
+    box-shadow: -10px 0 8px rgba(0,0,0,0.3)
+
+  .list.ui-sortable-helper
+    transform:rotate(0deg)
+
+  .minicard-wrapper.placeholder
+    background: rgba(0,0,0,0.1)
+
+  .minicard-wrapper.ui-sortable-helper
+    transform:rotate(0deg)
+    opacity: 0.8
+
+  .list-body .open-minicard-composer
+    color: rgba(0,0,0,.3)
+
+  .swinlane.ui-sortable-helper
+    transform:rotate(0deg)
+
+  .swimlane .swimlane-header-wrap
+    background: linear-gradient(0deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.25) 100%)
+
+  .swimlane-header-wrap .inlined-form
+    width: 100%
+
+  .swimlane-header-wrap .list-composer
+    text-align: center
+    margin: 5px
+
+  .swimlane-header-wrap .list-name-input.full-line
+    margin: 0
+    display: inline-block
+    width: 270px
+
+  .swimlane-header-wrap .edit-controls
+    display: inline-block
+    vertical-align: middle
+
+  .swimlane-header-wrap .primary.confirm
+    margin-right: 0
+
+  .swimlane-header-wrap .fa.fa-times-thin
+    margin-top: 2px
+
+  // This is a general fix so that the little grabby hand appears when dragging the list via the title
+  .list.ui-sortable-helper,
+  .list.ui-sortable-helper .list-header.ui-sortable-handle,
+  .list.ui-sortable-helper .viewer
+    cursor:-webkit-grabbing;
+    cursor:grabbing
+
+.board-color-clearblue
+  setBoardClear(rgb(73, 155, 234),rgb(0, 174, 204))
+
+/*
+  Alternate "Natural" Styling
+*/
+.board-color-natural
+  setBoardColor(#596557)
+
+  &#header-quick-access
+    background-color: #2d392b
+
+  .ui-sortable
+    background-color:#dedede
+
+  .list-header
+    background-color: #c9cfc3
+    border-bottom: 6px solid #c9cfc3
+
+  .swimlane .swimlane-header-wrap
+    background-color: #c2c0ab
+
+/*
+  Alternate "Modern" Styling
+*/
+.board-color-modern
+  setBoardColor(#2A80B8)
+
+  /* General */
+  body
+    background: #f5f5f5
+
+  &#header-quick-access
+    padding: 10px
+    font-size: 14px
+    background: #333 !important
+
+  &#header-quick-access ul
+    overflow: visible
+
+  &#header-quick-access ul li.current
+    border: 0 !important
+    font-weight: bold
+
+  &#header-quick-access ul li.separator
+    display: none
+
+  &#header-quick-access ul li:nth-child(3)
+    margin-right: 10px
+
+  &#header-quick-access ul li a
+    padding: 5px 10px
+    border-radius: 2px
+
+  &#header-quick-access ul li.current a
+    border-radius: 2px
+    background: rgba(255,255,255,.2)
+
+  &#header #header-main-bar h1
+    font-family: Poppins
+    font-weight: bold
+  &#header-quick-access #header-user-bar
+    position relative
+
+  &#header-quick-access #header-user-bar .header-user-bar-name
+    margin: 5px 3px 0 0
+
+  section#notifications-drawer
+    top: 46px
+    box-shadow: 0 4px 20px rgba(0,0,0,.1)
+    max-width: 100%
+
+  section#notifications-drawer .header
+    top: 46px
+    border-radius: 0 3px
+    height: 21px
+    background: #f7f7f7
+
+  .board-canvas
+    background: #f5f5f5
+
+  /* Swimlane */
+  .swimlane
+    background: none
+
+  .swimlane .swimlane-header-wrap .swimlane-header
+    font-family: Poppins
+
+  /* All board views */
+  .board-list .board-list-item
+    padding: 20px
+
+  .board-list-item-name
+    font-family: Poppins
+
+  /* Board */
+  .list
+    background: transparent
+    border-left: 0
+    margin: 10px 0
+    padding: 0px
+    border-radius: 5px
+    min-width: 300px
+
+  .list-body .open-minicard-composer:hover /*me*/
+    background: none
+    box-shadow: none
+
+  .list:first-child
+    margin-left: 5px
+
+  .list.list-composer.js-list-composer
+    transition: all .3s ease
+    min-width: 80px
+
+  .open-list-composer.js-open-inlined-form:hover
+    color: #222
+
+  .list-header
+    background: none
+    border-bottom-width: 0px
+
+  .list-header .list-header-name
+    font-family: Poppins
+    color: #000
+    font-weight: 500
+
+  /* Card changes */
+  .minicard
+    padding: 15px 15px 10px
+    box-shadow: 0 3px 8px rgba(0,0,0,.05)
+
+  .minicard-plum:hover:not(.minicard-composer), .is-selected .minicard-plum, .draggable-hover-card .minicard-plum
+    background: none
+
+  .minicard-title
+    line-height: 1.5em
+
+  .minicard .minicard-cover
+    background-size: cover
+    margin: -15px -15px 10px
+    height: 100px
+
+  .card-label-orange
+    color: #fff
+
+  .card-date
+    font-size: 12px
+    padding: 3px 5px
+
+  /* Pop over */
+  .header-title
+    font-family: Poppins
+    font-size: 16px
+    color: #333
+
+  .pop-over
+    box-shadow: 0 4px 20px rgba(0,0,0,.2)
+    border: 0
+    border-radius: 5px
+
+  .pop-over .header
+    padding: 10px
+    border-bottom: 0
+    border-radius: 5px 5px 0 0
+    background:#eee
+
+  .pop-over .header .header-title
+    font-family: Poppins
+    font-size:16px
+    color:#333
+
+  .pop-over .header .close-btn
+    font-size:20px
+    top:6px
+    right:8px
+
+  .pop-over .content-container .content
+    padding: 5px 20px 20px
+    width: 260px
+
+  .pop-over-list li > a
+    border-radius: 5px
+
+  .pop-over-list li > a > i
+    margin-right: 5px
+
+  .pop-over-list li>a .sub-name
+    margin-bottom: 8px
+
+  /* Sidebar */
+  .sidebar .sidebar-shadow
+    box-shadow: 0 0 60px rgba(0,0,0,.2)
+
+  .sidebar .sidebar-content
+    padding: 30px
+
+  /* Notifications */
+  .board-color-modern section#notifications-drawer
+    border-radius:5px
+
+  .board-color-modern section#notifications-drawer .header
+    padding: 18px 16px
+    border-bottom: 0
+    border-radius: 5px 5px 0 0
+    background: #eee
+
+  .board-color-modern section#notifications-drawer .header h5
+    font-family: Poppins
+    font-weight: bold
+
+  .board-color-modern section#notifications-drawer .header .close
+    font-size: 20px
+    top: 14px
+
+  section#notifications-drawer .header .toggle-read
+    top: 18px
+
+/*
+  Alternate "Modern Dark" Styling
+*/
+.board-color-moderndark
+  setBoardColor(#2a2a2a)
+
+  /* General */
+  body
+    background: #2a2a2a
+
+  .board-wrapper .board-canvas .board-overlay
+    opacity: .6
+
+  /* Forms */
+  button[type=submit].primary, .board-color-modern input[type=submit].primary
+    background-color: #819C5D
+
+  .toggle-switch:checked~.toggle-label
+    background-color: #D2E9B4
+
+  .toggle-label:after, .board-color-modern .toggle-switch:checked~.toggle-label:after
+    background-color: #819C5D !important
+
+  button, input:not([type=file]), select, textarea
+    border-radius: 2px
+
+  /* Headers */
+  &#header
+    background-color: #262626
+    border-bottom: 1px solid #555555;
+    border-top: 1px solid #555555;
+
+  &#header-quick-access, .background-box, #header
+    background-color: #333333
+
+  &#header-quick-access
+    padding: 4px
+    font-size: 14px
+
+  &#header-quick-access .allBoards
+    padding: 5px 10px 0 10px;
+
+  &#header-quick-access ul.header-quick-access-list
+    margin: -5px 0 -5px 0
+
+  &#header #header-main-bar
+    padding-top: 3px
+    padding-bottom: 3px
+
+  &#header-quick-access ul
+    overflow: visible
+
+  &#header-quick-access ul li.current
+    border: 0 !important
+    font-weight: bold
+
+  &#header-quick-access ul li.separator
+    display: none
+
+  &#header-quick-access ul li:nth-child(3)
+    margin-right: 10px
+
+  &#header-quick-access ul li a
+    padding: 5px 10px
+    border-radius: 2px
+
+  &#header-quick-access ul li.current a
+    border-radius: 2px
+    background: rgba(255,255,255,.2)
+
+  &#header #header-main-bar h1
+    font-family: Poppins
+    font-weight: bold
+    line-height: 0.8em
+    padding-top: 10px
+
+  /* Content */
+  .board-canvas
+    background: #2a2a2a
+
+  /* Swimlanes */
+  .swimlane .swimlane-header-wrap
+    background-color: #494949
+    color: #cccccc
+    padding: 4px 0
+
+  .swimlane .swimlane-header-wrap .swimlane-header
+    font-family: Poppins
+
+  .swimlane .swimlane-header-wrap .swimlane-header-menu
+    padding: 6px
+    font-size: 16px
+
+  .swimlane .swimlane-header-wrap .swimlane-header-plus-icon
+    font-size: 16px
+
+  .swimlane
+    background: #2a2a2a
+    line-height: 15px
+    max-height: 100%
+
+  /* Lists */
+  .swimlane .list
+    background: #666666
+    border-radius: 0
+    border: 0px solid #666666
+    flex: 0 0 265px;
+
+  .swimlane .list:first-child
+    margin-left: 0
+
+  .swimlane .list:nth-child(even)
+    background: #5f5f5f
+
+  .swimlane .list:nth-child(odd) .list-header
+    background: #3b3b3b
+
+  .list-header
+    background: #333333
+    padding: 10px
+    border-bottom: 0
+
+  .list-header .viewer
+    padding-left: 10px
+
+  .list-header .list-header-name
+    line-height: 14px
+    color: #eeeeee
+
+  .list-header .list-header-menu
+    padding: 10px
+    top: 0
+
+  .list-header .list-header-plus-icon
+    color: #a6a6a6
+
+  .list-body
+    scrollbar-width: thin
+    scrollbar-color: #343434 #999999
+
+  .list-body::-webkit-scrollbar
+    width: 10px
+
+  .list-body::-webkit-scrollbar-track
+    background: #343434
+    border-radius: 3px
+    margin: 4px 0
+
+  .list-body::-webkit-scrollbar-thumb
+    background-color: #999999
+    border-radius: 6px
+    border: 3px solid #343434
+
+  .list-body .open-minicard-composer:hover
+    background: none
+    box-shadow: none
+    border-bottom: 0
+
+  .list-body a.open-minicard-composer, .list-body a.open-minicard-composer i, .list .list-composer .open-list-composer i
+    color: #bbbbbb
+
+  .list-body a.open-minicard-composer:hover, .list-body a.open-minicard-composer:hover i, .list .list-composer .open-list-composer:hover i
+    color: #ffffff
+
+  /* Mini Card */
+  .minicard-wrapper
+    margin-bottom: 12px
+
+  .minicard
+    background-color: #444444
+    color: #cccccc
+    border-radius: 2px
+    font-size: 0.95em
+    padding: 10px
+    box-shadow: 0 4px 3px -3px rgba(0,0,0,0.8)
+    border-bottom: 1px solid #666666
+
+  .minicard:hover
+    background-color: #494949 !important
+
+  .minicard .minicard-labels
+    margin-bottom: 4px
+
+  .minicard .card-label
+    font-size: 11px
+    font-weight: 400
+    padding: 1px 6px 0
+    border-radius: 2px
+
+  .minicard .badges
+    color: #bbbbbb
+
+  .minicard .date
+    margin-top: 10px
+    font-size: 11px
+
+  .card-date
+    color: #444444
+    border-radius: 2px
+
+  .card-date.almost-due
+    color: #444444
+
+  .minicard.minicard-composer textarea.minicard-composer-textarea:focus
+    background-color: #eeeeee
+    color: #333333
+    padding: 6px
+
+  .is-selected .minicard
+    background-color: #666666
+
+  /* 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
+
+  .card-details
+    scrollbar-width: thin
+    scrollbar-color: #343434 #999999
+
+  .card-details::-webkit-scrollbar
+    width: 16px
+
+  .card-details::-webkit-scrollbar-track
+    background: #343434
+
+  .card-details::-webkit-scrollbar-thumb
+    background-color: #999999
+    border-radius: 6px
+    border: 4px solid #343434
+
+  .card-details .card-details-header
+    background: #333333
+    color: #cccccc
+    border-bottom: 2px solid #2d2d2d
+
+  .card-details hr
+    background: #2d2d2d
+
+  .card-details .card-details-item-title
+    color: #ffffff
+
+  .card-details .new-description textarea, .card-details .new-comment textarea
+    background-color: #dddddd
+    color: #111111
+
+  .card-details .checklist
+    background-color: transparent
+    margin-bottom: 10px
+
+  .card-details .checklist-item
+    background-color: rgba(255,255,255,0.1)
+    padding: 4px 8px
+    border-radius: 2px
+    font-size: 13px
+    margin-top: 5px
+
+  .card-details .checklist-item:hover
+    background-color: rgba(255,255,255,0.2)
+
+  .card-details .checklist-item .item-title .viewer p
+    max-width: auto
+
+  .card-details .check-box.materialCheckBox
+    border-color: #ffffff
+
+  .card-details .check-box.materialCheckBox.is-checked
+    border-bottom: 2px solid #819C5D
+    border-right: 2px solid #819C5D
+    border-top: 0
+    border-left: 0
+
+  .card-details .js-add-checklist-item
+    margin-top: 4px
+
+  .checklist-items .add-checklist-item
+    margin-top: .7em
+
+  .card-details .activities .activity .activity-desc .activity-comment
+    background-color: #cccccc
+    color: #222222
+
+  /* Sidebar */
+  .sidebar .sidebar-shadow
+    background-color: #222222
+    box-shadow: -10px 0 5px -10px #444444
+    border-left: 1px solid #333333
+    color: #cccccc
+
+  .activities .activity .activity-desc .activity-comment
+    background-color: #cccccc
+    color: #222222
+
+  /* Pop-Ups for "Modern Dark" */
+.pop-over.board-color-moderndark
+  background-color: #454545
+  color: #cccccc
+  border: 1px solid #111111
+  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)
+
+.pop-over.board-color-moderndark .header
+  background-color: #333333
+
+.pop-over.board-color-moderndark .header-title
+  font-family: Poppins
+  font-size: 16px
+  color: #cccccc
+
+.pop-over.board-color-moderndark .pop-over-list li:hover > a
+  background-color: #819C5D !important

+ 47 - 12
client/components/boards/boardHeader.jade

@@ -1,7 +1,9 @@
 template(name="boardHeaderBar")
 template(name="boardHeaderBar")
   h1.header-board-menu
   h1.header-board-menu
     with currentBoard
     with currentBoard
-      a(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}")
+      if $eq title 'Templates'
+        | {{_ 'templates'}}
+      else
         +viewer
         +viewer
           = title
           = title
 
 
@@ -9,6 +11,10 @@ template(name="boardHeaderBar")
     unless isMiniScreen
     unless isMiniScreen
       if currentBoard
       if currentBoard
         if currentUser
         if currentUser
+          with currentBoard
+            a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
+              i.fa.fa-pencil-square-o
+
           a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
           a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
             title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
             title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
             i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
             i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
@@ -31,6 +37,12 @@ template(name="boardHeaderBar")
             if $eq watchLevel "muted"
             if $eq watchLevel "muted"
               i.fa.fa-bell-slash
               i.fa.fa-bell-slash
             span {{_ watchLevel}}
             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
         else
           a.board-header-btn.js-log-in(
           a.board-header-btn.js-log-in(
@@ -42,6 +54,10 @@ template(name="boardHeaderBar")
     if currentBoard
     if currentBoard
       if isMiniScreen
       if isMiniScreen
         if currentUser
         if currentUser
+          with currentBoard
+            a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
+              i.fa.fa-pencil-square-o
+
           a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
           a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
             title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
             title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
             i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
             i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
@@ -99,13 +115,13 @@ template(name="boardHeaderBar")
         a.board-header-btn.js-toggle-board-view(
         a.board-header-btn.js-toggle-board-view(
           title="{{_ 'board-view'}}")
           title="{{_ 'board-view'}}")
           i.fa.fa-caret-down
           i.fa.fa-caret-down
-          if $eq boardView 'board-view-lists'
-            i.fa.fa-trello
           if $eq boardView 'board-view-swimlanes'
           if $eq boardView 'board-view-swimlanes'
             i.fa.fa-th-large
             i.fa.fa-th-large
+          if $eq boardView 'board-view-lists'
+            i.fa.fa-trello
           if $eq boardView 'board-view-cal'
           if $eq boardView 'board-view-cal'
             i.fa.fa-calendar
             i.fa.fa-calendar
-          span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-lists'}}{{/if}}
+          span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}}
 
 
       if canModifyBoard
       if canModifyBoard
         a.board-header-btn.js-multiselection-activate(
         a.board-header-btn.js-multiselection-activate(
@@ -118,7 +134,7 @@ template(name="boardHeaderBar")
               i.fa.fa-times-thin
               i.fa.fa-times-thin
 
 
       .separator
       .separator
-      a.board-header-btn.js-toggle-sidebar
+      a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
         i.fa.fa-navicon
         i.fa.fa-navicon
 
 
 template(name="boardVisibilityList")
 template(name="boardVisibilityList")
@@ -172,13 +188,6 @@ template(name="boardChangeWatchPopup")
 
 
 template(name="boardChangeViewPopup")
 template(name="boardChangeViewPopup")
   ul.pop-over-list
   ul.pop-over-list
-    li
-      with "board-view-lists"
-        a.js-open-lists-view
-          i.fa.fa-trello.colorful
-          | {{_ 'board-view-lists'}}
-          if $eq Utils.boardView "board-view-lists"
-            i.fa.fa-check
     li
     li
       with "board-view-swimlanes"
       with "board-view-swimlanes"
         a.js-open-swimlanes-view
         a.js-open-swimlanes-view
@@ -186,6 +195,13 @@ template(name="boardChangeViewPopup")
           | {{_ 'board-view-swimlanes'}}
           | {{_ 'board-view-swimlanes'}}
           if $eq Utils.boardView "board-view-swimlanes"
           if $eq Utils.boardView "board-view-swimlanes"
             i.fa.fa-check
             i.fa.fa-check
+    li
+      with "board-view-lists"
+        a.js-open-lists-view
+          i.fa.fa-trello.colorful
+          | {{_ 'board-view-lists'}}
+          if $eq Utils.boardView "board-view-lists"
+            i.fa.fa-check
     li
     li
       with "board-view-cal"
       with "board-view-cal"
         a.js-open-cal-view
         a.js-open-cal-view
@@ -212,6 +228,9 @@ template(name="createBoard")
           = " "
           = " "
           | {{{_ 'board-private-info'}}}
           | {{{_ 'board-private-info'}}}
         a.js-change-visibility {{_ 'change'}}.
         a.js-change-visibility {{_ 'change'}}.
+    //a.flex.js-toggle-add-template-container
+    //  .materialCheckBox#add-template-container
+    //  span {{_ 'add-template-container'}}
     input.primary.wide(type="submit" value="{{_ 'create'}}")
     input.primary.wide(type="submit" value="{{_ 'create'}}")
     span.quiet
     span.quiet
       | {{_ 'or'}}
       | {{_ 'or'}}
@@ -247,3 +266,19 @@ template(name="boardChangeTitlePopup")
 template(name="boardCreateRulePopup")
 template(name="boardCreateRulePopup")
   p {{_ 'close-board-pop'}}
   p {{_ 'close-board-pop'}}
   button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
   button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
+
+
+template(name="cardsSortPopup")
+  ul.pop-over-list
+    li
+      a.js-sort-due {{_ 'due-date'}}
+      hr
+    li
+      a.js-sort-title {{_ 'title-alphabetically'}}
+      hr
+    li
+      a.js-sort-created-desc {{_ 'created-at-newest-first'}}
+      hr
+    li
+      a.js-sort-created-asc {{_ 'created-at-oldest-first'}}
+

+ 117 - 28
client/components/boards/boardHeader.js

@@ -2,6 +2,7 @@
 const DOWNCLS = 'fa-sort-down';
 const DOWNCLS = 'fa-sort-down';
 const UPCLS = 'fa-sort-up';
 const UPCLS = 'fa-sort-up';
 */
 */
+const sortCardsBy = new ReactiveVar('');
 Template.boardMenuPopup.events({
 Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
   'click .js-rename-board': Popup.open('boardChangeTitle'),
   'click .js-custom-fields'() {
   'click .js-custom-fields'() {
@@ -33,22 +34,6 @@ Template.boardMenuPopup.events({
   'click .js-card-settings': Popup.open('boardCardSettings'),
   'click .js-card-settings': Popup.open('boardCardSettings'),
 });
 });
 
 
-Template.boardMenuPopup.helpers({
-  exportUrl() {
-    const params = {
-      boardId: Session.get('currentBoard'),
-    };
-    const queryParams = {
-      authToken: Accounts._storedLoginToken(),
-    };
-    return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
-  },
-  exportFilename() {
-    const boardId = Session.get('currentBoard');
-    return `wekan-export-board-${boardId}.json`;
-  },
-});
-
 Template.boardChangeTitlePopup.events({
 Template.boardChangeTitlePopup.events({
   submit(event, templateInstance) {
   submit(event, templateInstance) {
     const newTitle = templateInstance
     const newTitle = templateInstance
@@ -126,6 +111,7 @@ BlazeComponent.extendComponent({
         'click .js-open-filter-view'() {
         'click .js-open-filter-view'() {
           Sidebar.setView('filter');
           Sidebar.setView('filter');
         },
         },
+        'click .js-sort-cards': Popup.open('cardsSort'),
         /*
         /*
         'click .js-open-sort-view'(evt) {
         'click .js-open-sort-view'(evt) {
           const target = evt.target;
           const target = evt.target;
@@ -143,6 +129,9 @@ BlazeComponent.extendComponent({
           Sidebar.setView();
           Sidebar.setView();
           Filter.reset();
           Filter.reset();
         },
         },
+        'click .js-sort-reset'() {
+          Session.set('sortBy', '');
+        },
         'click .js-open-search-view'() {
         'click .js-open-search-view'() {
           Sidebar.setView('search');
           Sidebar.setView('search');
         },
         },
@@ -176,6 +165,9 @@ Template.boardHeaderBar.helpers({
   boardView() {
   boardView() {
     return Utils.boardView();
     return Utils.boardView();
   },
   },
+  isSortActive() {
+    return Session.get('sortBy') ? true : false;
+  },
 });
 });
 
 
 Template.boardChangeViewPopup.events({
 Template.boardChangeViewPopup.events({
@@ -217,24 +209,79 @@ const CreateBoard = BlazeComponent.extendComponent({
     this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
     this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
   },
   },
 
 
+  toggleAddTemplateContainer() {
+    $('#add-template-container').toggleClass('is-checked');
+  },
+
   onSubmit(event) {
   onSubmit(event) {
     event.preventDefault();
     event.preventDefault();
     const title = this.find('.js-new-board-title').value;
     const title = this.find('.js-new-board-title').value;
-    const visibility = this.visibility.get();
 
 
-    this.boardId.set(
-      Boards.insert({
-        title,
-        permission: visibility,
-      }),
-    );
+    const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
+    if (addTemplateContainer) {
+      //const templateContainerId = Meteor.call('setCreateTemplateContainer');
+      //Utils.goBoardId(templateContainerId);
+      //alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
 
 
-    Swimlanes.insert({
-      title: 'Default',
-      boardId: this.boardId.get(),
-    });
+      this.boardId.set(
+        Boards.insert({
+            // title: TAPi18n.__('templates'),
+            title: title,
+            permission: 'private',
+            type: 'template-container',
+          }),
+       );
+
+      // Insert the card templates swimlane
+      Swimlanes.insert({
+          // title: TAPi18n.__('card-templates-swimlane'),
+          title: 'Card Templates',
+          boardId: this.boardId.get(),
+          sort: 1,
+          type: 'template-container',
+        }),
+
+      // Insert the list templates swimlane
+      Swimlanes.insert(
+        {
+          // title: TAPi18n.__('list-templates-swimlane'),
+          title: 'List Templates',
+          boardId: this.boardId.get(),
+          sort: 2,
+          type: 'template-container',
+        },
+      );
+
+      // Insert the board templates swimlane
+      Swimlanes.insert(
+        {
+          //title: TAPi18n.__('board-templates-swimlane'),
+          title: 'Board Templates',
+          boardId: this.boardId.get(),
+          sort: 3,
+          type: 'template-container',
+        },
+      );
+
+      Utils.goBoardId(this.boardId.get());
+
+    } else {
+      const visibility = this.visibility.get();
 
 
-    Utils.goBoardId(this.boardId.get());
+      this.boardId.set(
+        Boards.insert({
+          title,
+          permission: visibility,
+        }),
+      );
+
+      Swimlanes.insert({
+        title: 'Default',
+        boardId: this.boardId.get(),
+      });
+
+      Utils.goBoardId(this.boardId.get());
+    }
   },
   },
 
 
   events() {
   events() {
@@ -248,6 +295,7 @@ const CreateBoard = BlazeComponent.extendComponent({
         submit: this.onSubmit,
         submit: this.onSubmit,
         'click .js-import-board': Popup.open('chooseBoardSource'),
         'click .js-import-board': Popup.open('chooseBoardSource'),
         'click .js-board-template': Popup.open('searchElement'),
         'click .js-board-template': Popup.open('searchElement'),
+        'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
       },
       },
     ];
     ];
   },
   },
@@ -384,3 +432,44 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('listsortPopup');
 }).register('listsortPopup');
 */
 */
+
+BlazeComponent.extendComponent({
+  events() {
+    return [
+      {
+        'click .js-sort-due'() {
+          const sortBy = {
+            dueAt: 1,
+          };
+          Session.set('sortBy', sortBy);
+          sortCardsBy.set(TAPi18n.__('due-date'));
+          Popup.close();
+        },
+        'click .js-sort-title'() {
+          const sortBy = {
+            title: 1,
+          };
+          Session.set('sortBy', sortBy);
+          sortCardsBy.set(TAPi18n.__('title'));
+          Popup.close();
+        },
+        'click .js-sort-created-asc'() {
+          const sortBy = {
+            createdAt: 1,
+          };
+          Session.set('sortBy', sortBy);
+          sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
+          Popup.close();
+        },
+        'click .js-sort-created-desc'() {
+          const sortBy = {
+            createdAt: -1,
+          };
+          Session.set('sortBy', sortBy);
+          sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
+          Popup.close();
+        },
+      },
+    ];
+  },
+}).register('cardsSortPopup');

+ 96 - 48
client/components/boards/boardsList.jade

@@ -1,10 +1,11 @@
 template(name="boardList")
 template(name="boardList")
   .wrapper
   .wrapper
-    ul.board-list.clearfix
+    ul.board-list.clearfix.js-boards
       li.js-add-board
       li.js-add-board
-        a.board-list-item.label {{_ 'add-board'}}
+        a.board-list-item.label(title="{{_ 'add-board'}}")
+          | {{_ 'add-board'}}
       each boards
       each boards
-        li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
+        li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
           if isInvited
           if isInvited
             .board-list-item
             .board-list-item
               span.details
               span.details
@@ -16,50 +17,97 @@ template(name="boardList")
                 button.js-accept-invite.primary {{_ 'accept'}}
                 button.js-accept-invite.primary {{_ 'accept'}}
                 button.js-decline-invite {{_ 'decline'}}
                 button.js-decline-invite {{_ 'decline'}}
           else
           else
-            a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
-              span.details
-                span.board-list-item-name
-                  +viewer
-                    = title
-                i.fa.js-star-board(
-                  class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
-                  title="{{_ 'star-board-title'}}")
-                p.board-list-item-desc
-                  +viewer
-                    = description
-                if hasSpentTimeCards
-                  i.fa.js-has-spenttime-cards(
-                    class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
-                    title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
-                unless isMiniScreen
-                  if isSandstorm
-                    i.fa.js-clone-board(
-                        class="fa-clone"
-                        title="{{_ 'duplicate-board'}}")
-                    i.fa.js-archive-board(
-                        class="fa-archive"
-                        title="{{_ 'archive-board'}}")
-                  else if currentUser.isBoardAdmin
-                    i.fa.js-clone-board(
-                        class="fa-clone"
-                        title="{{_ 'duplicate-board'}}")
-                    i.fa.js-archive-board(
-                        class="fa-archive"
-                        title="{{_ 'archive-board'}}")
-                  else if currentUser.isAdmin
-                    i.fa.js-clone-board(
-                        class="fa-clone"
-                        title="{{_ 'duplicate-board'}}")
-                    i.fa.js-archive-board(
-                        class="fa-archive"
-                        title="{{_ 'archive-board'}}")
+            if $eq type "template-container"
+              a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
+                span.details
+                  span.board-list-item-name(title="{{_ 'template-container'}}")
+                    +viewer
+                      = title
+                  i.fa.js-star-board(
+                    class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
+                    title="{{_ 'star-board-title'}}")
+                  p.board-list-item-desc
+                    +viewer
+                      = description
+                  if hasSpentTimeCards
+                    i.fa.js-has-spenttime-cards(
+                      class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
+                      title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
+                  if isMiniScreen
+                    i.fa.board-handle(
+                        class="fa-arrows"
+                        title="{{_ 'Drag board'}}")
+                  unless isMiniScreen
+                    if isSandstorm
+                      i.fa.js-clone-board(
+                          class="fa-clone"
+                          title="{{_ 'duplicate-board'}}")
+                      i.fa.js-archive-board(
+                          class="fa-archive"
+                          title="{{_ 'archive-board'}}")
+                    else if isAdministrable
+                      i.fa.js-clone-board(
+                          class="fa-clone"
+                          title="{{_ 'duplicate-board'}}")
+                      i.fa.js-archive-board(
+                          class="fa-archive"
+                          title="{{_ 'archive-board'}}")
+                    else if currentUser.isAdmin
+                      i.fa.js-clone-board(
+                          class="fa-clone"
+                          title="{{_ 'duplicate-board'}}")
+                      i.fa.js-archive-board(
+                          class="fa-archive"
+                          title="{{_ 'archive-board'}}")
+            else
+              a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
+                span.details
+                  span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+                    +viewer
+                      = title
+                  i.fa.js-star-board(
+                    class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
+                    title="{{_ 'star-board-title'}}")
+                  p.board-list-item-desc
+                    +viewer
+                      = description
+                  if hasSpentTimeCards
+                    i.fa.js-has-spenttime-cards(
+                      class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
+                      title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
+                  if isMiniScreen
+                    i.fa.board-handle(
+                        class="fa-arrows"
+                        title="{{_ 'Drag board'}}")
+                  unless isMiniScreen
+                    if isSandstorm
+                      i.fa.js-clone-board(
+                          class="fa-clone"
+                          title="{{_ 'duplicate-board'}}")
+                      i.fa.js-archive-board(
+                          class="fa-archive"
+                          title="{{_ 'archive-board'}}")
+                    else if isAdministrable
+                      i.fa.js-clone-board(
+                          class="fa-clone"
+                          title="{{_ 'duplicate-board'}}")
+                      i.fa.js-archive-board(
+                          class="fa-archive"
+                          title="{{_ 'archive-board'}}")
+                    else if currentUser.isAdmin
+                      i.fa.js-clone-board(
+                          class="fa-clone"
+                          title="{{_ 'duplicate-board'}}")
+                      i.fa.js-archive-board(
+                          class="fa-archive"
+                          title="{{_ 'archive-board'}}")
 
 
 template(name="boardListHeaderBar")
 template(name="boardListHeaderBar")
-  h1 {{_ 'my-boards'}}
-  .board-header-btns.right
-    a.board-header-btn.js-open-archived-board
-      i.fa.fa-archive
-      span {{_ 'archives'}}
-    a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
-      i.fa.fa-clone
-      span {{_ 'templates'}}
+  h1 {{_ title }}
+  //.board-header-btns.right
+  //  a.board-header-btn.js-open-archived-board
+  //    i.fa.fa-archive
+  //    span {{_ 'archives'}}
+  //  a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
+  //    i.fa.fa-clone
+  //    span {{_ 'templates'}}

+ 87 - 10
client/components/boards/boardsList.js

@@ -1,4 +1,5 @@
 const subManager = new SubsManager();
 const subManager = new SubsManager();
+const { calculateIndex, enableClickOnTouch } = Utils;
 
 
 Template.boardListHeaderBar.events({
 Template.boardListHeaderBar.events({
   'click .js-open-archived-board'() {
   'click .js-open-archived-board'() {
@@ -7,6 +8,9 @@ Template.boardListHeaderBar.events({
 });
 });
 
 
 Template.boardListHeaderBar.helpers({
 Template.boardListHeaderBar.helpers({
+  title() {
+    return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public';
+  },
   templatesBoardId() {
   templatesBoardId() {
     return Meteor.user() && Meteor.user().getTemplatesBoardId();
     return Meteor.user() && Meteor.user().getTemplatesBoardId();
   },
   },
@@ -18,22 +22,91 @@ Template.boardListHeaderBar.helpers({
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     Meteor.subscribe('setting');
     Meteor.subscribe('setting');
+    let currUser = Meteor.user();
+    let userLanguage;
+    if(currUser && currUser.profile){
+      userLanguage = currUser.profile.language
+    }
+    if (userLanguage) {
+      TAPi18n.setLanguage(userLanguage);
+      T9n.setLanguage(userLanguage);
+    }
   },
   },
 
 
-  boards() {
-    return Boards.find(
-      {
-        archived: false,
-        'members.userId': Meteor.userId(),
-        type: 'board',
+  onRendered() {
+    const itemsSelector = '.js-board:not(.placeholder)';
+
+    const $boards = this.$('.js-boards');
+    $boards.sortable({
+      connectWith: '.js-boards',
+      tolerance: 'pointer',
+      appendTo: '.board-list',
+      helper: 'clone',
+      distance: 7,
+      items: itemsSelector,
+      placeholder: 'board-wrapper placeholder',
+      start(evt, ui) {
+        ui.helper.css('z-index', 1000);
+        ui.placeholder.height(ui.helper.height());
+        EscapeActions.executeUpTo('popup-close');
       },
       },
-      { sort: ['title'] },
-    );
+      stop(evt, ui) {
+        // To attribute the new index number, we need to get the DOM element
+        // of the previous and the following card -- if any.
+        const prevBoardDom = ui.item.prev('.js-board').get(0);
+        const nextBoardBom = ui.item.next('.js-board').get(0);
+        const sortIndex = calculateIndex(prevBoardDom, nextBoardBom, 1);
+
+        const boardDomElement = ui.item.get(0);
+        const board = Blaze.getData(boardDomElement);
+        // Normally the jquery-ui sortable library moves the dragged DOM element
+        // to its new position, which disrupts Blaze reactive updates mechanism
+        // (especially when we move the last card of a list, or when multiple
+        // users move some cards at the same time). To prevent these UX glitches
+        // we ask sortable to gracefully cancel the move, and to put back the
+        // DOM in its initial state. The card move is then handled reactively by
+        // Blaze with the below query.
+        $boards.sortable('cancel');
+
+        board.move(sortIndex.base);
+      },
+    });
+
+    // 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()) {
+        $boards.sortable({
+          handle: '.board-handle',
+        });
+      }
+    });
+  },
+
+  boards() {
+    const query = {
+      archived: false,
+      //type: { $in: ['board','template-container'] },
+      type: 'board',
+    };
+    if (FlowRouter.getRouteName() === 'home')
+      query['members.userId'] = Meteor.userId();
+    else query.permission = 'public';
+
+    return Boards.find(query, {
+      sort: { sort: 1 /* boards default sorting */ },
+    });
   },
   },
   isStarred() {
   isStarred() {
     const user = Meteor.user();
     const user = Meteor.user();
     return user && user.hasStarred(this.currentData()._id);
     return user && user.hasStarred(this.currentData()._id);
   },
   },
+  isAdministrable() {
+    const user = Meteor.user();
+    return user && user.isBoardAdmin(this.currentData()._id);
+  },
 
 
   hasOvertimeCards() {
   hasOvertimeCards() {
     subManager.subscribe('board', this.currentData()._id, false);
     subManager.subscribe('board', this.currentData()._id, false);
@@ -61,9 +134,13 @@ BlazeComponent.extendComponent({
         },
         },
         'click .js-clone-board'(evt) {
         'click .js-clone-board'(evt) {
           Meteor.call(
           Meteor.call(
-            'cloneBoard',
+            'copyBoard',
             this.currentData()._id,
             this.currentData()._id,
-            Session.get('fromBoard'),
+            {
+              sort: Boards.find({ archived: false }).count(),
+              type: 'board',
+              title: Boards.findOne(this.currentData()._id).title,
+            },
             (err, res) => {
             (err, res) => {
               if (err) {
               if (err) {
                 this.setError(err.error);
                 this.setError(err.error);

+ 37 - 5
client/components/boards/boardsList.styl

@@ -7,10 +7,23 @@ $spaceBetweenTiles = 16px
 
 
   li
   li
     float: left
     float: left
-    width: 25%
+    width: 20%
     box-sizing: border-box
     box-sizing: border-box
     position: relative
     position: relative
 
 
+    &.placeholder:after
+      content: '';
+      display: block;
+      background: darken(white, 20%)
+      border-radius: 3px;
+      height: 106px;
+      margin: 8px;
+
+    &.ui-sortable-helper
+      cursor: grabbing
+      transform: rotate(4deg)
+      display: block !important
+
     &.starred
     &.starred
       .fa-star,
       .fa-star,
       .fa-star-o
       .fa-star-o
@@ -20,17 +33,20 @@ $spaceBetweenTiles = 16px
     overflow: hidden;
     overflow: hidden;
     background-color: #999
     background-color: #999
     color: #f6f6f6
     color: #f6f6f6
-    height: 90px
+    min-height: 100px
     font-size: 16px
     font-size: 16px
     line-height: 22px
     line-height: 22px
     border-radius: 3px
     border-radius: 3px
     display: block
     display: block
     font-weight: 700
     font-weight: 700
-    min-height: 18px
     padding: 8px
     padding: 8px
     margin: ($spaceBetweenTiles/2)
     margin: ($spaceBetweenTiles/2)
     position: relative
     position: relative
     text-decoration: none
     text-decoration: none
+    word-wrap: break-word
+
+    &.template-container
+      border: 4px solid #fff
 
 
     &.tile
     &.tile
       background-size: auto
       background-size: auto
@@ -55,7 +71,7 @@ $spaceBetweenTiles = 16px
 
 
     .label
     .label
       font-weight: normal
       font-weight: normal
-      line-height:90px
+      line-height: 56px
 
 
     :hover
     :hover
       background-color:#939393
       background-color:#939393
@@ -183,7 +199,7 @@ $spaceBetweenTiles = 16px
     overflow: scroll
     overflow: scroll
 
 
     li
     li
-      width: 50% 
+      width: 50%
 
 
     .board-list-item
     .board-list-item
       overflow: hidden
       overflow: hidden
@@ -194,6 +210,22 @@ $spaceBetweenTiles = 16px
       top: -100px
       top: -100px
       left: -100px
       left: -100px
 
 
+    .board-handle
+      position: absolute
+      padding: 7px
+      top: 50%
+      transform: translateY(-50%)
+      right: 10px
+      font-size: 24px
+
 @media screen and (max-width: 360px)
 @media screen and (max-width: 360px)
     li
     li
       width: 100%
       width: 100%
+
+    .board-handle
+      position: absolute
+      padding: 7px
+      top: 50%
+      transform: translateY(-50%)
+      right: 10px
+      font-size: 24px

+ 5 - 5
client/components/cards/attachments.jade

@@ -46,14 +46,14 @@ template(name="attachmentsGalery")
                         | {{_ 'remove-cover'}}
                         | {{_ 'remove-cover'}}
                       else
                       else
                         | {{_ 'add-cover'}}
                         | {{_ 'add-cover'}}
-                  a.js-confirm-delete
-                    i.fa.fa-close
-                    | {{_ 'delete'}}
+                  if currentUser.isBoardAdmin
+                    a.js-confirm-delete
+                      i.fa.fa-close
+                      | {{_ 'delete'}}
 
 
     if currentUser.isBoardMember
     if currentUser.isBoardMember
       unless currentUser.isCommentOnly
       unless currentUser.isCommentOnly
         unless currentUser.isWorker
         unless currentUser.isWorker
           //li.attachment-item.add-attachment
           //li.attachment-item.add-attachment
-          a.js-add-attachment
+          a.js-add-attachment(title="{{_ 'add-attachment' }}")
             i.fa.fa-plus
             i.fa.fa-plus
-            | {{_ 'add-attachment' }}

+ 6 - 0
client/components/cards/attachments.js

@@ -45,6 +45,12 @@ Template.attachmentsGalery.events({
   },
   },
 });
 });
 
 
+Template.attachmentsGalery.helpers({
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+});
+
 Template.previewAttachedImagePopup.events({
 Template.previewAttachedImagePopup.events({
   'click .js-large-image-clicked'() {
   'click .js-large-image-clicked'() {
     Popup.close();
     Popup.close();

+ 47 - 2
client/components/cards/cardCustomFields.jade

@@ -4,8 +4,7 @@ template(name="cardCustomFieldsPopup")
             li.item(class="")
             li.item(class="")
                 a.name.js-select-field(href="#")
                 a.name.js-select-field(href="#")
                     span.full-name
                     span.full-name
-                      +viewer
-                        = name
+                      = name
                     if hasCustomField
                     if hasCustomField
                       i.fa.fa-check
                       i.fa.fa-check
     hr
     hr
@@ -53,6 +52,31 @@ template(name="cardCustomField-number")
         if value
         if value
             = value
             = value
 
 
+template(name="cardCustomField-checkbox")
+  .js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
+    if canModifyCard
+      .check-box-container
+        .check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
+    else
+      .materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
+
+template(name="cardCustomField-currency")
+    if canModifyCard
+        +inlinedForm(classNames="js-card-customfield-currency")
+            input(type="text" value=data.value)
+            .edit-controls.clearfix
+                button.primary(type="submit") {{_ 'save'}}
+                a.fa.fa-times-thin.js-close-inlined-form
+        else
+            a.js-open-inlined-form
+                if value
+                    = formattedValue
+                else
+                    | {{_ 'edit'}}
+    else
+        if value
+            = formattedValue
+
 template(name="cardCustomField-date")
 template(name="cardCustomField-date")
     if canModifyCard
     if canModifyCard
         a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
         a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
@@ -95,3 +119,24 @@ template(name="cardCustomField-dropdown")
         if value
         if value
             +viewer
             +viewer
                 = selectedItem
                 = selectedItem
+
+template(name="cardCustomField-stringtemplate")
+    if canModifyCard
+        +inlinedForm(classNames="js-card-customfield-stringtemplate")
+            each item in stringtemplateItems.get
+                input.js-card-customfield-stringtemplate-item(type="text" value=item placeholder="")
+            input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}" autofocus)
+            .edit-controls.clearfix
+                button.primary(type="submit") {{_ 'save'}}
+                a.fa.fa-times-thin.js-close-inlined-form
+        else
+            a.js-open-inlined-form
+                if value
+                    +viewer
+                        = formattedValue
+                else
+                    | {{_ 'edit'}}
+    else
+        if value
+            +viewer
+                = formattedValue

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

@@ -1,3 +1,6 @@
+import { DatePicker } from '/client/lib/datepicker';
+import Cards from '/models/cards';
+
 Template.cardCustomFieldsPopup.helpers({
 Template.cardCustomFieldsPopup.helpers({
   hasCustomField() {
   hasCustomField() {
     const card = Cards.findOne(Session.get('currentCard'));
     const card = Cards.findOne(Session.get('currentCard'));
@@ -80,6 +83,56 @@ CardCustomField.register('cardCustomField');
   }
   }
 }.register('cardCustomField-number'));
 }.register('cardCustomField-number'));
 
 
+// cardCustomField-checkbox
+(class extends CardCustomField {
+  onCreated() {
+    super.onCreated();
+  }
+
+  toggleItem() {
+    this.card.setCustomField(this.customFieldId, !this.data().value);
+  }
+
+  events() {
+    return [
+      {
+        'click .js-checklist-item .check-box-container': this.toggleItem,
+      },
+    ];
+  }
+}.register('cardCustomField-checkbox'));
+
+// cardCustomField-currency
+(class extends CardCustomField {
+  onCreated() {
+    super.onCreated();
+
+    this.currencyCode = this.data().definition.settings.currencyCode;
+  }
+
+  formattedValue() {
+    const locale = TAPi18n.getLanguage();
+
+    return new Intl.NumberFormat(locale, {
+      style: 'currency',
+      currency: this.currencyCode,
+    }).format(this.data().value);
+  }
+
+  events() {
+    return [
+      {
+        'submit .js-card-customfield-currency'(event) {
+          event.preventDefault();
+          // To allow input separated by comma, the comma is replaced by a period.
+          const value = Number(this.find('input').value.replace(/,/i, '.'), 10);
+          this.card.setCustomField(this.customFieldId, value);
+        },
+      },
+    ];
+  }
+}.register('cardCustomField-currency'));
+
 // cardCustomField-date
 // cardCustomField-date
 (class extends CardCustomField {
 (class extends CardCustomField {
   onCreated() {
   onCreated() {
@@ -184,3 +237,90 @@ CardCustomField.register('cardCustomField');
     ];
     ];
   }
   }
 }.register('cardCustomField-dropdown'));
 }.register('cardCustomField-dropdown'));
+
+// cardCustomField-stringtemplate
+(class extends CardCustomField {
+  onCreated() {
+    super.onCreated();
+
+    this.stringtemplateFormat = this.data().definition.settings.stringtemplateFormat;
+    this.stringtemplateSeparator = this.data().definition.settings.stringtemplateSeparator;
+
+    this.stringtemplateItems = new ReactiveVar(this.data().value ?? []);
+  }
+
+  formattedValue() {
+    return (this.data().value ?? [])
+      .filter(value => !!value.trim())
+      .map(value => this.stringtemplateFormat.replace(/%\{value\}/gi, value))
+      .join(this.stringtemplateSeparator ?? '');
+  }
+
+  getItems() {
+    return Array.from(this.findAll('input'))
+      .map(input => input.value)
+      .filter(value => !!value.trim());
+  }
+
+  events() {
+    return [
+      {
+        'submit .js-card-customfield-stringtemplate'(event) {
+          event.preventDefault();
+          const items = this.getItems();
+          this.card.setCustomField(this.customFieldId, items);
+        },
+
+        'keydown .js-card-customfield-stringtemplate-item'(event) {
+          if (event.keyCode === 13) {
+            event.preventDefault();
+
+            if (event.metaKey || event.ctrlKey) {
+              this.find('button[type=submit]').click();
+            } else if (event.target.value.trim()) {
+              const inputLast = this.find('input.last');
+
+              let items = this.getItems();
+
+              if (event.target === inputLast) {
+                inputLast.value = '';
+              } else if (event.target.nextSibling === inputLast) {
+                inputLast.focus();
+              } else {
+                event.target.blur();
+
+                const idx = Array.from(this.findAll('input')).indexOf(
+                  event.target,
+                );
+                items.splice(idx + 1, 0, '');
+
+                Tracker.afterFlush(() => {
+                  const element = this.findAll('input')[idx + 1];
+                  element.focus();
+                  element.value = '';
+                });
+              }
+
+              this.stringtemplateItems.set(items);
+            }
+          }
+        },
+
+        'blur .js-card-customfield-stringtemplate-item'(event) {
+          if (
+            !event.target.value.trim() ||
+            event.target === this.find('input.last')
+          ) {
+            const items = this.getItems();
+            this.stringtemplateItems.set(items);
+            this.find('input.last').value = '';
+          }
+        },
+
+        'click .js-close-inlined-form'(event) {
+          this.stringtemplateItems.set(this.data().value ?? []);
+        },
+      },
+    ];
+  }
+}.register('cardCustomField-stringtemplate'));

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

@@ -8,3 +8,7 @@ template(name="dateBadge")
       time(datetime="{{showISODate}}")
       time(datetime="{{showISODate}}")
         | {{showDate}}
         | {{showDate}}
 
 
+template(name="dateCustomField")
+  a(title="{{showTitle}}" class="{{classes}}")
+    time(datetime="{{showISODate}}")
+      | {{showDate}}

+ 89 - 94
client/components/cards/cardDate.js

@@ -1,96 +1,4 @@
-// Edit received, start, due & end dates
-BlazeComponent.extendComponent({
-  template() {
-    return 'editCardDate';
-  },
-
-  onCreated() {
-    this.error = new ReactiveVar('');
-    this.card = this.data();
-    this.date = new ReactiveVar(moment.invalid());
-  },
-
-  onRendered() {
-    const $picker = this.$('.js-datepicker')
-      .datepicker({
-        todayHighlight: true,
-        todayBtn: 'linked',
-        language: TAPi18n.getLanguage(),
-      })
-      .on(
-        'changeDate',
-        function(evt) {
-          this.find('#date').value = moment(evt.date).format('L');
-          this.error.set('');
-          this.find('#time').focus();
-        }.bind(this),
-      );
-
-    if (this.date.get().isValid()) {
-      $picker.datepicker('update', this.date.get().toDate());
-    }
-  },
-
-  showDate() {
-    if (this.date.get().isValid()) return this.date.get().format('L');
-    return '';
-  },
-  showTime() {
-    if (this.date.get().isValid()) return this.date.get().format('LT');
-    return '';
-  },
-  dateFormat() {
-    return moment.localeData().longDateFormat('L');
-  },
-  timeFormat() {
-    return moment.localeData().longDateFormat('LT');
-  },
-
-  events() {
-    return [
-      {
-        'keyup .js-date-field'() {
-          // parse for localized date format in strict mode
-          const dateMoment = moment(this.find('#date').value, 'L', true);
-          if (dateMoment.isValid()) {
-            this.error.set('');
-            this.$('.js-datepicker').datepicker('update', dateMoment.toDate());
-          }
-        },
-        'keyup .js-time-field'() {
-          // parse for localized time format in strict mode
-          const dateMoment = moment(this.find('#time').value, 'LT', true);
-          if (dateMoment.isValid()) {
-            this.error.set('');
-          }
-        },
-        'submit .edit-date'(evt) {
-          evt.preventDefault();
-
-          // if no time was given, init with 12:00
-          const time =
-            evt.target.time.value ||
-            moment(new Date().setHours(12, 0, 0)).format('LT');
-
-          const dateString = `${evt.target.date.value} ${time}`;
-          const newDate = moment(dateString, 'L LT', true);
-          if (newDate.isValid()) {
-            this._storeDate(newDate.toDate());
-            Popup.close();
-          } else {
-            this.error.set('invalid-date');
-            evt.target.date.focus();
-          }
-        },
-        'click .js-delete-date'(evt) {
-          evt.preventDefault();
-          this._deleteDate();
-          Popup.close();
-        },
-      },
-    ];
-  },
-});
+import { DatePicker } from '/client/lib/datepicker';
 
 
 Template.dateBadge.helpers({
 Template.dateBadge.helpers({
   canModifyCard() {
   canModifyCard() {
@@ -279,7 +187,7 @@ class CardStartDate extends CardDate {
     // if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
     // if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
     if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
     if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
       classes += 'long-overdue';
       classes += 'long-overdue';
-    else if (theDate.isBefore(now, 'minute')) classes += 'almost-due';
+    else if (theDate.isAfter(now)) classes += '';
     else classes += 'current';
     else classes += 'current';
     return classes;
     return classes;
   }
   }
@@ -363,6 +271,33 @@ class CardEndDate extends CardDate {
 }
 }
 CardEndDate.register('cardEndDate');
 CardEndDate.register('cardEndDate');
 
 
+class CardCustomFieldDate extends CardDate {
+  template() {
+    return 'dateCustomField';
+  }
+
+  onCreated() {
+    super.onCreated();
+    const self = this;
+    self.autorun(() => {
+      self.date.set(moment(self.data().value));
+    });
+  }
+
+  classes() {
+    return 'customfield-date';
+  }
+
+  showTitle() {
+    return '';
+  }
+
+  events() {
+    return [];
+  }
+}
+CardCustomFieldDate.register('cardCustomFieldDate');
+
 (class extends CardReceivedDate {
 (class extends CardReceivedDate {
   showDate() {
   showDate() {
     return this.date.get().format('l');
     return this.date.get().format('l');
@@ -386,3 +321,63 @@ CardEndDate.register('cardEndDate');
     return this.date.get().format('l');
     return this.date.get().format('l');
   }
   }
 }.register('minicardEndDate'));
 }.register('minicardEndDate'));
+
+(class extends CardCustomFieldDate {
+  showDate() {
+    return this.date.get().format('l');
+  }
+}.register('minicardCustomFieldDate'));
+
+class VoteEndDate extends CardDate {
+  onCreated() {
+    super.onCreated();
+    const self = this;
+    self.autorun(() => {
+      self.date.set(moment(self.data().getVoteEnd()));
+    });
+  }
+  classes() {
+    const classes = 'end-date' + ' ';
+    return classes;
+  }
+  showDate() {
+    return this.date.get().format('l LT');
+  }
+  showTitle() {
+    return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
+  }
+
+  events() {
+    return super.events().concat({
+      'click .js-edit-date': Popup.open('editVoteEndDate'),
+    });
+  }
+}
+VoteEndDate.register('voteEndDate');
+
+class PokerEndDate extends CardDate {
+  onCreated() {
+    super.onCreated();
+    const self = this;
+    self.autorun(() => {
+      self.date.set(moment(self.data().getPokerEnd()));
+    });
+  }
+  classes() {
+    const classes = 'end-date' + ' ';
+    return classes;
+  }
+  showDate() {
+    return this.date.get().format('l LT');
+  }
+  showTitle() {
+    return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
+  }
+
+  events() {
+    return super.events().concat({
+      'click .js-edit-date': Popup.open('editPokerEndDate'),
+    });
+  }
+}
+PokerEndDate.register('pokerEndDate');

+ 9 - 5
client/components/cards/cardDate.styl

@@ -2,11 +2,11 @@
   display: block
   display: block
   border-radius: 4px
   border-radius: 4px
   padding: 1px 3px
   padding: 1px 3px
-  
+
   background-color: #dbdbdb
   background-color: #dbdbdb
   &:hover, &.is-active
   &:hover, &.is-active
     background-color: #b3b3b3
     background-color: #b3b3b3
-  
+
   &.current, &.almost-due, &.due, &.long-overdue
   &.current, &.almost-due, &.due, &.long-overdue
     color: #fff
     color: #fff
 
 
@@ -14,17 +14,17 @@
     background-color: #5ba639
     background-color: #5ba639
     &:hover, &.is-active
     &:hover, &.is-active
       background-color: darken(#5ba639, 10)
       background-color: darken(#5ba639, 10)
-  
+
   &.almost-due
   &.almost-due
     background-color: #edc909
     background-color: #edc909
     &:hover, &.is-active
     &:hover, &.is-active
       background-color: darken(#edc909, 10)
       background-color: darken(#edc909, 10)
-  
+
   &.due
   &.due
     background-color: #fa3f00
     background-color: #fa3f00
     &:hover, &.is-active
     &:hover, &.is-active
       background-color: darken(#fa3f00, 10)
       background-color: darken(#fa3f00, 10)
-  
+
   &.long-overdue
   &.long-overdue
     background-color: #fd5d47
     background-color: #fd5d47
     &:hover, &.is-active
     &:hover, &.is-active
@@ -57,3 +57,7 @@
       -webkit-font-smoothing: antialiased
       -webkit-font-smoothing: antialiased
       margin-right: 0.3em
       margin-right: 0.3em
 
 
+.customfield-date
+  display: block
+  border-radius: 4px
+  padding: 1px 3px

+ 7 - 0
client/components/cards/cardDescription.jade

@@ -0,0 +1,7 @@
+
+template(name="descriptionForm")
+  .new-description.js-new-description(
+    class="{{#if descriptionFormIsOpen}}is-open{{/if}}")
+    form.js-new-description-form
+      +editor(class="js-new-description-input" autofocus="autofocus")
+        | {{getUnsavedValue 'cardDescription' _id getDescription}}

+ 34 - 0
client/components/cards/cardDescription.js

@@ -0,0 +1,34 @@
+const descriptionFormIsOpen = new ReactiveVar(false);
+
+BlazeComponent.extendComponent({
+  onDestroyed() {
+    descriptionFormIsOpen.set(false);
+    $('.note-popover').hide();
+  },
+
+  descriptionFormIsOpen() {
+    return descriptionFormIsOpen.get();
+  },
+
+  getInput() {
+    return this.$('.js-new-description-input');
+  },
+
+  events() {
+    return [
+      {
+        'submit .js-card-description'(event) {
+          event.preventDefault();
+          const description = this.currentComponent().getValue();
+          this.data().setDescription(description);
+        },
+        // 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();
+          }
+        },
+      },
+    ];
+  },
+}).register('descriptionForm');

+ 59 - 0
client/components/cards/cardDescription.styl

@@ -0,0 +1,59 @@
+@import 'nib'
+
+.new-description
+  position: relative
+  margin: 0 0 20px 0
+
+
+  &.is-open
+    .helper
+      display: inline-block
+
+    textarea
+      min-height: 100px
+      color: #4d4d4d
+      cursor: auto
+      overflow: hidden
+      word-wrap: break-word
+
+  .too-long
+    margin-top: 8px
+
+  textarea
+    background-color: #fff
+    border: 0
+    box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
+    height: 36px
+    margin: 4px 4px 6px 0
+    padding: 9px 11px
+    width: 100%
+
+    &:hover,
+    &:is-open
+      background-color: #fff
+      box-shadow: 0 1px 3px rgba(0, 0, 0, .33)
+      border: 0
+      cursor: pointer
+
+    &:is-open
+      cursor: auto
+
+.description-item
+  background-color: #fff
+  border: 0
+  box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
+  color: #8c8c8c
+  height: 36px
+  margin: 4px 4px 6px 0
+  width: 92%
+
+  &:hover
+    background: darken(white, 12%)
+
+  &.add-description
+    display: flex
+    margin: 5px
+
+    a
+      display: block
+      margin: auto

+ 650 - 299
client/components/cards/cardDetails.jade

@@ -1,23 +1,26 @@
 template(name="cardDetails")
 template(name="cardDetails")
-  section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
+  section.card-details.js-card-details(class='{{#if cardMaximized}}card-details-maximized{{/if}}'): .card-details-canvas
     .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
     .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
       +inlinedForm(classNames="js-card-details-title")
       +inlinedForm(classNames="js-card-details-title")
         +editCardTitleForm
         +editCardTitleForm
       else
       else
         unless isMiniScreen
         unless isMiniScreen
-          a.fa.fa-times-thin.close-card-details.js-close-card-details
+          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'}}")
           if currentUser.isBoardMember
           if currentUser.isBoardMember
-            a.fa.fa-navicon.card-details-menu.js-open-card-details-menu
-            input.inline-input(type="text" id="cardURL_copy" value="{{ absoluteUrl }}")
+            a.fa.fa-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(
             a.fa.fa-link.card-copy-button.js-copy-link(
               class="fa-link"
               class="fa-link"
               title="{{_ 'copy-card-link-to-clipboard'}}"
               title="{{_ 'copy-card-link-to-clipboard'}}"
-              value="{{ absoluteUrl }}"
             )
             )
         if isMiniScreen
         if isMiniScreen
-          a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details
+          a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details(title="{{_ 'close-card'}}")
           if currentUser.isBoardMember
           if currentUser.isBoardMember
-            a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu
+            a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
             a.fa.fa-link.card-copy-mobile-button
             a.fa.fa-link.card-copy-mobile-button
         h2.card-details-title.js-card-title(
         h2.card-details-title.js-card-title(
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
@@ -32,7 +35,7 @@ template(name="cardDetails")
           // else
           // else
             {{_ 'top-level-card'}}
             {{_ 'top-level-card'}}
         if isLinkedCard
         if isLinkedCard
-          h3.linked-card-location
+          a.linked-card-location.js-go-to-linked-card
             +viewer
             +viewer
               | {{getBoardTitle}} > {{getTitle}}
               | {{getBoardTitle}} > {{getTitle}}
 
 
@@ -42,242 +45,501 @@ template(name="cardDetails")
       else
       else
         p.warning {{_ 'card-archived'}}
         p.warning {{_ 'card-archived'}}
 
 
-    .card-details-items
-      if currentBoard.allowsReceivedDate
-        .card-details-item.card-details-item-received
-          h3
-            i.fa.fa-sign-out
-            card-details-item-title {{_ 'card-received'}}
-          if getReceived
-            +cardReceivedDate
-          else
+    .card-details-left
+
+      .card-details-items
+        if currentBoard.allowsLabels
+          .card-details-item.card-details-item-labels
+            h3.card-details-item-title
+              i.fa.fa-tags
+              | {{_ 'labels'}}
+            a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
+              each labels
+                span.card-label(class="card-label-{{color}}" title=name)
+                  +viewer
+                    = name
             if canModifyCard
             if canModifyCard
               unless currentUser.isWorker
               unless currentUser.isWorker
-                a.card-label.add-label.js-received-date
+                a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
                   i.fa.fa-plus
                   i.fa.fa-plus
 
 
-      if currentBoard.allowsStartDate
-        .card-details-item.card-details-item-start
-          h3
-            i.fa.fa-hourglass-start
-            card-details-item-title {{_ 'card-start'}}
-          if getStart
-            +cardStartDate
-          else
+        if currentBoard.allowsReceivedDate
+          hr
+          .card-details-item.card-details-item-received
+            h3.card-details-item-title
+              i.fa.fa-sign-out
+              | {{_ 'card-received'}}
+            if getReceived
+              +cardReceivedDate
+            else
+              if canModifyCard
+                unless currentUser.isWorker
+                  a.card-label.add-label.js-received-date
+                    i.fa.fa-plus
+
+        if currentBoard.allowsStartDate
+          .card-details-item.card-details-item-start
+            h3.card-details-item-title
+              i.fa.fa-hourglass-start
+              | {{_ 'card-start'}}
+            if getStart
+              +cardStartDate
+            else
+              if canModifyCard
+                unless currentUser.isWorker
+                  a.card-label.add-label.js-start-date
+                    i.fa.fa-plus
+
+        if currentBoard.allowsDueDate
+          .card-details-item.card-details-item-due
+            h3.card-details-item-title
+              i.fa.fa-sign-in
+              | {{_ 'card-due'}}
+            if getDue
+              +cardDueDate
+            else
+              if canModifyCard
+                unless currentUser.isWorker
+                  a.card-label.add-label.js-due-date
+                    i.fa.fa-plus
+
+        if currentBoard.allowsEndDate
+          .card-details-item.card-details-item-end
+            h3.card-details-item-title
+              i.fa.fa-hourglass-end
+              | {{_ 'card-end'}}
+            if getEnd
+              +cardEndDate
+            else
+              if canModifyCard
+                unless currentUser.isWorker
+                  a.card-label.add-label.js-end-date
+                    i.fa.fa-plus
+
+        hr
+        if currentBoard.allowsCreator
+          .card-details-item.card-details-item-creator
+            h3.card-details-item-title
+              i.fa.fa-user
+              | {{_ 'creator'}}
+
+            +userAvatar(userId=userId noRemove=true)
+            | {{! XXX Hack to hide syntaxic coloration /// }}
+
+        //.card-details-items
+        if currentBoard.allowsMembers
+          .card-details-item.card-details-item-members
+            h3.card-details-item-title
+              i.fa.fa-users
+              | {{_ 'members'}}
+            each userId in getMembers
+              +userAvatar(userId=userId cardId=_id)
+              | {{! XXX Hack to hide syntaxic coloration /// }}
             if canModifyCard
             if canModifyCard
               unless currentUser.isWorker
               unless currentUser.isWorker
-                a.card-label.add-label.js-start-date
+                a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
                   i.fa.fa-plus
                   i.fa.fa-plus
 
 
-      if currentBoard.allowsDueDate
-        .card-details-item.card-details-item-due
-          h3
-            i.fa.fa-sign-in
-            card-details-item-title {{_ 'card-due'}}
-          if getDue
-            +cardDueDate
-          else
+        //if assigneeSelected
+        if currentBoard.allowsAssignee
+          .card-details-item.card-details-item-assignees
+            h3.card-details-item-title
+              i.fa.fa-user
+              | {{_ 'assignee'}}
+            each userId in getAssignees
+              +userAvatar(userId=userId cardId=_id assignee=true)
+              | {{! XXX Hack to hide syntaxic coloration /// }}
             if canModifyCard
             if canModifyCard
-              unless currentUser.isWorker
-                a.card-label.add-label.js-due-date
+              a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
+                i.fa.fa-plus
+            if currentUser.isWorker
+              unless assigneeSelected
+                a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
                   i.fa.fa-plus
                   i.fa.fa-plus
 
 
-      if currentBoard.allowsEndDate
-        .card-details-item.card-details-item-end
-          h3
-            i.fa.fa-hourglass-end
-            card-details-item-title {{_ 'card-end'}}
-          if getEnd
-            +cardEndDate
-          else
+        //.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
+            h3.card-details-item-title
+              i.fa.fa-shopping-cart
+              | {{_ 'requested-by'}}
             if canModifyCard
             if canModifyCard
               unless currentUser.isWorker
               unless currentUser.isWorker
-                a.card-label.add-label.js-end-date
-                  i.fa.fa-plus
-
-      //.card-details-items
-      if currentBoard.allowsMembers
-        .card-details-item.card-details-item-members
-          h3
-            i.fa.fa-users
-            card-details-item-title {{_ 'members'}}
-          each getMembers
-            +userAvatar(userId=this cardId=../_id)
-            | {{! XXX Hack to hide syntaxic coloration /// }}
-          if canModifyCard
-            unless currentUser.isWorker
-              a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
-                i.fa.fa-plus
-
-      //if assigneeSelected
-      if currentBoard.allowsAssignee
-        .card-details-item.card-details-item-assignees
-          h3
-            i.fa.fa-user
-            card-details-item-title {{_ 'assignee'}}
-          each getAssignees
-            +userAvatarAssignee(userId=this cardId=../_id)
-            | {{! XXX Hack to hide syntaxic coloration /// }}
-          if canModifyCard
-            a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
-              i.fa.fa-plus
-          if currentUser.isWorker
-            unless assigneeSelected
-              a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
-                i.fa.fa-plus
+                +inlinedForm(classNames="js-card-details-requester")
+                  +editCardRequesterForm
+                else
+                  a.js-open-inlined-form
+                    if getRequestedBy
+                      +viewer
+                        = getRequestedBy
+                    else
+                      | {{_ 'add'}}
+            else if getRequestedBy
+              +viewer
+                = getRequestedBy
+
+        if currentBoard.allowsAssignedBy
+          .card-details-item.card-details-item-name
+            h3.card-details-item-title
+              i.fa.fa-user-plus
+              | {{_ 'assigned-by'}}
+            if canModifyCard
+              unless currentUser.isWorker
+                +inlinedForm(classNames="js-card-details-assigner")
+                  +editCardAssignerForm
+                else
+                  a.js-open-inlined-form
+                    if getAssignedBy
+                      +viewer
+                        = getAssignedBy
+                    else
+                      | {{_ 'add'}}
+            else if getRequestedBy
+              +viewer
+                = getAssignedBy
+
+        if currentBoard.allowsCardSortingByNumber
+          .card-details-item.card-details-sort-order
+            h3.card-details-item-title
+              i.fa.fa-sort
+              | {{_ 'sort'}}
+            if canModifyCard
+              +inlinedForm(classNames="js-card-details-sort")
+                +editCardSortOrderForm
+              else
+                a.js-open-inlined-form
+                  +viewer
+                    = sort
 
 
-      if currentBoard.allowsLabels
-        .card-details-item.card-details-item-labels
-          h3
-            i.fa.fa-tags
-            card-details-item-title {{_ 'labels'}}
-          a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
-            each labels
-              span.card-label(class="card-label-{{color}}" title=name)
-                +viewer
-                  = name
-          if canModifyCard
-            unless currentUser.isWorker
-              a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
+        //.card-details-items
+        if customFieldsWD
+          hr
+          each customFieldsWD
+            .card-details-item.card-details-item-customfield
+              h3.card-details-item-title
+                i.fa.fa-list-alt
+                = definition.name
+              +cardCustomField
+
+      if getVoteQuestion
+        hr
+        .vote-title
+          div.flex
+            h3
+              i.fa.fa-thumbs-up
+              | {{_ 'vote-question'}}
+            if getVoteEnd
+              +voteEndDate
+          .vote-result
+            if votePublic
+              a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }}
+              a.card-label.card-label-red.js-show-negative-votes {{ voteCountNegative }}
+            else
+              .card-label.card-label-green {{ voteCountPositive }}
+              .card-label.card-label-red {{ voteCountNegative }}
+            unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
+              .card-label.card-label-gray  {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
+        +viewer
+          = getVoteQuestion
+        if showVotingButtons
+          button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}")
+            if voteState
+              i.fa.fa-thumbs-up
+            | {{_ 'vote-for-it'}}
+          button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
+            if $eq voteState false
+              i.fa.fa-thumbs-down
+            | {{_ 'vote-against'}}
+
+      if getPokerQuestion
+        hr
+        .poker-title
+          div.flex
+            h3
+              i.fa.fa-thumbs-up
+              | {{_ 'poker-question'}}
+            if getPokerEnd
+              +pokerEndDate
+          div.flex
+            .poker-result
+              if expiredPoker
+                unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
+                  .card-label.card-label-gray  {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
+        if showPlanningPokerButtons
+          .poker-result
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
+              if $eq pokerState "one"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
+              if $eq pokerState "two"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
+              if $eq pokerState "three"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
+              if $eq pokerState "five"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
+              if $eq pokerState "eight"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
+              if $eq pokerState "thirteen"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
+              if $eq pokerState "twenty"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
+              if $eq pokerState "forty"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
+              if $eq pokerState "oneHundred"
+                i.fa.fa-check
+            .poker-deck
+              .poker-card
+                span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
+              if $eq pokerState "unsure"
+                i.fa.fa-check
+
+          if currentUser.isBoardAdmin
+            button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
+
+        if expiredPoker
+          .poker-table
+            .poker-table-side-left
+              .poker-table-heading-left
+                .poker-table-row
+                  .poker-table-cell
+                  .poker-table-cell
+                    | {{_ 'poker-result-votes' }}
+                  .poker-table-cell.poker-table-cell-who
+                    | {{_ 'poker-result-who' }}
+              .poker-table-body
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 1}}winner{{else}}loser{{/if}}") {{_ 'poker-one'}}
+                  .poker-table-cell {{ pokerCountOne }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberOne
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 2}}winner{{else}}loser{{/if}}") {{_ 'poker-two'}}
+                  .poker-table-cell {{ pokerCountTwo }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberTwo
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 3}}winner{{else}}loser{{/if}}") {{_ 'poker-three'}}
+                  .poker-table-cell {{ pokerCountThree }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberThree
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 5}}winner{{else}}loser{{/if}}") {{_ 'poker-five'}}
+                  .poker-table-cell {{ pokerCountFive }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberFive
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 8}}winner{{else}}loser{{/if}}") {{_ 'poker-eight'}}
+                  .poker-table-cell {{ pokerCountEight }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberEight
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+            .poker-table-side-right
+              .poker-table-heading-right
+                .poker-table-row
+                  .poker-table-cell
+                  .poker-table-cell
+                    | {{_ 'poker-result-votes' }}
+                  .poker-table-cell.poker-table-cell-who
+                    | {{_ 'poker-result-who' }}
+              .poker-table-body
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 13}}winner{{else}}loser{{/if}}") {{_ 'poker-thirteen'}}
+                  .poker-table-cell {{ pokerCountThirteen }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberThirteen
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 20}}winner{{else}}loser{{/if}}") {{_ 'poker-twenty'}}
+                  .poker-table-cell {{ pokerCountTwenty }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberTwenty
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 40}}winner{{else}}loser{{/if}}") {{_ 'poker-forty'}}
+                  .poker-table-cell {{ pokerCountForty }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberForty
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 100}}winner{{else}}loser{{/if}}") {{_ 'poker-oneHundred'}}
+                  .poker-table-cell {{ pokerCountOneHundred }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberOneHundred
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+                .poker-table-row
+                  .poker-table-cell
+                    button.card-details-gray.js-poker.poker-card-result(class="{{#if $eq pokerWinner 'unsure'}}winner{{else}}loser{{/if}}") {{_ 'poker-unsure'}}
+                  .poker-table-cell {{ pokerCountUnsure }}
+                  .poker-table-cell.poker-table-cell-who
+                    .poker-result
+                        each m in pokerMemberUnsure
+                          a.name
+                            +userAvatar(userId=m._id noRemove=true)
+
+          if currentUser.isBoardAdmin
+            div.estimation-add
+              button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
+            div.estimation-add
+              button.js-poker-estimation
                 i.fa.fa-plus
                 i.fa.fa-plus
+                | {{_ 'set-estimation'}}
+              input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
 
 
-      //.card-details-items
-      each customFieldsWD
-        .card-details-item.card-details-item-customfield
-          h3.card-details-item-title
-            +viewer
-              = definition.name
-          +cardCustomField
-
-      //.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
-          h3
-            i.fa.fa-shopping-cart
-            card-details-item-title {{_ 'requested-by'}}
-          if canModifyCard
-            unless currentUser.isWorker
-              +inlinedForm(classNames="js-card-details-requester")
-                +editCardRequesterForm
-              else
-                a.js-open-inlined-form
-                  if getRequestedBy
-                    +viewer
-                      = getRequestedBy
-                  else
-                    | {{_ 'add'}}
-          else if getRequestedBy
-            +viewer
-              = getRequestedBy
-
-      if currentBoard.allowsAssignedBy
-        .card-details-item.card-details-item-name
-          h3
-            i.fa.fa-user-plus
-            card-details-item-title {{_ 'assigned-by'}}
-          if canModifyCard
-            unless currentUser.isWorker
-              +inlinedForm(classNames="js-card-details-assigner")
-                +editCardAssignerForm
-              else
+      //- XXX We should use "editable" to avoid repetiting ourselves
+      if canModifyCard
+        unless currentUser.isWorker
+          if currentBoard.allowsDescriptionTitle
+            hr
+            h3.card-details-item-title
+              i.fa.fa-align-left
+              | {{_ 'description'}}
+          if currentBoard.allowsDescriptionText
+            +inlinedCardDescription(classNames="card-description js-card-description")
+              +descriptionForm
+              .edit-controls.clearfix
+                button.primary(type="submit") {{_ 'save'}}
+                a.fa.fa-times-thin.js-close-inlined-form
+            else
+              if currentBoard.allowsDescriptionText
                 a.js-open-inlined-form
                 a.js-open-inlined-form
-                  if getAssignedBy
+                  if getDescription
                     +viewer
                     +viewer
-                      = getAssignedBy
+                      = getDescription
                   else
                   else
-                    | {{_ 'add'}}
-          else if getRequestedBy
-            +viewer
-              = getAssignedBy
-
-    //- XXX We should use "editable" to avoid repetiting ourselves
-    if canModifyCard
-      unless currentUser.isWorker
+                    | {{_ 'edit'}}
+                if (hasUnsavedValue 'cardDescription' _id)
+                  p.quiet
+                    | {{_ 'unsaved-description'}}
+                    a.js-open-inlined-form {{_ 'view-it'}}
+                    = ' - '
+                    a.js-close-inlined-form {{_ 'discard'}}
+      else if getDescription
         if currentBoard.allowsDescriptionTitle
         if currentBoard.allowsDescriptionTitle
           hr
           hr
-          h3
-            i.fa.fa-align-left
-            card-details-item-title {{_ 'description'}}
+          h3.card-details-item-title {{_ 'description'}}
         if currentBoard.allowsDescriptionText
         if currentBoard.allowsDescriptionText
-          +inlinedCardDescription(classNames="card-description js-card-description")
-            +editor(autofocus=true)
-              | {{getUnsavedValue 'cardDescription' _id getDescription}}
-            .edit-controls.clearfix
-              button.primary(type="submit") {{_ 'save'}}
-              a.fa.fa-times-thin.js-close-inlined-form
-          else
-            if currentBoard.allowsDescriptionText
-              a.js-open-inlined-form
-                if getDescription
-                  +viewer
-                    = getDescription
-                else
-                  | {{_ 'edit'}}
-              if (hasUnsavedValue 'cardDescription' _id)
-                p.quiet
-                  | {{_ 'unsaved-description'}}
-                  a.js-open-inlined-form {{_ 'view-it'}}
-                  = ' - '
-                  a.js-close-inlined-form {{_ 'discard'}}
-    else if getDescription
-      if currentBoard.allowsDescriptionTitle
-        hr
-        h3.card-details-item-title {{_ 'description'}}
-      if currentBoard.allowsDescriptionText
-        +viewer
-          = getDescription
-
-    .card-checklist-attachmentGalerys
-      .card-checklist-attachmentGalery.card-checklists
-        if currentBoard.allowsChecklists
-          hr
-          +checklists(cardId = _id)
-        if currentBoard.allowsSubtasks
+          +viewer
+            = getDescription
+
+      .card-checklist-attachmentGalerys
+        .card-checklist-attachmentGalery.card-checklists
+          if currentBoard.allowsChecklists
+            hr
+            +checklists(cardId = _id)
+          if currentBoard.allowsSubtasks
+            hr
+            +subtasks(cardId = _id)
+        if currentBoard.allowsAttachments
           hr
           hr
-          +subtasks(cardId = _id)
-      if currentBoard.allowsAttachments
-        hr
-        h3
-          i.fa.fa-paperclip
-          | {{_ 'attachments'}}
-        .card-checklist-attachmentGalery.card-attachmentGalery
-          +attachmentsGalery
+          h3.card-details-item-title
+            i.fa.fa-paperclip
+            | {{_ 'attachments'}}
+          .card-checklist-attachmentGalery.card-attachmentGalery
+            +attachmentsGalery
 
 
-    hr
-    unless currentUser.isNoComments
-      .activity-title
-        h3
-          i.fa.fa-history
-          | {{ _ 'activity'}}
+    .card-details-right
+
+      unless currentUser.isNoComments
+        .activity-title
+          h3.card-details-item-title
+            i.fa.fa-history
+            | {{ _ 'activity'}}
+          if currentUser.isBoardMember
+            .material-toggle-switch(title="{{_ 'hide-system-messages'}}")
+              //span.toggle-switch-title
+              if hiddenSystemMessages
+                input.toggle-switch(type="checkbox" id="toggleButton" checked="checked")
+              else
+                input.toggle-switch(type="checkbox" id="toggleButton")
+              label.toggle-label(for="toggleButton")
+      if currentBoard.allowsComments
         if currentUser.isBoardMember
         if currentUser.isBoardMember
-          .material-toggle-switch
-            span.toggle-switch-title {{_ 'hide-system-messages'}}
-            if hiddenSystemMessages
-              input.toggle-switch(type="checkbox" id="toggleButton" checked="checked")
-            else
-              input.toggle-switch(type="checkbox" id="toggleButton")
-            label.toggle-label(for="toggleButton")
-    if currentBoard.allowsComments
-      if currentUser.isBoardMember
-        unless currentUser.isNoComments
-          +commentForm
-    unless currentUser.isNoComments
-      if isLoaded.get
-        if isLinkedCard
-          +activities(card=this mode="linkedcard")
-        else if isLinkedBoard
-          +activities(card=this mode="linkedboard")
-        else
-          +activities(card=this mode="card")
+          unless currentUser.isNoComments
+            +commentForm
+      unless currentUser.isNoComments
+        if isLoaded.get
+          if isLinkedCard
+            +activities(card=this mode="linkedcard")
+          else if isLinkedBoard
+            +activities(card=this mode="linkedboard")
+          else
+            +activities(card=this mode="card")
 
 
 template(name="editCardTitleForm")
 template(name="editCardTitleForm")
   textarea.js-edit-card-title(rows='1' autofocus dir="auto")
   textarea.js-edit-card-title(rows='1' autofocus dir="auto")
@@ -298,6 +560,12 @@ template(name="editCardAssignerForm")
     button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
     button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
     a.fa.fa-times-thin.js-close-inlined-form
     a.fa.fa-times-thin.js-close-inlined-form
 
 
+template(name="editCardSortOrderForm")
+  input.js-edit-card-sort(type='text' autofocus value=sort dir="auto")
+  .edit-controls.clearfix
+    button.primary.confirm.js-submit-edit-card-sort-form(type="submit") {{_ 'save'}}
+    a.fa.fa-times-thin.js-close-inlined-form
+
 template(name="cardDetailsActionsPopup")
 template(name="cardDetailsActionsPopup")
   ul.pop-over-list
   ul.pop-over-list
     li
     li
@@ -308,17 +576,26 @@ template(name="cardDetailsActionsPopup")
         else
         else
           i.fa.fa-eye-slash
           i.fa.fa-eye-slash
           |  {{_ 'watch'}}
           |  {{_ 'watch'}}
+  hr
   if canModifyCard
   if canModifyCard
     unless currentUser.isWorker
     unless currentUser.isWorker
-      hr
       ul.pop-over-list
       ul.pop-over-list
         //li: a.js-members {{_ 'card-edit-members'}}
         //li: a.js-members {{_ 'card-edit-members'}}
         //li: a.js-labels {{_ 'card-edit-labels'}}
         //li: a.js-labels {{_ 'card-edit-labels'}}
         //li: a.js-attachments {{_ 'card-edit-attachments'}}
         //li: a.js-attachments {{_ 'card-edit-attachments'}}
         li
         li
-          a.js-custom-fields
-            i.fa.fa-list-alt
-            | {{_ 'card-edit-custom-fields'}}
+          a.js-start-voting
+            i.fa.fa-thumbs-up
+            | {{_ 'card-edit-voting'}}
+        li
+          a.js-start-planning-poker
+            i.fa.fa-thumbs-up
+            | {{_ 'card-edit-planning-poker'}}
+        if currentUser.isBoardAdmin
+          li
+            a.js-custom-fields
+              i.fa.fa-list-alt
+              | {{_ 'card-edit-custom-fields'}}
         //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
         //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
         //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
         //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
         //li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
         //li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
@@ -331,48 +608,63 @@ template(name="cardDetailsActionsPopup")
           a.js-set-card-color
           a.js-set-card-color
             i.fa.fa-paint-brush
             i.fa.fa-paint-brush
             | {{_ 'setCardColorPopup-title'}}
             | {{_ 'setCardColorPopup-title'}}
-      hr
-    ul.pop-over-list
-      li
-        a.js-move-card-to-top
-          i.fa.fa-arrow-up
-          | {{_ 'moveCardToTop-title'}}
+  hr
+  ul.pop-over-list
+    li
+      a.js-export-card
+        i.fa.fa-share-alt
+        | {{_ 'export-card'}}
+  hr
+  ul.pop-over-list
+    li
+      a.js-move-card-to-top
+        i.fa.fa-arrow-up
+        | {{_ 'moveCardToTop-title'}}
+    li
+      a.js-move-card-to-bottom
+        i.fa.fa-arrow-down
+        | {{_ 'moveCardToBottom-title'}}
+  hr
+  ul.pop-over-list
+    if currentUser.isBoardAdmin
       li
       li
-        a.js-move-card-to-bottom
-          i.fa.fa-arrow-down
-          | {{_ 'moveCardToBottom-title'}}
+        a.js-move-card
+          i.fa.fa-arrow-right
+          | {{_ 'moveCardPopup-title'}}
     unless currentUser.isWorker
     unless currentUser.isWorker
+      li
+        a.js-copy-card
+          i.fa.fa-copy
+          | {{_ 'copyCardPopup-title'}}
+  unless currentUser.isWorker
+    hr
+    ul.pop-over-list
+      li
+        a.js-copy-checklist-cards
+          i.fa.fa-list
+          i.fa.fa-copy
+          | {{_ 'copyChecklistToManyCardsPopup-title'}}
+    unless archived
       hr
       hr
       ul.pop-over-list
       ul.pop-over-list
         li
         li
-          a.js-move-card
+          a.js-archive
             i.fa.fa-arrow-right
             i.fa.fa-arrow-right
-            | {{_ 'moveCardPopup-title'}}
-        li
-          a.js-copy-card
-            i.fa.fa-copy
-            | {{_ 'copyCardPopup-title'}}
-      hr
-      ul.pop-over-list
-        li
-          a.js-copy-checklist-cards
-            i.fa.fa-list
-            i.fa.fa-copy
-            | {{_ 'copyChecklistToManyCardsPopup-title'}}
-      unless archived
-        hr
-        ul.pop-over-list
-          li
-            a.js-archive
-              i.fa.fa-arrow-right
-              i.fa.fa-archive
-              | {{_ 'archive-card'}}
-      hr
-      ul.pop-over-list
-        li
-          a.js-more
-            i.fa.fa-link
-            | {{_ 'cardMorePopup-title'}}
+            i.fa.fa-archive
+            | {{_ 'archive-card'}}
+    hr
+    ul.pop-over-list
+      li
+        a.js-more
+          i.fa.fa-link
+          | {{_ 'cardMorePopup-title'}}
+
+template(name="exportCardPopup")
+  ul.pop-over-list
+    li
+      a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
+        i.fa.fa-share-alt
+        | {{_ 'export-card-pdf'}}
 
 
 template(name="moveCardPopup")
 template(name="moveCardPopup")
   +boardsAndLists
   +boardsAndLists
@@ -390,13 +682,14 @@ template(name="copyChecklistToManyCardsPopup")
   +boardsAndLists
   +boardsAndLists
 
 
 template(name="boardsAndLists")
 template(name="boardsAndLists")
-  label {{_ 'boards'}}:
-  select.js-select-boards(autofocus)
-    each boards
-      if $eq _id currentBoard._id
-        option(value="{{_id}}" selected) {{_ 'current'}}
-      else
-        option(value="{{_id}}") {{title}}
+  unless currentUser.isWorker
+    label {{_ 'boards'}}:
+    select.js-select-boards(autofocus)
+      each boards
+        if $eq _id currentBoard._id
+          option(value="{{_id}}" selected) {{_ 'current'}}
+        else
+          option(value="{{_id}}") {{title}}
 
 
   label {{_ 'swimlanes'}}:
   label {{_ 'swimlanes'}}:
   select.js-select-swimlanes
   select.js-select-swimlanes
@@ -437,31 +730,14 @@ template(name="cardAssigneesPopup")
               i.fa.fa-check
               i.fa.fa-check
   if currentUser.isWorker
   if currentUser.isWorker
     ul.pop-over-list.js-card-assignee-list
     ul.pop-over-list.js-card-assignee-list
-        li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
-          a.name.js-select-assignee(href="#")
-            +userAvatar(userId=currentUser._id)
-            span.full-name
-              = currentUser.profile.fullname
-              | (<span class="username">{{ currentUser.username }}</span>)
-            if currentUser.isCardAssignee
-              i.fa.fa-check
-
-template(name="userAvatarAssignee")
-  a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
-    if userData.profile.avatarUrl
-      img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}")
-    else
-      +userAvatarAssigneeInitials(userId=userData._id)
-
-    if showStatus
-      span.assignee-presence-status(class=presenceStatusClassName)
-      span.member-type(class=memberType)
-
-    unless isSandstorm
-      if showEdit
-        if $eq currentUser._id userData._id
-          a.edit-avatar.js-change-avatar
-            i.fa.fa-pencil
+      li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
+        a.name.js-select-assignee(href="#")
+          +userAvatar(userId=currentUser._id)
+          span.full-name
+            = currentUser.profile.fullname
+            | (<span class="username">{{ currentUser.username }}</span>)
+          if currentUser.isCardAssignee
+            i.fa.fa-check
 
 
 template(name="cardAssigneePopup")
 template(name="cardAssigneePopup")
   .board-assignee-menu
   .board-assignee-menu
@@ -480,18 +756,14 @@ template(name="cardAssigneePopup")
           with currentUser
           with currentUser
             li: a.js-edit-profile {{_ 'edit-profile'}}
             li: a.js-edit-profile {{_ 'edit-profile'}}
 
 
-template(name="userAvatarAssigneeInitials")
-  svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15")
-    text(x="50%" y="13" text-anchor="middle")= initials
-
 template(name="cardMorePopup")
 template(name="cardMorePopup")
   p.quiet
   p.quiet
     span.clearfix
     span.clearfix
       span {{_ 'link-card'}}
       span {{_ 'link-card'}}
       = ' '
       = ' '
       i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
       i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
-      input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}" autofocus="autofocus")
-      button.js-copy-card-link-to-clipboard(class="btn") {{_ 'copy-card-link-to-clipboard'}}
+      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'}}
     span.clearfix
     span.clearfix
     br
     br
     h2 {{_ 'change-card-parent'}}
     h2 {{_ 'change-card-parent'}}
@@ -521,7 +793,8 @@ template(name="cardMorePopup")
     br
     br
     | {{_ 'added'}}
     | {{_ 'added'}}
     span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
     span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
-    a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
+    if currentUser.isBoardAdmin
+      a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
 
 
 template(name="setCardColorPopup")
 template(name="setCardColorPopup")
   form.edit-label
   form.edit-label
@@ -536,5 +809,83 @@ template(name="setCardColorPopup")
 template(name="cardDeletePopup")
 template(name="cardDeletePopup")
   p {{_ "card-delete-pop"}}
   p {{_ "card-delete-pop"}}
   unless archived
   unless archived
-   p {{_ "card-delete-suggest-archive"}}
+    p {{_ "card-delete-suggest-archive"}}
+  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="deleteVotePopup")
+  p {{_ "vote-delete-pop"}}
   button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
   button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="cardStartVotingPopup")
+  form.edit-vote-question
+    .fields
+      label(for="vote") {{_ 'vote-question'}}
+      input.js-vote-field#vote(type="text" name="vote" value="{{getVoteQuestion}}" autofocus disabled="{{#if getVoteQuestion}}disabled{{/if}}")
+      .check-div
+        a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-allow-non-members{{/if}}")
+          .materialCheckBox#vote-allow-non-members(name="vote-allow-non-members" class="{{#if voteAllowNonBoardMembers}}is-checked{{/if}}")
+          span {{_ 'allowNonBoardMembers'}}
+      .check-div
+        a.flex(class="{{#if getVoteQuestion}}is-disabled{{else}}js-toggle-vote-public{{/if}}")
+          .materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
+          span {{_ 'vote-public'}}
+      .check-div.flex
+        i.fa.fa-hourglass-end
+        a.js-end-date
+          span
+            | {{_ 'card-end'}}
+            unless getVoteEnd
+              i.fa.fa-plus
+        if getVoteEnd
+          +voteEndDate
+
+    button.primary.js-submit {{_ 'save'}}
+    if getVoteQuestion
+      if currentUser.isBoardAdmin
+        button.js-remove-vote.negate.wide.right {{_ 'delete'}}
+
+template(name="positiveVoteMembersPopup")
+  ul.pop-over-list.js-card-member-list
+    each m in voteMemberPositive
+      li.item
+        a.name
+          +userAvatar(userId=m._id)
+          span.full-name
+            = m.profile.fullname
+            | (<span class="username">{{ m.username }}</span>)
+
+template(name="negativeVoteMembersPopup")
+  ul.pop-over-list.js-card-member-list
+    each m in voteMemberNegative
+      li.item
+        a.name
+          +userAvatar(userId=m._id)
+          span.full-name
+            = m.profile.fullname
+            | (<span class="username">{{ m.username }}</span>)
+
+template(name="deletePokerPopup")
+  p {{_ "poker-delete-pop"}}
+  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+
+template(name="cardStartPlanningPokerPopup")
+  form.edit-poker-question
+    .fields
+      .check-div
+        a.flex(class="{{#if getPokerQuestion}}is-disabled{{else}}js-toggle-poker-allow-non-members{{/if}}")
+          .materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
+          span {{_ 'allowNonBoardMembers'}}
+      .check-div.flex
+        i.fa.fa-hourglass-end
+        a.js-end-date
+          span
+            | {{_ 'card-end'}}
+            unless getPokerEnd
+              i.fa.fa-plus
+        if getPokerEnd
+          +pokerEndDate
+
+    button.primary.js-submit {{_ 'save'}}
+    if getPokerQuestion
+      if currentUser.isBoardAdmin
+        button.js-remove-poker.negate.wide.right {{_ 'delete'}}

+ 779 - 184
client/components/cards/cardDetails.js

@@ -1,14 +1,21 @@
-const subManager = new SubsManager();
-const { calculateIndexData, enableClickOnTouch } = Utils;
+import { DatePicker } from '/client/lib/datepicker';
+import Cards from '/models/cards';
+import Boards from '/models/boards';
+import Checklists from '/models/checklists';
+import Integrations from '/models/integrations';
+import Users from '/models/users';
+import Lists from '/models/lists';
+import CardComments from '/models/cardComments';
+import { ALLOWED_COLORS } from '/config/const';
+import moment from 'moment';
+import { UserAvatar } from '../users/userAvatar';
 
 
-let cardColors;
-Meteor.startup(() => {
-  cardColors = Cards.simpleSchema()._schema.color.allowedValues;
-});
+const subManager = new SubsManager();
+const { calculateIndexData } = Utils;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   mixins() {
   mixins() {
-    return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
+    return [Mixins.InfiniteScrolling];
   },
   },
 
 
   calculateNextPeak() {
   calculateNextPeak() {
@@ -47,6 +54,10 @@ BlazeComponent.extendComponent({
     return Meteor.user().hasHiddenSystemMessages();
     return Meteor.user().hasHiddenSystemMessages();
   },
   },
 
 
+  cardMaximized() {
+    return Meteor.user().hasCardMaximized();
+  },
+
   canModifyCard() {
   canModifyCard() {
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
@@ -57,12 +68,19 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   scrollParentContainer() {
   scrollParentContainer() {
-    const cardPanelWidth = 510;
-    const bodyBoardComponent = this.parentComponent().parentComponent();
+    const cardPanelWidth = 600;
+    const parentComponent = this.parentComponent();
+    // TODO sometimes parentComponent is not available, maybe because it's not
+    // yet created?!
+    if (!parentComponent) return;
+    const bodyBoardComponent = parentComponent.parentComponent();
     //On Mobile View Parent is Board, Not Board Body. I cant see how this funciton should work then.
     //On Mobile View Parent is Board, Not Board Body. I cant see how this funciton should work then.
     if (bodyBoardComponent === null) return;
     if (bodyBoardComponent === null) return;
     const $cardView = this.$(this.firstNode());
     const $cardView = this.$(this.firstNode());
     const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
     const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
+    // TODO sometimes cardContainer is not available, maybe because it's not yet
+    // created?!
+    if (!$cardContainer) return;
     const cardContainerScroll = $cardContainer.scrollLeft();
     const cardContainerScroll = $cardContainer.scrollLeft();
     const cardContainerWidth = $cardContainer.width();
     const cardContainerWidth = $cardContainer.width();
 
 
@@ -107,7 +125,7 @@ BlazeComponent.extendComponent({
     if (card) {
     if (card) {
       const board = Boards.findOne(card.boardId);
       const board = Boards.findOne(card.boardId);
       if (board) {
       if (board) {
-        result = FlowRouter.url('card', {
+        result = FlowRouter.path('card', {
           boardId: card.boardId,
           boardId: card.boardId,
           slug: board.slug,
           slug: board.slug,
           cardId: card._id,
           cardId: card._id,
@@ -117,6 +135,24 @@ BlazeComponent.extendComponent({
     return result;
     return result;
   },
   },
 
 
+  showVotingButtons() {
+    const card = this.currentData();
+    return (
+      (currentUser.isBoardMember() ||
+        (currentUser && card.voteAllowNonBoardMembers())) &&
+      !card.expiredVote()
+    );
+  },
+
+  showPlanningPokerButtons() {
+    const card = this.currentData();
+    return (
+      (currentUser.isBoardMember() ||
+        (currentUser && card.pokerAllowNonBoardMembers())) &&
+      !card.expiredPoker()
+    );
+  },
+
   onRendered() {
   onRendered() {
     if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
     if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
       // Send Webhook but not create Activities records ---
       // Send Webhook but not create Activities records ---
@@ -138,15 +174,13 @@ BlazeComponent.extendComponent({
       }).fetch();
       }).fetch();
 
 
       if (integrations.length > 0) {
       if (integrations.length > 0) {
-        integrations.forEach(integration => {
+        integrations.forEach((integration) => {
           Meteor.call(
           Meteor.call(
             'outgoingWebhooks',
             'outgoingWebhooks',
             integration,
             integration,
             'CardSelected',
             'CardSelected',
             params,
             params,
-            () => {
-              return;
-            },
+            () => {},
           );
           );
         });
         });
       }
       }
@@ -155,13 +189,6 @@ BlazeComponent.extendComponent({
 
 
     if (!Utils.isMiniScreen()) {
     if (!Utils.isMiniScreen()) {
       Meteor.setTimeout(() => {
       Meteor.setTimeout(() => {
-        $('.card-details').mCustomScrollbar({
-          theme: 'minimal-dark',
-          setWidth: false,
-          setLeft: 0,
-          scrollbarPosition: 'outside',
-          mouseWheel: true,
-        });
         this.scrollParentContainer();
         this.scrollParentContainer();
       }, 500);
       }, 500);
     }
     }
@@ -200,9 +227,6 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.card-checklist-items .js-checklist');
-
     const $subtasksDom = this.$('.card-subtasks-items');
     const $subtasksDom = this.$('.card-subtasks-items');
 
 
     $subtasksDom.sortable({
     $subtasksDom.sortable({
@@ -238,26 +262,24 @@ BlazeComponent.extendComponent({
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.card-subtasks-items .js-subtasks');
-
     function userIsMember() {
     function userIsMember() {
       return Meteor.user() && Meteor.user().isBoardMember();
       return Meteor.user() && Meteor.user().isBoardMember();
     }
     }
 
 
     // Disable sorting if the current user is not a board member
     // Disable sorting if the current user is not a board member
     this.autorun(() => {
     this.autorun(() => {
-      if ($checklistsDom.data('sortable')) {
-        $checklistsDom.sortable('option', 'disabled', !userIsMember());
-      }
-      if ($subtasksDom.data('sortable')) {
-        $subtasksDom.sortable('option', 'disabled', !userIsMember());
-      }
-      if ($checklistsDom.data('sortable')) {
-        $checklistsDom.sortable('option', 'disabled', Utils.isMiniScreen());
+      const disabled = !userIsMember();
+      if (
+        $checklistsDom.data('uiSortable') ||
+        $checklistsDom.data('sortable')
+      ) {
+        $checklistsDom.sortable('option', 'disabled', disabled);
+        if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
+          $checklistsDom.sortable({ handle: '.checklist-handle' });
+        }
       }
       }
-      if ($subtasksDom.data('sortable')) {
-        $subtasksDom.sortable('option', 'disabled', Utils.isMiniScreen());
+      if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
+        $subtasksDom.sortable('option', 'disabled', disabled);
       }
       }
     });
     });
   },
   },
@@ -286,7 +308,9 @@ BlazeComponent.extendComponent({
           Utils.goBoardId(this.data().boardId);
           Utils.goBoardId(this.data().boardId);
         },
         },
         'click .js-copy-link'() {
         'click .js-copy-link'() {
-          StringToCopyElement = document.getElementById('cardURL_copy');
+          const StringToCopyElement = document.getElementById('cardURL_copy');
+          StringToCopyElement.value =
+            window.location.origin + window.location.pathname;
           StringToCopyElement.select();
           StringToCopyElement.select();
           if (document.execCommand('copy')) {
           if (document.execCommand('copy')) {
             StringToCopyElement.blur();
             StringToCopyElement.blur();
@@ -316,9 +340,7 @@ BlazeComponent.extendComponent({
         },
         },
         'submit .js-card-details-title'(event) {
         'submit .js-card-details-title'(event) {
           event.preventDefault();
           event.preventDefault();
-          const title = this.currentComponent()
-            .getValue()
-            .trim();
+          const title = this.currentComponent().getValue().trim();
           if (title) {
           if (title) {
             this.data().setTitle(title);
             this.data().setTitle(title);
           } else {
           } else {
@@ -327,9 +349,7 @@ BlazeComponent.extendComponent({
         },
         },
         'submit .js-card-details-assigner'(event) {
         'submit .js-card-details-assigner'(event) {
           event.preventDefault();
           event.preventDefault();
-          const assigner = this.currentComponent()
-            .getValue()
-            .trim();
+          const assigner = this.currentComponent().getValue().trim();
           if (assigner) {
           if (assigner) {
             this.data().setAssignedBy(assigner);
             this.data().setAssignedBy(assigner);
           } else {
           } else {
@@ -338,15 +358,26 @@ BlazeComponent.extendComponent({
         },
         },
         'submit .js-card-details-requester'(event) {
         'submit .js-card-details-requester'(event) {
           event.preventDefault();
           event.preventDefault();
-          const requester = this.currentComponent()
-            .getValue()
-            .trim();
+          const requester = this.currentComponent().getValue().trim();
           if (requester) {
           if (requester) {
             this.data().setRequestedBy(requester);
             this.data().setRequestedBy(requester);
           } else {
           } else {
             this.data().setRequestedBy('');
             this.data().setRequestedBy('');
           }
           }
         },
         },
+        'submit .js-card-details-sort'(event) {
+          event.preventDefault();
+          const sort = parseFloat(this.currentComponent()
+            .getValue()
+            .trim());
+          if (!Number.isNaN(sort)) {
+            let card = this.data();
+            card.move(card.boardId, card.swimlaneId, card.listId, sort);
+          }
+        },
+        'click .js-go-to-linked-card'() {
+          Utils.goCardId(this.data().linkedId);
+        },
         'click .js-member': Popup.open('cardMember'),
         'click .js-member': Popup.open('cardMember'),
         'click .js-add-members': Popup.open('cardMembers'),
         'click .js-add-members': Popup.open('cardMembers'),
         'click .js-assignee': Popup.open('cardAssignee'),
         'click .js-assignee': Popup.open('cardAssignee'),
@@ -356,6 +387,8 @@ BlazeComponent.extendComponent({
         'click .js-start-date': Popup.open('editCardStartDate'),
         'click .js-start-date': Popup.open('editCardStartDate'),
         'click .js-due-date': Popup.open('editCardDueDate'),
         'click .js-due-date': Popup.open('editCardDueDate'),
         'click .js-end-date': Popup.open('editCardEndDate'),
         'click .js-end-date': Popup.open('editCardEndDate'),
+        'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
+        'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
         'mouseenter .js-card-details'() {
         'mouseenter .js-card-details'() {
           const parentComponent = this.parentComponent().parentComponent();
           const parentComponent = this.parentComponent().parentComponent();
           //on mobile view parent is Board, not BoardBody.
           //on mobile view parent is Board, not BoardBody.
@@ -379,125 +412,144 @@ BlazeComponent.extendComponent({
         'click #toggleButton'() {
         'click #toggleButton'() {
           Meteor.call('toggleSystemMessages');
           Meteor.call('toggleSystemMessages');
         },
         },
-      },
-    ];
-  },
-}).register('cardDetails');
-
-Template.cardDetails.helpers({
-  userData() {
-    // We need to handle a special case for the search results provided by the
-    // `matteodem:easy-search` package. Since these results gets published in a
-    // separate collection, and not in the standard Meteor.Users collection as
-    // expected, we use a component parameter ("property") to distinguish the
-    // two cases.
-    const userCollection = this.esSearch ? ESSearchResults : Users;
-    return userCollection.findOne(this.userId, {
-      fields: {
-        profile: 1,
-        username: 1,
-      },
-    });
-  },
-
-  receivedSelected() {
-    if (this.getReceived().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
-
-  startSelected() {
-    if (this.getStart().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
-
-  endSelected() {
-    if (this.getEnd().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
-
-  dueSelected() {
-    if (this.getDue().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
-
-  memberSelected() {
-    if (this.getMembers().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
-
-  labelSelected() {
-    if (this.getLabels().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
-
-  assigneeSelected() {
-    if (this.getAssignees().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
+        'click .js-maximize-card-details'() {
+          Meteor.call('toggleCardMaximized');
+          autosize($('.card-details'));
+        },
+        'click .js-minimize-card-details'() {
+          Meteor.call('toggleCardMaximized');
+          autosize($('.card-details'));
+        },
+        'click .js-vote'(e) {
+          const forIt = $(e.target).hasClass('js-vote-positive');
+          let newState = null;
+          if (
+            this.data().voteState() === null ||
+            (this.data().voteState() === false && forIt) ||
+            (this.data().voteState() === true && !forIt)
+          ) {
+            newState = forIt;
+          }
+          this.data().setVote(Meteor.userId(), newState);
+        },
+        'click .js-poker'(e) {
+          let newState = null;
+          if ($(e.target).hasClass('js-poker-vote-one')) {
+            newState = 'one';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-two')) {
+            newState = 'two';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-three')) {
+            newState = 'three';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-five')) {
+            newState = 'five';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-eight')) {
+            newState = 'eight';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-thirteen')) {
+            newState = 'thirteen';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-twenty')) {
+            newState = 'twenty';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-forty')) {
+            newState = 'forty';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
+            newState = 'oneHundred';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+          if ($(e.target).hasClass('js-poker-vote-unsure')) {
+            newState = 'unsure';
+            this.data().setPoker(Meteor.userId(), newState);
+          }
+        },
+        'click .js-poker-finish'(e) {
+          if ($(e.target).hasClass('js-poker-finish')) {
+            e.preventDefault();
+            const now = moment().format('YYYY-MM-DD HH:mm');
+            this.data().setPokerEnd(now);
+          }
+        },
 
 
-  requestBySelected() {
-    if (this.getRequestBy().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
+        'click .js-poker-replay'(e) {
+          if ($(e.target).hasClass('js-poker-replay')) {
+            e.preventDefault();
+            this.currentCard = this.currentData();
+            this.currentCard.replayPoker();
+            this.data().unsetPokerEnd();
+            this.data().unsetPokerEstimation();
+          }
+        },
+        'click .js-poker-estimation'(event) {
+          event.preventDefault();
 
 
-  assigneeBySelected() {
-    if (this.getAssigneeBy().length === 0) {
-      return false;
-    } else {
-      return true;
-    }
-  },
+          const ruleTitle = this.find('#pokerEstimation').value;
+          if (ruleTitle !== undefined && ruleTitle !== '') {
+            this.find('#pokerEstimation').value = '';
 
 
-  memberType() {
-    const user = Users.findOne(this.userId);
-    return user && user.isBoardAdmin() ? 'admin' : 'normal';
+            if (ruleTitle) {
+              this.data().setPokerEstimation(parseInt(ruleTitle, 10));
+            } else {
+              this.data().setPokerEstimation('');
+            }
+          }
+        },
+      },
+    ];
   },
   },
+}).register('cardDetails');
 
 
-  presenceStatusClassName() {
-    const user = Users.findOne(this.userId);
-    const userPresence = presences.findOne({ userId: this.userId });
-    if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending';
-    else if (!userPresence) return 'disconnected';
-    else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
-      return 'active';
-    else return 'idle';
+BlazeComponent.extendComponent({
+  template() {
+    return 'exportCard';
   },
   },
-});
-
-Template.userAvatarAssigneeInitials.helpers({
-  initials() {
-    const user = Users.findOne(this.userId);
-    return user && user.getInitials();
+  withApi() {
+    return Template.instance().apiEnabled.get();
   },
   },
-
-  viewPortWidth() {
-    const user = Users.findOne(this.userId);
-    return ((user && user.getInitials().length) || 1) * 12;
+  exportUrlCardPDF() {
+    const params = {
+      boardId: Session.get('currentBoard'),
+      listId: this.listId,
+      cardId: this.cardId,
+    };
+    const queryParams = {
+      authToken: Accounts._storedLoginToken(),
+    };
+    return FlowRouter.path(
+      '/api/boards/:boardId/lists/:listId/cards/:cardId/exportPDF',
+      params,
+      queryParams,
+    );
   },
   },
+  exportFilenameCardPDF() {
+    //const boardId = Session.get('currentBoard');
+    //return `export-card-pdf-${boardId}.xlsx`;
+    return `export-card.pdf`;
+  },
+}).register('exportCardPopup');
+
+// only allow number input
+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.]');
+    let ret = regex.test(charCode);
+    // only working here, defining in events() doesn't handle the return value correctly
+    return ret;
+  });
 });
 });
 
 
 // We extends the normal InlinedForm component to support UnsavedEdits draft
 // We extends the normal InlinedForm component to support UnsavedEdits draft
@@ -546,6 +598,10 @@ Template.cardDetailsActionsPopup.helpers({
     return this.findWatcher(Meteor.userId());
     return this.findWatcher(Meteor.userId());
   },
   },
 
 
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   canModifyCard() {
   canModifyCard() {
     return (
     return (
       Meteor.user() &&
       Meteor.user() &&
@@ -556,10 +612,13 @@ Template.cardDetailsActionsPopup.helpers({
 });
 });
 
 
 Template.cardDetailsActionsPopup.events({
 Template.cardDetailsActionsPopup.events({
+  'click .js-export-card': Popup.open('exportCard'),
   'click .js-members': Popup.open('cardMembers'),
   'click .js-members': Popup.open('cardMembers'),
   'click .js-assignees': Popup.open('cardAssignees'),
   'click .js-assignees': Popup.open('cardAssignees'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
   'click .js-attachments': Popup.open('cardAttachments'),
+  'click .js-start-voting': Popup.open('cardStartVoting'),
+  'click .js-start-planning-poker': Popup.open('cardStartPlanningPoker'),
   'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
@@ -575,7 +634,7 @@ Template.cardDetailsActionsPopup.events({
     const minOrder = _.min(
     const minOrder = _.min(
       this.list()
       this.list()
         .cards(this.swimlaneId)
         .cards(this.swimlaneId)
-        .map(c => c.sort),
+        .map((c) => c.sort),
     );
     );
     this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
     this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
   },
   },
@@ -584,7 +643,7 @@ Template.cardDetailsActionsPopup.events({
     const maxOrder = _.max(
     const maxOrder = _.max(
       this.list()
       this.list()
         .cards(this.swimlaneId)
         .cards(this.swimlaneId)
-        .map(c => c.sort),
+        .map((c) => c.sort),
     );
     );
     this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
     this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
   },
   },
@@ -603,7 +662,7 @@ Template.cardDetailsActionsPopup.events({
   },
   },
 });
 });
 
 
-Template.editCardTitleForm.onRendered(function() {
+Template.editCardTitleForm.onRendered(function () {
   autosize(this.$('.js-edit-card-title'));
   autosize(this.$('.js-edit-card-title'));
 });
 });
 
 
@@ -617,7 +676,7 @@ Template.editCardTitleForm.events({
   },
   },
 });
 });
 
 
-Template.editCardRequesterForm.onRendered(function() {
+Template.editCardRequesterForm.onRendered(function () {
   autosize(this.$('.js-edit-card-requester'));
   autosize(this.$('.js-edit-card-requester'));
 });
 });
 
 
@@ -630,7 +689,7 @@ Template.editCardRequesterForm.events({
   },
   },
 });
 });
 
 
-Template.editCardAssignerForm.onRendered(function() {
+Template.editCardAssignerForm.onRendered(function () {
   autosize(this.$('.js-edit-card-assigner'));
   autosize(this.$('.js-edit-card-assigner'));
 });
 });
 
 
@@ -649,7 +708,11 @@ Template.moveCardPopup.events({
     // instead from a “component” state.
     // instead from a “component” state.
     const card = Cards.findOne(Session.get('currentCard'));
     const card = Cards.findOne(Session.get('currentCard'));
     const bSelect = $('.js-select-boards')[0];
     const bSelect = $('.js-select-boards')[0];
-    const boardId = bSelect.options[bSelect.selectedIndex].value;
+    let boardId;
+    // if we are a worker, we won't have a board select so we just use the
+    // current boardId of the card.
+    if (bSelect) boardId = bSelect.options[bSelect.selectedIndex].value;
+    else boardId = card.boardId;
     const lSelect = $('.js-select-lists')[0];
     const lSelect = $('.js-select-lists')[0];
     const listId = lSelect.options[lSelect.selectedIndex].value;
     const listId = lSelect.options[lSelect.selectedIndex].value;
     const slSelect = $('.js-select-swimlanes')[0];
     const slSelect = $('.js-select-swimlanes')[0];
@@ -665,17 +728,16 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   boards() {
   boards() {
-    const boards = Boards.find(
+    return Boards.find(
       {
       {
         archived: false,
         archived: false,
         'members.userId': Meteor.userId(),
         'members.userId': Meteor.userId(),
         _id: { $ne: Meteor.user().getTemplatesBoardId() },
         _id: { $ne: Meteor.user().getTemplatesBoardId() },
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
-    return boards;
   },
   },
 
 
   swimlanes() {
   swimlanes() {
@@ -704,7 +766,7 @@ Template.copyCardPopup.events({
   'click .js-done'() {
   'click .js-done'() {
     const card = Cards.findOne(Session.get('currentCard'));
     const card = Cards.findOne(Session.get('currentCard'));
     const lSelect = $('.js-select-lists')[0];
     const lSelect = $('.js-select-lists')[0];
-    listId = lSelect.options[lSelect.selectedIndex].value;
+    const listId = lSelect.options[lSelect.selectedIndex].value;
     const slSelect = $('.js-select-swimlanes')[0];
     const slSelect = $('.js-select-swimlanes')[0];
     const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
     const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
     const bSelect = $('.js-select-boards')[0];
     const bSelect = $('.js-select-boards')[0];
@@ -712,9 +774,7 @@ Template.copyCardPopup.events({
     const textarea = $('#copy-card-title');
     const textarea = $('#copy-card-title');
     const title = textarea.val().trim();
     const title = textarea.val().trim();
     // insert new card to the bottom of new list
     // insert new card to the bottom of new list
-    card.sort = Lists.findOne(card.listId)
-      .cards()
-      .count();
+    card.sort = Lists.findOne(card.listId).cards().count();
 
 
     if (title) {
     if (title) {
       card.title = title;
       card.title = title;
@@ -745,9 +805,7 @@ Template.copyChecklistToManyCardsPopup.events({
     const textarea = $('#copy-card-title');
     const textarea = $('#copy-card-title');
     const titleEntry = textarea.val().trim();
     const titleEntry = textarea.val().trim();
     // insert new card to the bottom of new list
     // insert new card to the bottom of new list
-    card.sort = Lists.findOne(card.listId)
-      .cards()
-      .count();
+    card.sort = Lists.findOne(card.listId).cards().count();
 
 
     if (titleEntry) {
     if (titleEntry) {
       const titleList = JSON.parse(titleEntry);
       const titleList = JSON.parse(titleEntry);
@@ -764,13 +822,13 @@ Template.copyChecklistToManyCardsPopup.events({
         Filter.addException(_id);
         Filter.addException(_id);
 
 
         // copy checklists
         // copy checklists
-        Checklists.find({ cardId: oldId }).forEach(ch => {
+        Checklists.find({ cardId: oldId }).forEach((ch) => {
           ch.copy(_id);
           ch.copy(_id);
         });
         });
 
 
         // copy subtasks
         // copy subtasks
-        cursor = Cards.find({ parentId: oldId });
-        cursor.forEach(function() {
+        const cursor = Cards.find({ parentId: oldId });
+        cursor.forEach(function () {
           'use strict';
           'use strict';
           const subtask = arguments[0];
           const subtask = arguments[0];
           subtask.parentId = _id;
           subtask.parentId = _id;
@@ -779,7 +837,7 @@ Template.copyChecklistToManyCardsPopup.events({
         });
         });
 
 
         // copy card comments
         // copy card comments
-        CardComments.find({ cardId: oldId }).forEach(cmt => {
+        CardComments.find({ cardId: oldId }).forEach((cmt) => {
           cmt.copy(_id);
           cmt.copy(_id);
         });
         });
       }
       }
@@ -795,7 +853,7 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   colors() {
   colors() {
-    return cardColors.map(color => ({ color, name: '' }));
+    return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
   },
   },
 
 
   isSelected(color) {
   isSelected(color) {
@@ -839,7 +897,7 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   boards() {
   boards() {
-    const boards = Boards.find(
+    return Boards.find(
       {
       {
         archived: false,
         archived: false,
         'members.userId': Meteor.userId(),
         'members.userId': Meteor.userId(),
@@ -848,10 +906,9 @@ BlazeComponent.extendComponent({
         },
         },
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
-    return boards;
   },
   },
 
 
   cards() {
   cards() {
@@ -919,9 +976,25 @@ BlazeComponent.extendComponent({
             }
             }
           }
           }
         },
         },
-        'click .js-delete': Popup.afterConfirm('cardDelete', function() {
+        'click .js-delete': Popup.afterConfirm('cardDelete', function () {
           Popup.close();
           Popup.close();
-          Cards.remove(this._id);
+          // verify that there are no linked cards
+          if (Cards.find({ linkedId: this._id }).count() === 0) {
+            Cards.remove(this._id);
+          } else {
+            // TODO: Maybe later we can list where the linked cards are.
+            // Now here is popup with a hint that the card cannot be deleted
+            // as there are linked cards.
+            // Related:
+            //   client/components/lists/listHeader.js about line 248
+            //   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`;
+            alert(message);
+          }
           Utils.goBoardId(this.boardId);
           Utils.goBoardId(this.boardId);
         }),
         }),
         'change .js-field-parent-board'(event) {
         'change .js-field-parent-board'(event) {
@@ -945,6 +1018,528 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('cardMorePopup');
 }).register('cardMorePopup');
 
 
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentCard = this.currentData();
+    this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion);
+  },
+
+  events() {
+    return [
+      {
+        'click .js-end-date': Popup.open('editVoteEndDate'),
+        'submit .edit-vote-question'(evt) {
+          evt.preventDefault();
+          const voteQuestion = evt.target.vote.value;
+          const publicVote = $('#vote-public').hasClass('is-checked');
+          const allowNonBoardMembers = $('#vote-allow-non-members').hasClass(
+            'is-checked',
+          );
+          const endString = this.currentCard.getVoteEnd();
+
+          this.currentCard.setVoteQuestion(
+            voteQuestion,
+            publicVote,
+            allowNonBoardMembers,
+          );
+          if (endString) {
+            this.currentCard.setVoteEnd(endString);
+          }
+          Popup.close();
+        },
+        'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
+          event.preventDefault();
+          this.currentCard.unsetVote();
+          Popup.close();
+        }),
+        'click a.js-toggle-vote-public'(event) {
+          event.preventDefault();
+          $('#vote-public').toggleClass('is-checked');
+        },
+        'click a.js-toggle-vote-allow-non-members'(event) {
+          event.preventDefault();
+          $('#vote-allow-non-members').toggleClass('is-checked');
+        },
+      },
+    ];
+  },
+}).register('cardStartVotingPopup');
+
+// editVoteEndDatePopup
+(class extends DatePicker {
+  onCreated() {
+    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
+    this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
+  }
+  events() {
+    return [
+      {
+        'submit .edit-date'(evt) {
+          evt.preventDefault();
+
+          // if no time was given, init with 12:00
+          const time =
+            evt.target.time.value ||
+            moment(new Date().setHours(12, 0, 0)).format('LT');
+
+          const dateString = `${evt.target.date.value} ${time}`;
+
+          /*
+          const newDate = moment(dateString, 'L LT', true);
+          if (newDate.isValid()) {
+            // if active vote -  store it
+            if (this.currentData().getVoteQuestion()) {
+              this._storeDate(newDate.toDate());
+              Popup.close();
+            } else {
+              this.currentData().vote = { end: newDate.toDate() }; // set vote end temp
+              Popup.back();
+            }
+
+
+          */
+
+          // Try to parse different date formats of all languages.
+          // This code is same for vote and planning poker.
+          const usaDate = moment(dateString, 'L LT', true);
+          const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
+          const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
+          const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
+          const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
+          const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
+          const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
+          const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
+          const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
+          const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
+          const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
+          // greekDate does not work: el Greek Ελληνικά ,
+          // it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
+          // where MM is maybe some text like AM/PM ?
+          // Also some other languages that have non-ascii characters in dates
+          // do not work.
+          const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
+          const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
+
+          if (usaDate.isValid()) {
+            // 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } else {
+            this.error.set('invalid-date');
+            evt.target.date.focus();
+          }
+        },
+        'click .js-delete-date'(evt) {
+          evt.preventDefault();
+          this._deleteDate();
+          Popup.close();
+        },
+      },
+    ];
+  }
+  _storeDate(newDate) {
+    this.card.setVoteEnd(newDate);
+  }
+  _deleteDate() {
+    this.card.unsetVoteEnd();
+  }
+}.register('editVoteEndDatePopup'));
+
+BlazeComponent.extendComponent({
+  onCreated() {
+    this.currentCard = this.currentData();
+    this.pokerQuestion = new ReactiveVar(this.currentCard.pokerQuestion);
+  },
+
+  events() {
+    return [
+      {
+        'click .js-end-date': Popup.open('editPokerEndDate'),
+        'submit .edit-poker-question'(evt) {
+          evt.preventDefault();
+          const pokerQuestion = true;
+          const allowNonBoardMembers = $('#poker-allow-non-members').hasClass(
+            'is-checked',
+          );
+          const endString = this.currentCard.getPokerEnd();
+
+          this.currentCard.setPokerQuestion(
+            pokerQuestion,
+            allowNonBoardMembers,
+          );
+          if (endString) {
+            this.currentCard.setPokerEnd(endString);
+          }
+          Popup.close();
+        },
+        'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
+          event.preventDefault();
+          this.currentCard.unsetPoker();
+          Popup.close();
+        }),
+        'click a.js-toggle-poker-allow-non-members'(event) {
+          event.preventDefault();
+          $('#poker-allow-non-members').toggleClass('is-checked');
+        },
+      },
+    ];
+  },
+}).register('cardStartPlanningPokerPopup');
+
+// editPokerEndDatePopup
+(class extends DatePicker {
+  onCreated() {
+    super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
+    this.data().getPokerEnd() &&
+      this.date.set(moment(this.data().getPokerEnd()));
+  }
+
+  /*
+  Tried to use dateFormat and timeFormat from client/components/lib/datepicker.js
+  to make detecting all date formats not necessary,
+  but got error "language mk does not exist".
+  Maybe client/components/lib/datepicker.jade could have hidden input field for
+  datepicker format that could be used to detect date format?
+
+  dateFormat() {
+    return moment.localeData().longDateFormat('L');
+  }
+
+  timeFormat() {
+    return moment.localeData().longDateFormat('LT');
+  }
+
+  const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
+  */
+
+  events() {
+    return [
+      {
+        'submit .edit-date'(evt) {
+          evt.preventDefault();
+
+          // if no time was given, init with 12:00
+          const time =
+            evt.target.time.value ||
+            moment(new Date().setHours(12, 0, 0)).format('LT');
+
+          const dateString = `${evt.target.date.value} ${time}`;
+
+          /*
+          Tried to use dateFormat and timeFormat from client/components/lib/datepicker.js
+          to make detecting all date formats not necessary,
+          but got error "language mk does not exist".
+          Maybe client/components/lib/datepicker.jade could have hidden input field for
+          datepicker format that could be used to detect date format?
+
+          const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
+
+          if (newDate.isValid()) {
+            // if active poker -  store it
+            if (this.currentData().getPokerQuestion()) {
+              this._storeDate(newDate.toDate());
+              Popup.close();
+            } else {
+              this.currentData().poker = { end: newDate.toDate() }; // set poker end temp
+              Popup.back();
+            }
+          */
+
+          // Try to parse different date formats of all languages.
+          // This code is same for vote and planning poker.
+          const usaDate = moment(dateString, 'L LT', true);
+          const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
+          const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
+          const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
+          const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
+          const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
+          const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
+          const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
+          const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
+          const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
+          const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
+          // greekDate does not work: el Greek Ελληνικά ,
+          // it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
+          // where MM is maybe some text like AM/PM ?
+          // Also some other languages that have non-ascii characters in dates
+          // do not work.
+          const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
+          const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
+
+          if (usaDate.isValid()) {
+            // 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } 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();
+            }
+          } else {
+            // this.error.set('invalid-date);
+            this.error.set('invalid-date' + ' ' + dateString);
+            evt.target.date.focus();
+          }
+        },
+        'click .js-delete-date'(evt) {
+          evt.preventDefault();
+          this._deleteDate();
+          Popup.close();
+        },
+      },
+    ];
+  }
+  _storeDate(newDate) {
+    this.card.setPokerEnd(newDate);
+  }
+  _deleteDate() {
+    this.card.unsetPokerEnd();
+  }
+}.register('editPokerEndDatePopup'));
+
 // Close the card details pane by pressing escape
 // Close the card details pane by pressing escape
 EscapeActions.register(
 EscapeActions.register(
   'detailsPane',
   'detailsPane',

+ 220 - 18
client/components/cards/cardDetails.styl

@@ -10,6 +10,9 @@ avatar-radius = 50%
   left: -2000px
   left: -2000px
   top: 0px
   top: 0px
 
 
+#clipboard
+  white-space: normal
+
 .assignee
 .assignee
   border-radius: 3px
   border-radius: 3px
   display: block
   display: block
@@ -37,6 +40,8 @@ avatar-radius = 50%
       position: absolute
       position: absolute
 
 
     &.avatar-image
     &.avatar-image
+      object-fit: cover;
+      object-position: center;
       height: 100%
       height: 100%
       width: @height
       width: @height
 
 
@@ -84,7 +89,7 @@ avatar-radius = 50%
 .card-details
 .card-details
   padding: 0
   padding: 0
   flex-shrink: 0
   flex-shrink: 0
-  flex-basis: 510px
+  flex-basis: 600px
   will-change: flex-basis
   will-change: flex-basis
   overflow-y: scroll
   overflow-y: scroll
   overflow-x: hidden
   overflow-x: hidden
@@ -94,25 +99,24 @@ avatar-radius = 50%
   animation: flexGrowIn 0.1s
   animation: flexGrowIn 0.1s
   box-shadow: 0 0 7px 0 darken(white, 30%)
   box-shadow: 0 0 7px 0 darken(white, 30%)
   transition: flex-basis 0.1s
   transition: flex-basis 0.1s
+  box-sizing: border-box
 
 
   .mCustomScrollBox
   .mCustomScrollBox
     padding-left: 0
     padding-left: 0
 
 
-  .ps-scrollbar-y-rail
-    pointer-event: all
-    position: absolute;
-
   .card-details-canvas
   .card-details-canvas
-    width: 470px
-    padding-left: 20px;
+    width: auto
+    padding: 0 20px
 
 
   .card-details-header
   .card-details-header
     margin: 0 -20px 5px
     margin: 0 -20px 5px
-    padding 7px 16px
+    padding: 7px 20px
     background: darken(white, 7%)
     background: darken(white, 7%)
     border-bottom: 1px solid darken(white, 14%)
     border-bottom: 1px solid darken(white, 14%)
 
 
     .close-card-details,
     .close-card-details,
+    .maximize-card-details,
+    .minimize-card-details,
     .card-details-menu,
     .card-details-menu,
     .card-copy-button,
     .card-copy-button,
     .card-copy-mobile-button,
     .card-copy-mobile-button,
@@ -120,9 +124,11 @@ avatar-radius = 50%
     .card-details-menu-mobile-web
     .card-details-menu-mobile-web
       float: right
       float: right
 
 
-    .close-card-details
+    .close-card-details,
+    .maximize-card-details,
+    .minimize-card-details
       font-size: 24px
       font-size: 24px
-      padding: 5px
+      padding: 5px 10px 5px 10px
       margin-right: -8px
       margin-right: -8px
 
 
     .close-card-details-mobile-web
     .close-card-details-mobile-web
@@ -196,23 +202,33 @@ avatar-radius = 50%
       margin-right: 0.5em
       margin-right: 0.5em
       &:last-child
       &:last-child
         margin-right: 0
         margin-right: 0
-      &.card-details-item-labels,
+      &.card-details-item-labels
+        display: block
+        word-wrap: break-word
+        max-width: 95%
+        flex-grow: 1
       &.card-details-item-members,
       &.card-details-item-members,
       &.card-details-item-assignees,
       &.card-details-item-assignees,
+      &.card-details-item-customfield,
+      &.card-details-item-name
+        display: block
+        word-wrap: break-word
+        max-width: 36%
+        flex-grow: 1
+      &.card-details-item-creator,
       &.card-details-item-received,
       &.card-details-item-received,
       &.card-details-item-start,
       &.card-details-item-start,
       &.card-details-item-due,
       &.card-details-item-due,
-      &.card-details-item-end,
-      &.card-details-item-customfield,
-      &.card-details-item-name
+      &.card-details-item-end
         display: block
         display: block
         word-wrap: break-word
         word-wrap: break-word
-        max-width: 48%
+        max-width: 28%
         flex-grow: 1
         flex-grow: 1
 
 
   .card-details-item-title
   .card-details-item-title
     font-size: 16px
     font-size: 16px
-    color: #000
+    font-weight: bold
+    color: #4d4d4d
 
 
   .card-label
   .card-label
     padding-top: 5px
     padding-top: 5px
@@ -221,6 +237,43 @@ avatar-radius = 50%
   .activities
   .activities
     padding-top: 10px
     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
+    position: absolute
+    float: left
+    top: 60px
+    left: 20px
+    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
 input[type="text"].attachment-add-link-input
   float: left
   float: left
   margin: 0 0 8px
   margin: 0 0 8px
@@ -241,14 +294,20 @@ input[type="submit"].attachment-add-link-submit
 
 
     .card-details-canvas
     .card-details-canvas
       width: 100%
       width: 100%
-      padding-left: 0px;
+      padding-left: 0px
 
 
     .card-details-header
     .card-details-header
       .close-card-details
       .close-card-details
         margin-right: 0px
         margin-right: 0px
 
 
       .card-details-menu
       .card-details-menu
-        margin-right: 10px
+        margin-right: 40px
+
+      .maximize-card-details
+        margin-right: 40px
+
+      .minimize-card-details
+        margin-right: 40px
 
 
 card-details-color(background, color...)
 card-details-color(background, color...)
   background: background !important
   background: background !important
@@ -330,3 +389,146 @@ card-details-color(background, color...)
 
 
 .card-details-indigo
 .card-details-indigo
   card-details-color(#4b0082, #ffffff) //White text for better visibility
   card-details-color(#4b0082, #ffffff) //White text for better visibility
+
+.voted
+  opacity: .7
+.vote-title
+  display: flex
+  justify-content: space-between
+
+  .js-edit-date
+    align-self: baseline
+    margin-left: 5px
+
+.vote-result
+  display: flex
+.js-show-positive-votes
+  cursor: pointer
+
+.poker-voted
+  opacity: .7
+
+.poker-title
+  display: flex
+  justify-content: space-between
+
+  .js-edit-date
+    align-self: baseline
+    margin-left: 5px
+
+.poker-result
+  display: flex
+  flex-flow: row wrap
+.js-show-positive-poker-votes
+  cursor: pointer
+
+.poker-deck
+  display: grid
+  flex-direction: column
+  text-align: center
+
+.poker-card-result
+  width: 32px
+  font-size: 1em
+  font-weight: bold
+  padding: 4px 2px 4px 2px
+  cursor: default
+
+.winner
+  font-weight: bold
+  outline: #2d2d2d solid 2px
+
+.loser
+  opacity: .5
+
+.responsive-table
+  overflow-x: auto
+
+.poker-table
+  display: table
+  width: 100%
+  padding-top: 10px
+
+.poker-table-row
+  display: table-row
+
+.poker-table-heading
+  background-color: #EEE
+  display: table-header-group
+
+.poker-table-cell
+  display: table-cell
+  padding: 0 0 5px 2px
+  border-bottom: 1px solid #d2d0d0
+  text-align: center
+  min-width: 45px
+
+.poker-table-cell-who
+  width: 150px
+  vertical-align: middle
+
+.poker-table-heading-left,
+.poker-table-heading-right
+  display: table-header-group
+  font-weight: bold
+  border-top: 1px solid #808080
+
+@media (max-width: 400px)
+  .poker-table-heading-right
+    display: none
+
+.poker-table-body
+  display: table-row-group
+
+.poker-table-side-left,
+.poker-table-side-right
+  display: inline-block
+
+.poker-table-side-right
+  padding-left: 10px
+
+@media (max-width: 400px)
+  .poker-table-side-right
+    padding-left: 0px
+
+.estimation-add
+  display: block
+  overflow: auto
+  margin-top: 15px
+  margin-bottom: 5px
+  input
+    display: inline-block
+    float: right
+    margin: auto
+    margin-right: 10px
+    width: 100px
+  button
+    display: inline-block
+    float: right
+    margin: auto
+
+.poker-card
+  width:48px
+  height:72px
+  float:left
+  background:#fff
+  border-radius:5px
+  display:table
+  box-sizing:border-box
+  padding:5px
+  margin:3px
+  font-size:20px
+  font-weight: bold
+  text-shadow: #2d2d2d 1px 1px 0
+  box-shadow:0 0 5px #aaaaaa
+  text-align:center
+  position:relative
+  cursor: pointer
+
+  .inner
+    display:table-cell
+    vertical-align:middle
+    border-radius:5px
+    overflow:hidden
+    background-color: #cecece
+

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

@@ -1,7 +1,17 @@
 template(name="checklists")
 template(name="checklists")
-  h3
-    i.fa.fa-check
-    | {{_ 'checklists'}}
+  .checklists-title
+    h3.card-details-item-title
+      i.fa.fa-check
+      | {{_ 'checklists'}}
+    if currentUser.isBoardMember
+      .material-toggle-switch(title="{{_ 'hide-checked-items'}}")
+        //span.toggle-switch-title
+        if hideCheckedItems
+          input.toggle-switch(type="checkbox" id="toggleHideCheckedItemsButton" checked="checked")
+        else
+          input.toggle-switch(type="checkbox" id="toggleHideCheckedItemsButton")
+        label.toggle-label(for="toggleHideCheckedItemsButton")
+
   if toggleDeleteDialog.get
   if toggleDeleteDialog.get
     .board-overlay#card-details-overlay
     .board-overlay#card-details-overlay
     +checklistDeleteDialog(checklist = checklistToDelete)
     +checklistDeleteDialog(checklist = checklistToDelete)
@@ -15,9 +25,8 @@ template(name="checklists")
     +inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
     +inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
       +addChecklistItemForm
       +addChecklistItemForm
     else
     else
-      a.js-open-inlined-form
+      a.js-open-inlined-form(title="{{_ 'add-checklist'}}")
         i.fa.fa-plus
         i.fa.fa-plus
-        | {{_ 'add-checklist'}}...
 
 
 template(name="checklistDetail")
 template(name="checklistDetail")
   .js-checklist.checklist
   .js-checklist.checklist
@@ -31,6 +40,8 @@ template(name="checklistDetail")
 
 
         if canModifyCard
         if canModifyCard
           h2.title.js-open-inlined-form.is-editable
           h2.title.js-open-inlined-form.is-editable
+            if isMiniScreenOrShowDesktopDragHandles
+              span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
             +viewer
             +viewer
               = checklist.title
               = checklist.title
         else
         else
@@ -81,14 +92,16 @@ template(name="checklistItems")
       +inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
       +inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
         +addChecklistItemForm
         +addChecklistItemForm
       else
       else
-        a.add-checklist-item.js-open-inlined-form
+        a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
           i.fa.fa-plus
           i.fa.fa-plus
-          | {{_ 'add-checklist-item'}}...
 
 
 template(name='checklistItemDetail')
 template(name='checklistItemDetail')
-  .js-checklist-item.checklist-item
+  .js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if hideCheckedItems}} invisible{{/if}}{{/if}}")
     if canModifyCard
     if canModifyCard
-      .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      .check-box-container
+        .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
+      if isMiniScreenOrShowDesktopDragHandles
+        span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
         +viewer
         +viewer
           = item.title
           = item.title

+ 46 - 12
client/components/cards/checklists.js

@@ -1,4 +1,4 @@
-const { calculateIndexData, enableClickOnTouch } = Utils;
+const { calculateIndexData, capitalize } = Utils;
 
 
 function initSorting(items) {
 function initSorting(items) {
   items.sortable({
   items.sortable({
@@ -6,7 +6,7 @@ function initSorting(items) {
     helper: 'clone',
     helper: 'clone',
     items: '.js-checklist-item:not(.placeholder)',
     items: '.js-checklist-item:not(.placeholder)',
     connectWith: '.js-checklist-items',
     connectWith: '.js-checklist-items',
-    appendTo: '.board-canvas',
+    appendTo: 'parent',
     distance: 7,
     distance: 7,
     placeholder: 'checklist-item placeholder',
     placeholder: 'checklist-item placeholder',
     scroll: false,
     scroll: false,
@@ -36,9 +36,6 @@ function initSorting(items) {
       checklistItem.move(checklistId, sortIndex.base);
       checklistItem.move(checklistId, sortIndex.base);
     },
     },
   });
   });
-
-  // ugly touch event hotfix
-  enableClickOnTouch('.js-checklist-item:not(.placeholder)');
 }
 }
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
@@ -54,14 +51,16 @@ BlazeComponent.extendComponent({
       return Meteor.user() && Meteor.user().isBoardMember();
       return Meteor.user() && Meteor.user().isBoardMember();
     }
     }
 
 
-    // Disable sorting if the current user is not a board member
+    // Disable sorting if the current user is not a board member or is a miniscreen
     self.autorun(() => {
     self.autorun(() => {
       const $itemsDom = $(self.itemsDom);
       const $itemsDom = $(self.itemsDom);
-      if ($itemsDom.data('sortable')) {
+      if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
         $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
         $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
-      }
-      if ($itemsDom.data('sortable')) {
-        $(self.itemsDom).sortable('option', 'disabled', Utils.isMiniScreen());
+        if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
+          $(self.itemsDom).sortable({
+            handle: 'span.fa.checklistitem-handle',
+          });
+        }
       }
       }
     });
     });
   },
   },
@@ -112,7 +111,7 @@ BlazeComponent.extendComponent({
         title,
         title,
         checklistId: checklist._id,
         checklistId: checklist._id,
         cardId: checklist.cardId,
         cardId: checklist.cardId,
-        sort: checklist.itemCount(),
+        sort: Utils.calculateIndexData(checklist.lastItem()).base,
       });
       });
     }
     }
     // We keep the form opened, empty it.
     // We keep the form opened, empty it.
@@ -177,6 +176,16 @@ BlazeComponent.extendComponent({
     }
     }
   },
   },
 
 
+  focusChecklistItem(event) {
+    // If a new checklist is created, pre-fill the title and select it.
+    const checklist = this.currentData().checklist;
+    if (!checklist) {
+      const textarea = event.target;
+      textarea.value = capitalize(TAPi18n.__('r-checklist'));
+      textarea.select();
+    }
+  },
+
   events() {
   events() {
     const events = {
     const events = {
       'click .toggle-delete-checklist-dialog'(event) {
       'click .toggle-delete-checklist-dialog'(event) {
@@ -185,6 +194,9 @@ BlazeComponent.extendComponent({
         }
         }
         this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
         this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
       },
       },
+      'click #toggleHideCheckedItemsButton'() {
+        Meteor.call('toggleHideCheckedItems');
+      },
     };
     };
 
 
     return [
     return [
@@ -196,12 +208,29 @@ BlazeComponent.extendComponent({
         'submit .js-edit-checklist-item': this.editChecklistItem,
         'submit .js-edit-checklist-item': this.editChecklistItem,
         'click .js-delete-checklist-item': this.deleteItem,
         'click .js-delete-checklist-item': this.deleteItem,
         'click .confirm-checklist-delete': this.deleteChecklist,
         'click .confirm-checklist-delete': this.deleteChecklist,
+        'focus .js-add-checklist-item': this.focusChecklistItem,
         keydown: this.pressKey,
         keydown: this.pressKey,
       },
       },
     ];
     ];
   },
   },
 }).register('checklists');
 }).register('checklists');
 
 
+Template.checklists.helpers({
+  hideCheckedItems() {
+    const currentUser = Meteor.user();
+    if (currentUser) return currentUser.hasHideCheckedItems();
+    return false;
+  },
+});
+
+Template.addChecklistItemForm.onRendered(() => {
+  autosize($('textarea.js-add-checklist-item'));
+});
+
+Template.editChecklistItemForm.onRendered(() => {
+  autosize($('textarea.js-edit-checklist-item'));
+});
+
 Template.checklistDeleteDialog.onCreated(() => {
 Template.checklistDeleteDialog.onCreated(() => {
   const $cardDetails = this.$('.card-details');
   const $cardDetails = this.$('.card-details');
   this.scrollState = {
   this.scrollState = {
@@ -237,6 +266,11 @@ Template.checklistItemDetail.helpers({
       !Meteor.user().isWorker()
       !Meteor.user().isWorker()
     );
     );
   },
   },
+  hideCheckedItems() {
+    const user = Meteor.user();
+    if (user) return user.hasHideCheckedItems();
+    return false;
+  },
 });
 });
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
@@ -250,7 +284,7 @@ BlazeComponent.extendComponent({
   events() {
   events() {
     return [
     return [
       {
       {
-        'click .js-checklist-item .check-box': this.toggleItem,
+        'click .js-checklist-item .check-box-container': this.toggleItem,
       },
       },
     ];
     ];
   },
   },

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

@@ -16,6 +16,10 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   &:hover
   &:hover
     color: inherit
     color: inherit
 
 
+.checklists-title
+  display: flex
+  justify-content: space-between
+
 .checklist-title
 .checklist-title
   .checkbox
   .checkbox
     float: left
     float: left
@@ -38,6 +42,11 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   .js-delete-checklist
   .js-delete-checklist
     @extends .delete-text
     @extends .delete-text
 
 
+  span.fa.checklist-handle
+    padding-right: 20px
+    padding-top: 3px
+    float: left
+
 
 
 .js-confirm-checklist-delete
 .js-confirm-checklist-delete
   background-color: darken(white, 3%)
   background-color: darken(white, 3%)
@@ -99,6 +108,17 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   margin-top: 3px
   margin-top: 3px
   display: flex
   display: flex
   background: darken(white, 3%)
   background: darken(white, 3%)
+  opacity: 1
+  transition: height 0ms 400ms, opacity 400ms 0ms
+  height: auto
+  overflow: hidden
+
+  &.is-checked.invisible
+    opacity: 0
+    height: 0
+    transition: height 0ms 0ms, opacity 600ms 0ms
+    margin-top: 0
+    margin-bottom: 0
 
 
   &.placeholder
   &.placeholder
     background: darken(white, 20%)
     background: darken(white, 20%)
@@ -113,6 +133,9 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   &:hover
   &:hover
     background-color: darken(white, 8%)
     background-color: darken(white, 8%)
 
 
+  .check-box-container
+    padding-right: 10px;
+
   .check-box
   .check-box
     margin: 0.1em 0 0 0;
     margin: 0.1em 0 0 0;
     &.is-checked
     &.is-checked
@@ -121,10 +144,10 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
 
 
   .item-title
   .item-title
     flex: 1
     flex: 1
-    padding-left: 10px;
     &.is-checked
     &.is-checked
       color: #8c8c8c
       color: #8c8c8c
       font-style: italic
       font-style: italic
+      text-decoration: line-through
     & .viewer
     & .viewer
       p
       p
         margin-bottom: 2px
         margin-bottom: 2px
@@ -132,6 +155,10 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
         word-wrap: break-word
         word-wrap: break-word
         max-width: 420px
         max-width: 420px
 
 
+  span.fa.checklistitem-handle
+    padding-top: 2px
+    padding-right: 10px;
+
 .js-delete-checklist-item
 .js-delete-checklist-item
   margin: 0 0 0.5em 1.33em
   margin: 0 0 0.5em 1.33em
   @extends .delete-text
   @extends .delete-text

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

@@ -44,9 +44,20 @@
     align-items: center
     align-items: center
     justify-content: center
     justify-content: center
 
 
+.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
+
 .card-label-green
 .card-label-green
   background-color: #3cb500
   background-color: #3cb500
 
 
+.card-label-green:hover
+  color: #000000 //Black hover text for better visibility
+
 .card-label-yellow
 .card-label-yellow
   background-color: #fad900
   background-color: #fad900
   color: #000000 //Black text for better visibility
   color: #000000 //Black text for better visibility
@@ -158,6 +169,8 @@
 
 
 .edit-labels-pop-over
 .edit-labels-pop-over
   margin-bottom: 8px
   margin-bottom: 8px
+  .card-label .viewer p
+    margin: 0
 
 
 .edit-labels-pop-over .shortcut
 .edit-labels-pop-over .shortcut
   display: inline-block
   display: inline-block

+ 40 - 5
client/components/cards/minicard.jade

@@ -4,8 +4,8 @@ template(name="minicard")
     class="{{#if isLinkedBoard}}linked-board{{/if}}"
     class="{{#if isLinkedBoard}}linked-board{{/if}}"
     class="minicard-{{colorClass}}")
     class="minicard-{{colorClass}}")
     if isMiniScreen
     if isMiniScreen
-      //.handle
-      //  .fa.fa-arrows
+      .handle
+        .fa.fa-arrows
     unless isMiniScreen
     unless isMiniScreen
       if showDesktopDragHandles
       if showDesktopDragHandles
         .handle
         .handle
@@ -74,20 +74,35 @@ template(name="minicard")
                   +viewer
                   +viewer
                     = definition.name
                     = definition.name
               .minicard-custom-field-item
               .minicard-custom-field-item
-                +viewer
-                  = trueValue
+                if $eq definition.type "currency"
+                  +viewer
+                    = formattedCurrencyCustomFieldValue(definition)
+                else if $eq definition.type "date"
+                  .date
+                    +minicardCustomFieldDate
+                else if $eq definition.type "checkbox"
+                  .materialCheckBox(class="{{#if value }}is-checked{{/if}}")
+                else if $eq definition.type "stringtemplate"
+                  +viewer
+                    = formattedStringtemplateCustomFieldValue(definition)
+                else
+                  +viewer
+                    = trueValue
 
 
     if getAssignees
     if getAssignees
       .minicard-assignees.js-minicard-assignees
       .minicard-assignees.js-minicard-assignees
         each getAssignees
         each getAssignees
           +userAvatar(userId=this)
           +userAvatar(userId=this)
-        hr
 
 
     if getMembers
     if getMembers
       .minicard-members.js-minicard-members
       .minicard-members.js-minicard-members
         each getMembers
         each getMembers
           +userAvatar(userId=this)
           +userAvatar(userId=this)
 
 
+    if showCreator
+      .minicard-creator
+        +userAvatar(userId=this.userId noRemove=true)
+
     .badges
     .badges
       unless currentUser.isNoComments
       unless currentUser.isNoComments
         if comments.count
         if comments.count
@@ -100,6 +115,17 @@ template(name="minicard")
       if getDescription
       if getDescription
         .badge.badge-state-image-only(title=getDescription)
         .badge.badge-state-image-only(title=getDescription)
           span.badge-icon.fa.fa-align-left
           span.badge-icon.fa.fa-align-left
+      if getVoteQuestion
+        .badge.badge-state-image-only(title=getVoteQuestion)
+          span.badge-icon.fa.fa-thumbs-up(class="{{#if voteState}}text-green{{/if}}")
+          span.badge-text {{ voteCountPositive }}
+          span.badge-icon.fa.fa-thumbs-down(class="{{#if $eq voteState false}}text-red{{/if}}")
+          span.badge-text {{ voteCountNegative }}
+      if getPokerQuestion
+        .badge.badge-state-image-only(title=getPokerQuestion)
+          span.badge-icon.fa.fa-check(class="{{#if pokerState}}text-green{{/if}}")
+          if expiredPoker
+            span.badge-text {{ getPokerEstimation }}
       if attachments.count
       if attachments.count
         .badge
         .badge
           span.badge-icon.fa.fa-paperclip
           span.badge-icon.fa.fa-paperclip
@@ -108,3 +134,12 @@ template(name="minicard")
         .badge(class="{{#if checklistFinished}}is-finished{{/if}}")
         .badge(class="{{#if checklistFinished}}is-finished{{/if}}")
           span.badge-icon.fa.fa-check-square-o
           span.badge-icon.fa.fa-check-square-o
           span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
           span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
+      if allSubtasks.count
+        .badge
+          span.badge-icon.fa.fa-sitemap
+          span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
+          //{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
+      if currentBoard.allowsCardSortingByNumber
+        .badge
+          span.badge-icon.fa.fa-sort
+          span.badge-text {{ sort }}

+ 47 - 7
client/components/cards/minicard.js

@@ -1,5 +1,3 @@
-import { Cookies } from 'meteor/ostrio:cookies';
-const cookies = new Cookies();
 // Template.cards.events({
 // Template.cards.events({
 //   'click .member': Popup.open('cardMember')
 //   'click .member': Popup.open('cardMember')
 // });
 // });
@@ -9,6 +7,48 @@ BlazeComponent.extendComponent({
     return 'minicard';
     return 'minicard';
   },
   },
 
 
+  formattedCurrencyCustomFieldValue(definition) {
+    const customField = this.data()
+      .customFieldsWD()
+      .find(f => f._id === definition._id);
+    const customFieldTrueValue =
+      customField && customField.trueValue ? customField.trueValue : '';
+
+    const locale = TAPi18n.getLanguage();
+    return new Intl.NumberFormat(locale, {
+      style: 'currency',
+      currency: definition.settings.currencyCode,
+    }).format(customFieldTrueValue);
+  },
+
+  formattedStringtemplateCustomFieldValue(definition) {
+    const customField = this.data()
+      .customFieldsWD()
+      .find(f => f._id === definition._id);
+
+    const customFieldTrueValue =
+      customField && customField.trueValue ? customField.trueValue : [];
+
+    return customFieldTrueValue
+      .filter(value => !!value.trim())
+      .map(value =>
+        definition.settings.stringtemplateFormat.replace(/%\{value\}/gi, value),
+      )
+      .join(definition.settings.stringtemplateSeparator ?? '');
+  },
+
+  showCreator() {
+    if (this.data().board()) {
+      return (
+        this.data().board.allowsCreator === null ||
+        this.data().board().allowsCreator === undefined ||
+        this.data().board().allowsCreator
+      );
+      // return this.data().board().allowsCreator;
+    }
+    return false;
+  },
+
   events() {
   events() {
     return [
     return [
       {
       {
@@ -20,10 +60,10 @@ BlazeComponent.extendComponent({
       },
       },
       {
       {
         'click .js-toggle-minicard-label-text'() {
         'click .js-toggle-minicard-label-text'() {
-          if (cookies.has('hiddenMinicardLabelText')) {
-            cookies.remove('hiddenMinicardLabelText'); //true
+          if (window.localStorage.getItem('hiddenMinicardLabelText')) {
+            window.localStorage.removeItem('hiddenMinicardLabelText'); //true
           } else {
           } else {
-            cookies.set('hiddenMinicardLabelText', 'true'); //true
+            window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
           }
           }
         },
         },
       },
       },
@@ -36,7 +76,7 @@ Template.minicard.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (cookies.has('showDesktopDragHandles')) {
+    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
       return true;
       return true;
     } else {
     } else {
       return false;
       return false;
@@ -46,7 +86,7 @@ Template.minicard.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).hiddenMinicardLabelText;
       return (currentUser.profile || {}).hiddenMinicardLabelText;
-    } else if (cookies.has('hiddenMinicardLabelText')) {
+    } else if (window.localStorage.getItem('hiddenMinicardLabelText')) {
       return true;
       return true;
     } else {
     } else {
       return false;
       return false;

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

@@ -87,7 +87,9 @@
       width: 11px
       width: 11px
       height: @width
       height: @width
       border-radius: 2px
       border-radius: 2px
-      margin-left: 3px
+      margin-right: 3px
+      margin-bottom: 3px
+
   .minicard-custom-fields
   .minicard-custom-fields
     display:block;
     display:block;
   .minicard-custom-field
   .minicard-custom-field
@@ -161,15 +163,18 @@
         line-height: 12px
         line-height: 12px
 
 
   .minicard-members,
   .minicard-members,
-  .minicard-assignees
+  .minicard-assignees,
+  .minicard-creator
     float: right
     float: right
-    margin: 2px -8px 12px 0
+    margin-left: 5px
+    margin-bottom: 4px
 
 
     .member
     .member
       float: right
       float: right
       border-radius: 50%
       border-radius: 50%
       height: 28px
       height: 28px
       width: @height
       width: @height
+      margin-bottom: 4px
 
 
     .assignee
     .assignee
       float: right
       float: right
@@ -178,7 +183,13 @@
       width: @height
       width: @height
 
 
     + .badges
     + .badges
-      margin-top: 10px
+      margin-top: 5px
+
+  .minicard-assignees
+    border-bottom: 1px solid red
+
+  .minicard-creator
+    border-bottom: 1px solid green
 
 
   .minicard-members:empty,
   .minicard-members:empty,
   .minicard-assignees:empty
   .minicard-assignees:empty
@@ -299,3 +310,8 @@ minicard-color(background, color...)
 
 
 .minicard-indigo
 .minicard-indigo
   minicard-color(#4b0082, #ffffff) //White text for better visibility
   minicard-color(#4b0082, #ffffff) //White text for better visibility
+
+.text-red
+  color:red
+.text-green
+  color:green

+ 44 - 0
client/components/cards/resultCard.jade

@@ -0,0 +1,44 @@
+template(name="resultCard")
+  .result-card-wrapper
+    a.minicard-wrapper.card-title(href=originRelativeUrl)
+      +minicard(this)
+      //= card.title
+    ul.result-card-context-list
+      li.result-card-context(title="{{_ 'board'}}")
+        .result-card-block-wrapper
+          if boardId
+            +viewer
+              = getBoard.title
+          else
+            .broken-cards-null
+              | NULL
+        if getBoard.archived
+          i.fa.fa-archive
+      li.result-card-context.result-card-context-separator
+        = ' '
+        | {{_ 'context-separator'}}
+        = ' '
+      li.result-card-context(title="{{_ 'swimlane'}}")
+        .result-card-block-wrapper
+          if swimlaneId
+            +viewer
+              = getSwimlane.title
+          else
+            .broken-cards-null
+              | NULL
+        if getSwimlane.archived
+          i.fa.fa-archive
+      li.result-card-context.result-card-context-separator
+        = ' '
+        | {{_ 'context-separator'}}
+        = ' '
+      li.result-card-context(title="{{_ 'list'}}")
+        .result-card-block-wrapper
+          if listId
+            +viewer
+              = getList.title
+          else
+            .broken-cards-null
+              | NULL
+        if getList.archived
+          i.fa.fa-archive

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

@@ -0,0 +1,11 @@
+Template.resultCard.helpers({
+  userId() {
+    return Meteor.userId();
+  },
+});
+
+BlazeComponent.extendComponent({
+  events() {
+    return [{}];
+  },
+}).register('resultCard');

+ 24 - 0
client/components/cards/resultCard.styl

@@ -0,0 +1,24 @@
+.result-card-list-wrapper
+  margin: 1rem
+  border-radius: 5px
+  padding: 1.5rem
+  padding-top: 0.75rem
+  display: inline-block
+  min-width: 250px
+  max-width: 350px
+
+.result-card-wrapper
+  margin-top: 0
+  margin-bottom: 10px
+
+.result-card-context
+  display: inline-block
+
+.result-card-context-separator
+  font-weight: bold
+
+.result-card-context-list
+  margin-bottom: 0.7rem
+
+.result-card-block-wrapper
+  display: inline-block

+ 10 - 9
client/components/cards/subtasks.jade

@@ -1,11 +1,11 @@
 template(name="subtasks")
 template(name="subtasks")
-  h3
+  h3.card-details-item-title
     i.fa.fa-sitemap
     i.fa.fa-sitemap
     | {{_ 'subtasks'}}
     | {{_ 'subtasks'}}
-  if toggleDeleteDialog.get
-    .board-overlay#card-details-overlay
-    +subtaskDeleteDialog(subtask = subtaskToDelete)
-
+  if currentUser.isBoardAdmin
+    if toggleDeleteDialog.get
+      .board-overlay#card-details-overlay
+      +subtaskDeleteDialog(subtask = subtaskToDelete)
 
 
   .card-subtasks-items
   .card-subtasks-items
     each subtask in currentCard.subtasks
     each subtask in currentCard.subtasks
@@ -15,9 +15,8 @@ template(name="subtasks")
     +inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
     +inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
       +addSubtaskItemForm
       +addSubtaskItemForm
     else
     else
-      a.js-open-inlined-form
+      a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
         i.fa.fa-plus
         i.fa.fa-plus
-        | {{_ 'add-subtask'}}...
 
 
 template(name="subtaskDetail")
 template(name="subtaskDetail")
   .js-subtasks.subtask
   .js-subtasks.subtask
@@ -28,7 +27,8 @@ template(name="subtaskDetail")
         span
         span
         a.js-view-subtask(title="{{ subtask.title }}") {{_ "view-it"}}
         a.js-view-subtask(title="{{ subtask.title }}") {{_ "view-it"}}
         if canModifyCard
         if canModifyCard
-          a.js-delete-subtask.toggle-delete-subtask-dialog {{_ "delete"}}...
+          if currentUser.isBoardAdmin
+            a.js-delete-subtask.toggle-delete-subtask-dialog {{_ "delete"}}...
 
 
         if canModifyCard
         if canModifyCard
           h2.title.js-open-inlined-form.is-editable
           h2.title.js-open-inlined-form.is-editable
@@ -68,7 +68,8 @@ template(name="editSubtaskItemForm")
     a.fa.fa-times-thin.js-close-inlined-form
     a.fa.fa-times-thin.js-close-inlined-form
     span(title=createdAt) {{ moment createdAt }}
     span(title=createdAt) {{ moment createdAt }}
     if canModifyCard
     if canModifyCard
-      a.js-delete-subtask-item {{_ "delete"}}...
+      if currentUser.isBoardAdmin
+        a.js-delete-subtask-item {{_ "delete"}}...
 
 
 template(name="subtasksItems")
 template(name="subtasksItems")
   .subtasks-items.js-subtasks-items
   .subtasks-items.js-subtasks-items

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

@@ -22,11 +22,20 @@ BlazeComponent.extendComponent({
     const listId = targetBoard.getDefaultSubtasksListId();
     const listId = targetBoard.getDefaultSubtasksListId();
 
 
     //Get the full swimlane data for the parent task.
     //Get the full swimlane data for the parent task.
-    const parentSwimlane = Swimlanes.findOne({boardId: crtBoard._id, _id: card.swimlaneId});
+    const parentSwimlane = Swimlanes.findOne({
+      boardId: crtBoard._id,
+      _id: card.swimlaneId,
+    });
     //find the swimlane of the same name in the target board.
     //find the swimlane of the same name in the target board.
-    const targetSwimlane = Swimlanes.findOne({boardId: targetBoard._id, title: parentSwimlane.title});
+    const targetSwimlane = Swimlanes.findOne({
+      boardId: targetBoard._id,
+      title: parentSwimlane.title,
+    });
     //If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
     //If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
-    const swimlaneId = targetSwimlane === undefined ? targetBoard.getDefaultSwimline()._id : targetSwimlane._id;
+    const swimlaneId =
+      targetSwimlane === undefined
+        ? targetBoard.getDefaultSwimline()._id
+        : targetSwimlane._id;
 
 
     if (title) {
     if (title) {
       const _id = Cards.insert({
       const _id = Cards.insert({

+ 6 - 6
client/components/forms/forms.styl

@@ -86,7 +86,7 @@ select
   margin-bottom: 8px
   margin-bottom: 8px
 
 
   &.inline
   &.inline
-  	width: 100%
+    width: 100%
 
 
 option[disabled]
 option[disabled]
   color: #8c8c8c
   color: #8c8c8c
@@ -242,11 +242,11 @@ textarea
     margin: 3px 4px
     margin: 3px 4px
 
 
 // Material Design checkboxes
 // Material Design checkboxes
-[type="checkbox"]:not(:checked),
-[type="checkbox"]:checked
-  position: absolute
-  left: -9999px
-  visibility: hidden
+  [type="checkbox"]:not(:checked),
+  [type="checkbox"]:checked
+    position: absolute
+    left: -9999px
+    visibility: hidden
 
 
 .materialCheckBox
 .materialCheckBox
   position: relative
   position: relative

+ 37 - 0
client/components/import/csvMembersMapper.js

@@ -0,0 +1,37 @@
+export function csvGetMembersToMap(data) {
+  // we will work on the list itself (an ordered array of objects) when a
+  // mapping is done, we add a 'wekan' field to the object representing the
+  // imported member
+
+  const membersToMap = [];
+  const importedMembers = [];
+  let membersIndex;
+
+  for (let i = 0; i < data[0].length; i++) {
+    if (data[0][i].toLowerCase() === 'members') {
+      membersIndex = i;
+    }
+  }
+
+  for (let i = 1; i < data.length; i++) {
+    if (data[i][membersIndex]) {
+      for (const importedMember of data[i][membersIndex].split(' ')) {
+        if (importedMember && importedMembers.indexOf(importedMember) === -1) {
+          importedMembers.push(importedMember);
+        }
+      }
+    }
+  }
+
+  for (let importedMember of importedMembers) {
+    importedMember = {
+      username: importedMember,
+      id: importedMember,
+    };
+    const wekanUser = Users.findOne({ username: importedMember.username });
+    if (wekanUser) importedMember.wekanId = wekanUser._id;
+    membersToMap.push(importedMember);
+  }
+
+  return membersToMap;
+}

+ 32 - 30
client/components/import/import.jade

@@ -13,41 +13,43 @@ template(name="import")
 template(name="importTextarea")
 template(name="importTextarea")
   form
   form
     p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
     p: label(for='import-textarea') {{_ instruction}} {{_ 'import-board-instruction-about-errors'}}
-    textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
+    textarea.js-import-json(id='import-textarea' placeholder="{{_ importPlaceHolder}}" autofocus)
       | {{jsonText}}
       | {{jsonText}}
-    if isSandstorm
-      h1.warning {{_ 'import-sandstorm-backup-warning'}}
-      p.warning {{_ 'import-sandstorm-warning'}}
     input.primary.wide(type="submit" value="{{_ 'import'}}")
     input.primary.wide(type="submit" value="{{_ 'import'}}")
 
 
 template(name="importMapMembers")
 template(name="importMapMembers")
   h2 {{_ 'import-map-members'}}
   h2 {{_ 'import-map-members'}}
-  .map-members
-    p {{_ 'import-members-map'}}
-    .mapping-list
-      each members
-        a.mapping-item.js-select-member(class="{{#if wekanId}}filled{{/if}}")
-          .profile-source
-            .full-name= fullName
-            .username
-              | ({{username}})
-          .wekan
-            if wekanId
-              +userAvatar(userId=wekanId)
-            else
-              a.member.add-member
-                i.fa.fa-plus
-      //-
-        Due to the way the flewbox layout is working, we need to set some
-        invisible items so that the last row items have a consistent width.
-        See http://jsfiddle.net/Ln4h3c4n/ for an minimal example of the issue.
-      .mapping-item.ghost-item
-      .mapping-item.ghost-item
-      .mapping-item.ghost-item
-      .mapping-item.ghost-item
-      .mapping-item.ghost-item
-    form
-      input.primary.wide(type="submit" value="{{_ 'done'}}")
+  if usersLoaded.get
+    .map-members
+      p {{_ 'import-members-map'}}
+      p.import-members-map-note
+        | {{_ 'import-members-map-note' }}
+      .mapping-list
+        each members
+          a.mapping-item.js-select-member(class="{{#if wekanId}}filled{{/if}}")
+            .profile-source
+              .full-name= fullName
+              .username
+                | ({{username}})
+            .wekan
+              if wekanId
+                +userAvatar(userId=wekanId)
+              else
+                a.member.add-member
+                  i.fa.fa-plus
+        //-
+          Due to the way the flewbox layout is working, we need to set some
+          invisible items so that the last row items have a consistent width.
+          See http://jsfiddle.net/Ln4h3c4n/ for an minimal example of the issue.
+        .mapping-item.ghost-item
+        .mapping-item.ghost-item
+        .mapping-item.ghost-item
+        .mapping-item.ghost-item
+        .mapping-item.ghost-item
+      form
+        input.primary.wide(type="submit" value="{{_ 'done'}}")
+  else
+    +spinner
 
 
 template(name="importMapMembersAddPopup")
 template(name="importMapMembersAddPopup")
   .select-member
   .select-member

+ 77 - 21
client/components/import/import.js

@@ -1,5 +1,8 @@
-import trelloMembersMapper from './trelloMembersMapper';
-import wekanMembersMapper from './wekanMembersMapper';
+import { trelloGetMembersToMap } from './trelloMembersMapper';
+import { wekanGetMembersToMap } from './wekanMembersMapper';
+import { csvGetMembersToMap } from './csvMembersMapper';
+
+const Papa = require('papaparse');
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   title() {
   title() {
@@ -30,20 +33,30 @@ BlazeComponent.extendComponent({
     }
     }
   },
   },
 
 
-  importData(evt) {
+  importData(evt, dataSource) {
     evt.preventDefault();
     evt.preventDefault();
-    const dataJson = this.find('.js-import-json').value;
-    try {
-      const dataObject = JSON.parse(dataJson);
-      this.setError('');
-      this.importedData.set(dataObject);
-      const membersToMap = this._prepareAdditionalData(dataObject);
-      // store members data and mapping in Session
-      // (we go deep and 2-way, so storing in data context is not a viable option)
+    const input = this.find('.js-import-json').value;
+    if (dataSource === 'csv') {
+      const csv = input.indexOf('\t') > 0 ? input.replace(/(\t)/g, ',') : input;
+      const ret = Papa.parse(csv);
+      if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
+      else throw new Meteor.Error('error-csv-schema');
+      const membersToMap = this._prepareAdditionalData(ret.data);
       this.membersToMap.set(membersToMap);
       this.membersToMap.set(membersToMap);
       this.nextStep();
       this.nextStep();
-    } catch (e) {
-      this.setError('error-json-malformed');
+    } else {
+      try {
+        const dataObject = JSON.parse(input);
+        this.setError('');
+        this.importedData.set(dataObject);
+        const membersToMap = this._prepareAdditionalData(dataObject);
+        // store members data and mapping in Session
+        // (we go deep and 2-way, so storing in data context is not a viable option)
+        this.membersToMap.set(membersToMap);
+        this.nextStep();
+      } catch (e) {
+        this.setError('error-json-malformed');
+      }
     }
     }
   },
   },
 
 
@@ -86,10 +99,13 @@ BlazeComponent.extendComponent({
     let membersToMap;
     let membersToMap;
     switch (importSource) {
     switch (importSource) {
       case 'trello':
       case 'trello':
-        membersToMap = trelloMembersMapper.getMembersToMap(dataObject);
+        membersToMap = trelloGetMembersToMap(dataObject);
         break;
         break;
       case 'wekan':
       case 'wekan':
-        membersToMap = wekanMembersMapper.getMembersToMap(dataObject);
+        membersToMap = wekanGetMembersToMap(dataObject);
+        break;
+      case 'csv':
+        membersToMap = csvGetMembersToMap(dataObject);
         break;
         break;
     }
     }
     return membersToMap;
     return membersToMap;
@@ -109,11 +125,23 @@ BlazeComponent.extendComponent({
     return `import-board-instruction-${Session.get('importSource')}`;
     return `import-board-instruction-${Session.get('importSource')}`;
   },
   },
 
 
+  importPlaceHolder() {
+    const importSource = Session.get('importSource');
+    if (importSource === 'csv') {
+      return 'import-csv-placeholder';
+    } else {
+      return 'import-json-placeholder';
+    }
+  },
+
   events() {
   events() {
     return [
     return [
       {
       {
         submit(evt) {
         submit(evt) {
-          return this.parentComponent().importData(evt);
+          return this.parentComponent().importData(
+            evt,
+            Session.get('importSource'),
+          );
         },
         },
       },
       },
     ];
     ];
@@ -122,14 +150,42 @@ BlazeComponent.extendComponent({
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
+    this.usersLoaded = new ReactiveVar(false);
+
     this.autorun(() => {
     this.autorun(() => {
-      this.parentComponent()
-        .membersToMap.get()
-        .forEach(({ wekanId }) => {
-          if (wekanId) {
-            this.subscribe('user-miniprofile', wekanId);
+      const handle = this.subscribe(
+        'user-miniprofile',
+        this.members().map(member => {
+          return member.username;
+        }),
+      );
+      Tracker.nonreactive(() => {
+        Tracker.autorun(() => {
+          if (
+            handle.ready() &&
+            !this.usersLoaded.get() &&
+            this.members().length
+          ) {
+            this._refreshMembers(
+              this.members().map(member => {
+                if (!member.wekanId) {
+                  let user = Users.findOne({ username: member.username });
+                  if (!user) {
+                    user = Users.findOne({ importUsernames: member.username });
+                  }
+                  if (user) {
+                    // eslint-disable-next-line no-console
+                    // console.log('found username:', user.username);
+                    member.wekanId = user._id;
+                  }
+                }
+                return member;
+              }),
+            );
           }
           }
+          this.usersLoaded.set(handle.ready());
         });
         });
+      });
     });
     });
   },
   },
 
 

+ 4 - 0
client/components/import/import.styl

@@ -47,3 +47,7 @@
 
 
 a.show-mapping
 a.show-mapping
   text-decoration underline
   text-decoration underline
+
+.import-members-map-note
+  font-size: 90%
+  font-weight: bold

+ 1 - 1
client/components/import/trelloMembersMapper.js

@@ -1,4 +1,4 @@
-export function getMembersToMap(data) {
+export function trelloGetMembersToMap(data) {
   // we will work on the list itself (an ordered array of objects) when a
   // we will work on the list itself (an ordered array of objects) when a
   // mapping is done, we add a 'wekan' field to the object representing the
   // mapping is done, we add a 'wekan' field to the object representing the
   // imported member
   // imported member

+ 1 - 1
client/components/import/wekanMembersMapper.js

@@ -1,4 +1,4 @@
-export function getMembersToMap(data) {
+export function wekanGetMembersToMap(data) {
   // we will work on the list itself (an ordered array of objects) when a
   // we will work on the list itself (an ordered array of objects) when a
   // mapping is done, we add a 'wekan' field to the object representing the
   // mapping is done, we add a 'wekan' field to the object representing the
   // imported member
   // imported member

+ 20 - 32
client/components/lists/list.js

@@ -1,6 +1,4 @@
-import { Cookies } from 'meteor/ostrio:cookies';
-const cookies = new Cookies();
-const { calculateIndex, enableClickOnTouch } = Utils;
+const { calculateIndex } = Utils;
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   // Proxy
   // Proxy
@@ -74,18 +72,16 @@ BlazeComponent.extendComponent({
         const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
         const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
         const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
         const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
         const currentBoard = Boards.findOne(Session.get('currentBoard'));
         const currentBoard = Boards.findOne(Session.get('currentBoard'));
-        let swimlaneId = '';
+        const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
+        let targetSwimlaneId = null;
+
+        // only set a new swimelane ID if the swimlanes view is active
         if (
         if (
           Utils.boardView() === 'board-view-swimlanes' ||
           Utils.boardView() === 'board-view-swimlanes' ||
           currentBoard.isTemplatesBoard()
           currentBoard.isTemplatesBoard()
         )
         )
-          swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
-        else if (
-          Utils.boardView() === 'board-view-lists' ||
-          Utils.boardView() === 'board-view-cal' ||
-          !Utils.boardView
-        )
-          swimlaneId = currentBoard.getDefaultSwimline()._id;
+          targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
+            ._id;
 
 
         // Normally the jquery-ui sortable library moves the dragged DOM element
         // Normally the jquery-ui sortable library moves the dragged DOM element
         // to its new position, which disrupts Blaze reactive updates mechanism
         // to its new position, which disrupts Blaze reactive updates mechanism
@@ -98,9 +94,12 @@ BlazeComponent.extendComponent({
 
 
         if (MultiSelection.isActive()) {
         if (MultiSelection.isActive()) {
           Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
           Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
+            const newSwimlaneId = targetSwimlaneId
+              ? targetSwimlaneId
+              : card.swimlaneId || defaultSwimlaneId;
             card.move(
             card.move(
               currentBoard._id,
               currentBoard._id,
-              swimlaneId,
+              newSwimlaneId,
               listId,
               listId,
               sortIndex.base + i * sortIndex.increment,
               sortIndex.base + i * sortIndex.increment,
             );
             );
@@ -108,28 +107,28 @@ BlazeComponent.extendComponent({
         } else {
         } else {
           const cardDomElement = ui.item.get(0);
           const cardDomElement = ui.item.get(0);
           const card = Blaze.getData(cardDomElement);
           const card = Blaze.getData(cardDomElement);
-          card.move(currentBoard._id, swimlaneId, listId, sortIndex.base);
+          const newSwimlaneId = targetSwimlaneId
+            ? targetSwimlaneId
+            : card.swimlaneId || defaultSwimlaneId;
+          card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
         }
         }
         boardComponent.setIsDragging(false);
         boardComponent.setIsDragging(false);
       },
       },
     });
     });
 
 
-    // ugly touch event hotfix
-    enableClickOnTouch(itemsSelector);
-
     this.autorun(() => {
     this.autorun(() => {
       let showDesktopDragHandles = false;
       let showDesktopDragHandles = false;
       currentUser = Meteor.user();
       currentUser = Meteor.user();
       if (currentUser) {
       if (currentUser) {
         showDesktopDragHandles = (currentUser.profile || {})
         showDesktopDragHandles = (currentUser.profile || {})
           .showDesktopDragHandles;
           .showDesktopDragHandles;
-      } else if (cookies.has('showDesktopDragHandles')) {
+      } else if (window.localStorage.getItem('showDesktopDragHandles')) {
         showDesktopDragHandles = true;
         showDesktopDragHandles = true;
       } else {
       } else {
         showDesktopDragHandles = false;
         showDesktopDragHandles = false;
       }
       }
 
 
-      if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+      if (Utils.isMiniScreen() || showDesktopDragHandles) {
         $cards.sortable({
         $cards.sortable({
           handle: '.handle',
           handle: '.handle',
         });
         });
@@ -139,27 +138,16 @@ BlazeComponent.extendComponent({
         });
         });
       }
       }
 
 
-      if ($cards.data('sortable')) {
+      if ($cards.data('uiSortable') || $cards.data('sortable')) {
         $cards.sortable(
         $cards.sortable(
           'option',
           'option',
           'disabled',
           'disabled',
-          // Disable drag-dropping when user is not member/is miniscreen
+          // Disable drag-dropping when user is not member
           !userIsMember(),
           !userIsMember(),
           // Not disable drag-dropping while in multi-selection mode
           // Not disable drag-dropping while in multi-selection mode
           // MultiSelection.isActive() || !userIsMember(),
           // MultiSelection.isActive() || !userIsMember(),
         );
         );
       }
       }
-
-      if ($cards.data('sortable')) {
-        $cards.sortable(
-          'option',
-          'disabled',
-          // Disable drag-dropping when user is not member/is miniscreen
-          Utils.isMiniScreen(),
-          // Not disable drag-dropping while in multi-selection mode
-          // MultiSelection.isActive() || !userIsMember(),
-        );
-      }
     });
     });
 
 
     // We want to re-run this function any time a card is added.
     // We want to re-run this function any time a card is added.
@@ -195,7 +183,7 @@ Template.list.helpers({
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (cookies.has('showDesktopDragHandles')) {
+    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
       return true;
       return true;
     } else {
     } else {
       return false;
       return false;

+ 5 - 7
client/components/lists/list.styl

@@ -43,9 +43,6 @@
       background: white
       background: white
       margin: -3px 0 8px
       margin: -3px 0 8px
 
 
-.list-header-card-count
-  height: 35px
-
 .list-header-add
 .list-header-add
   flex: 0 0 auto
   flex: 0 0 auto
   padding: 20px 12px 4px
   padding: 20px 12px 4px
@@ -60,6 +57,9 @@
   background-color: #e4e4e4;
   background-color: #e4e4e4;
   border-bottom: 6px solid #e4e4e4;
   border-bottom: 6px solid #e4e4e4;
 
 
+  &.list-header-card-count
+    min-height: 35px
+    height: auto
 
 
   &.ui-sortable-handle
   &.ui-sortable-handle
     cursor: grab
     cursor: grab
@@ -120,9 +120,6 @@
     form
     form
       margin-bottom: 9px
       margin-bottom: 9px
 
 
-  .ps-scrollbar-y-rail
-    transform: translateX(2px)
-
   .open-minicard-composer
   .open-minicard-composer
     border-radius: 2px
     border-radius: 2px
     color: #8c8c8c
     color: #8c8c8c
@@ -183,7 +180,8 @@
     border-bottom: 1px solid darken(white, 20%)
     border-bottom: 1px solid darken(white, 20%)
 
 
   .list
   .list
-    display: block
+    display: contents
+    flex-basis: auto
     width: 100%
     width: 100%
     border-left: 0px
     border-left: 0px
     &:first-child
     &:first-child

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

@@ -1,11 +1,11 @@
 template(name="listBody")
 template(name="listBody")
-  .list-body.js-perfect-scrollbar
+  .list-body
     .minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
     .minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
       if cards.count
       if cards.count
         +inlinedForm(autoclose=false position="top")
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
           +addCardForm(listId=_id position="top")
       each (cardsWithLimit (idOrNull ../../_id))
       each (cardsWithLimit (idOrNull ../../_id))
-        a.minicard-wrapper.js-minicard(href=absoluteUrl
+        a.minicard-wrapper.js-minicard(href=originRelativeUrl
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if cardIsSelected}}is-selected{{/if}}"
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
           if MultiSelection.isActive
           if MultiSelection.isActive
@@ -19,19 +19,14 @@ template(name="listBody")
         +inlinedForm(autoclose=false position="bottom")
         +inlinedForm(autoclose=false position="bottom")
           +addCardForm(listId=_id position="bottom")
           +addCardForm(listId=_id position="bottom")
         else
         else
-          a.open-minicard-composer.js-card-composer.js-open-inlined-form
+          a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
             i.fa.fa-plus
             i.fa.fa-plus
-            | {{_ 'add-card'}}
 
 
 template(name="spinnerList")
 template(name="spinnerList")
-  .sk-spinner.sk-spinner-wave.sk-spinner-list(
-    class=currentBoard.colorClass
+  .sk-spinner.sk-spinner-list(
+    class="{{currentBoard.colorClass}} {{getSkSpinnerName}}"
     id="showMoreResults")
     id="showMoreResults")
-    .sk-rect1
-    .sk-rect2
-    .sk-rect3
-    .sk-rect4
-    .sk-rect5
+    +spinnerRaw
 
 
 template(name="addCardForm")
 template(name="addCardForm")
   .minicard.minicard-composer.js-composer
   .minicard.minicard-composer.js-composer
@@ -105,8 +100,10 @@ template(name="searchElementPopup")
         each boards
         each boards
           option(value="{{_id}}") {{title}}
           option(value="{{_id}}") {{title}}
   form.js-search-term-form
   form.js-search-term-form
+    label
+      | {{_ 'template'}}
     input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
     input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
-  .list-body.js-perfect-scrollbar.search-card-results
+  .list-body.search-card-results
     .minicards.clearfix.js-minicards
     .minicards.clearfix.js-minicards
       if isBoardTemplateSearch
       if isBoardTemplateSearch
         each results
         each results

+ 65 - 48
client/components/lists/listBody.js

@@ -1,3 +1,5 @@
+import { Spinner } from '/client/lib/spinner';
+
 const subManager = new SubsManager();
 const subManager = new SubsManager();
 const InfiniteScrollIter = 10;
 const InfiniteScrollIter = 10;
 
 
@@ -8,7 +10,7 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   mixins() {
   mixins() {
-    return [Mixins.PerfectScrollbar];
+    return [];
   },
   },
 
 
   openForm(options) {
   openForm(options) {
@@ -77,7 +79,7 @@ BlazeComponent.extendComponent({
       else if (
       else if (
         Utils.boardView() === 'board-view-lists' ||
         Utils.boardView() === 'board-view-lists' ||
         Utils.boardView() === 'board-view-cal' ||
         Utils.boardView() === 'board-view-cal' ||
-        !Utils.boardView
+        !Utils.boardView()
       )
       )
         swimlaneId = board.getDefaultSwimline()._id;
         swimlaneId = board.getDefaultSwimline()._id;
 
 
@@ -116,8 +118,6 @@ BlazeComponent.extendComponent({
       if (position === 'bottom') {
       if (position === 'bottom') {
         this.scrollToBottom();
         this.scrollToBottom();
       }
       }
-
-      formComponent.reset();
     }
     }
   },
   },
 
 
@@ -168,13 +168,16 @@ BlazeComponent.extendComponent({
 
 
   cardsWithLimit(swimlaneId) {
   cardsWithLimit(swimlaneId) {
     const limit = this.cardlimit.get();
     const limit = this.cardlimit.get();
+    const defaultSort = { sort: 1 };
+    const sortBy = Session.get('sortBy') ? Session.get('sortBy') : defaultSort;
     const selector = {
     const selector = {
       listId: this.currentData()._id,
       listId: this.currentData()._id,
       archived: false,
       archived: false,
     };
     };
     if (swimlaneId) selector.swimlaneId = swimlaneId;
     if (swimlaneId) selector.swimlaneId = swimlaneId;
     return Cards.find(Filter.mongoSelector(selector), {
     return Cards.find(Filter.mongoSelector(selector), {
-      sort: ['sort'],
+      // sort: ['sort'],
+      sort: sortBy,
       limit,
       limit,
     });
     });
   },
   },
@@ -239,7 +242,7 @@ BlazeComponent.extendComponent({
         .customFields()
         .customFields()
         .fetch(),
         .fetch(),
       function(field) {
       function(field) {
-        if (field.automaticallyOnCard)
+        if (field.automaticallyOnCard || field.alwaysOnCard)
           arr.push({ _id: field._id, value: null });
           arr.push({ _id: field._id, value: null });
       },
       },
     );
     );
@@ -411,7 +414,7 @@ BlazeComponent.extendComponent({
         type: 'board',
         type: 'board',
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
     return boards;
     return boards;
@@ -523,7 +526,7 @@ BlazeComponent.extendComponent({
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   mixins() {
   mixins() {
-    return [Mixins.PerfectScrollbar];
+    return [];
   },
   },
 
 
   onCreated() {
   onCreated() {
@@ -549,7 +552,7 @@ BlazeComponent.extendComponent({
       board = Boards.findOne((Meteor.user().profile || {}).templatesBoardId);
       board = Boards.findOne((Meteor.user().profile || {}).templatesBoardId);
     } else {
     } else {
       // Prefetch first non-current board id
       // Prefetch first non-current board id
-      board = Boards.findOne({
+      board = Boards.find({
         archived: false,
         archived: false,
         'members.userId': Meteor.userId(),
         'members.userId': Meteor.userId(),
         _id: {
         _id: {
@@ -597,7 +600,7 @@ BlazeComponent.extendComponent({
         type: 'board',
         type: 'board',
       },
       },
       {
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
       },
     );
     );
     return boards;
     return boards;
@@ -658,10 +661,7 @@ BlazeComponent.extendComponent({
               _id = element.copy(this.boardId, this.swimlaneId, this.listId);
               _id = element.copy(this.boardId, this.swimlaneId, this.listId);
               // 1.B Linked card
               // 1.B Linked card
             } else {
             } else {
-              delete element._id;
-              element.type = 'cardType-linkedCard';
-              element.linkedId = element.linkedId || element._id;
-              _id = Cards.insert(element);
+              _id = element.link(this.boardId, this.swimlaneId, this.listId);
             }
             }
             Filter.addException(_id);
             Filter.addException(_id);
             // List insertion
             // List insertion
@@ -675,15 +675,21 @@ BlazeComponent.extendComponent({
             element.sort = Boards.findOne(this.boardId)
             element.sort = Boards.findOne(this.boardId)
               .swimlanes()
               .swimlanes()
               .count();
               .count();
-            element.type = 'swimlalne';
+            element.type = 'swimlane';
             _id = element.copy(this.boardId);
             _id = element.copy(this.boardId);
           } else if (this.isBoardTemplateSearch) {
           } else if (this.isBoardTemplateSearch) {
-            board = Boards.findOne(element.linkedId);
-            board.sort = Boards.find({ archived: false }).count();
-            board.type = 'board';
-            board.title = element.title;
-            delete board.slug;
-            _id = board.copy();
+            Meteor.call(
+              'copyBoard',
+              element.linkedId,
+              {
+                sort: Boards.find({ archived: false }).count(),
+                type: 'board',
+                title: element.title,
+              },
+              (err, data) => {
+                _id = data;
+              },
+            );
           }
           }
           Popup.close();
           Popup.close();
         },
         },
@@ -692,7 +698,7 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('searchElementPopup');
 }).register('searchElementPopup');
 
 
-BlazeComponent.extendComponent({
+(class extends Spinner {
   onCreated() {
   onCreated() {
     this.cardlimit = this.parentComponent().cardlimit;
     this.cardlimit = this.parentComponent().cardlimit;
 
 
@@ -720,11 +726,11 @@ BlazeComponent.extendComponent({
         .parentComponent()
         .parentComponent()
         .data()._id;
         .data()._id;
     }
     }
-  },
+  }
 
 
   onRendered() {
   onRendered() {
     this.spinner = this.find('.sk-spinner-list');
     this.spinner = this.find('.sk-spinner-list');
-    this.container = this.$(this.spinner).parents('.js-perfect-scrollbar')[0];
+    this.container = this.$(this.spinner).parents('.list-body')[0];
 
 
     $(this.container).on(
     $(this.container).on(
       `scroll.spinner_${this.swimlaneId}_${this.listId}`,
       `scroll.spinner_${this.swimlaneId}_${this.listId}`,
@@ -735,47 +741,58 @@ BlazeComponent.extendComponent({
     );
     );
 
 
     this.updateList();
     this.updateList();
-  },
+  }
 
 
   onDestroyed() {
   onDestroyed() {
     $(this.container).off(`scroll.spinner_${this.swimlaneId}_${this.listId}`);
     $(this.container).off(`scroll.spinner_${this.swimlaneId}_${this.listId}`);
     $(window).off(`resize.spinner_${this.swimlaneId}_${this.listId}`);
     $(window).off(`resize.spinner_${this.swimlaneId}_${this.listId}`);
-  },
+  }
+
+  checkIdleTime() {
+    return window.requestIdleCallback ||
+    function(handler) {
+      const startTime = Date.now();
+      return setTimeout(function() {
+        handler({
+          didTimeout: false,
+          timeRemaining() {
+            return Math.max(0, 50.0 - (Date.now() - startTime));
+          },
+        });
+      }, 1);
+    };
+  }
 
 
   updateList() {
   updateList() {
     // Use fallback when requestIdleCallback is not available on iOS and Safari
     // Use fallback when requestIdleCallback is not available on iOS and Safari
     // https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/
     // https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/
-    checkIdleTime =
-      window.requestIdleCallback ||
-      function(handler) {
-        const startTime = Date.now();
-        return setTimeout(function() {
-          handler({
-            didTimeout: false,
-            timeRemaining() {
-              return Math.max(0, 50.0 - (Date.now() - startTime));
-            },
-          });
-        }, 1);
-      };
 
 
     if (this.spinnerInView()) {
     if (this.spinnerInView()) {
       this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
       this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
-      checkIdleTime(() => this.updateList());
+      this.checkIdleTime(() => this.updateList());
     }
     }
-  },
+  }
 
 
   spinnerInView() {
   spinnerInView() {
+    // spinner deleted
+    if (!this.spinner.offsetTop) {
+      return false;
+    }
+
     const parentViewHeight = this.container.clientHeight;
     const parentViewHeight = this.container.clientHeight;
     const bottomViewPosition = this.container.scrollTop + parentViewHeight;
     const bottomViewPosition = this.container.scrollTop + parentViewHeight;
 
 
-    const threshold = this.spinner.offsetTop;
+    let spinnerOffsetTop = this.spinner.offsetTop;
 
 
-    // spinner deleted
-    if (!this.spinner.offsetTop) {
-      return false;
+    const addCard = $(this.container).find("a.open-minicard-composer").first()[0];
+    if (addCard !== undefined) {
+      spinnerOffsetTop -= addCard.clientHeight;
     }
     }
 
 
-    return bottomViewPosition > threshold;
-  },
-}).register('spinnerList');
+    return bottomViewPosition > spinnerOffsetTop;
+  }
+
+  getSkSpinnerName() {
+    return "sk-spinner-" + super.getSpinnerName().toLowerCase();
+  }
+}.register('spinnerList'));

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

@@ -1,7 +1,7 @@
 template(name="listHeader")
 template(name="listHeader")
   .list-header.js-list-header(
   .list-header.js-list-header(
     class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}"
     class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}"
-    class="{{#if colorClass}}list-header-{{colorClass}}{{/if}}")
+    class=colorClass)
     +inlinedForm
     +inlinedForm
       +editListTitleForm
       +editListTitleForm
     else
     else
@@ -15,7 +15,7 @@ template(name="listHeader")
           = title
           = title
         if wipLimit.enabled
         if wipLimit.enabled
          |&nbsp;(
          |&nbsp;(
-         span(class="{{#if reachedWipLimit}}highlight{{/if}}") {{cards.count}}
+         span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.count}}
          |/#{wipLimit.value})
          |/#{wipLimit.value})
 
 
         if showCardsCountForList cards.count
         if showCardsCountForList cards.count
@@ -28,12 +28,11 @@ template(name="listHeader")
           div.list-header-menu
           div.list-header-menu
             unless currentUser.isCommentOnly
             unless currentUser.isCommentOnly
               if canSeeAddCard
               if canSeeAddCard
-                a.js-add-card.fa.fa-plus.list-header-plus-icon
-              a.fa.fa-navicon.js-open-list-menu
-          //a.list-header-handle.handle.fa.fa-arrows.js-list-handle
+                a.js-add-card.fa.fa-plus.list-header-plus-icon(title="{{_ 'add-card-to-top-of-list'}}")
+              a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
         else
         else
           a.list-header-menu-icon.fa.fa-angle-right.js-select-list
           a.list-header-menu-icon.fa.fa-angle-right.js-select-list
-          //a.list-header-handle.handle.fa.fa-arrows.js-list-handle
+          a.list-header-handle.handle.fa.fa-arrows.js-list-handle
       else if currentUser.isBoardMember
       else if currentUser.isBoardMember
         if isWatching
         if isWatching
           i.list-header-watch-icon.fa.fa-eye
           i.list-header-watch-icon.fa.fa-eye
@@ -42,10 +41,11 @@ template(name="listHeader")
             //if isBoardAdmin
             //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-icon(class="fa-star{{#unless starred}}-o{{/unless}}")
             if canSeeAddCard
             if canSeeAddCard
-              a.js-add-card.fa.fa-plus.list-header-plus-icon
-            a.fa.fa-navicon.js-open-list-menu
-          if showDesktopDragHandles
-            a.list-header-handle.handle.fa.fa-arrows.js-list-handle
+              a.js-add-card.fa.fa-plus.list-header-plus-icon(title="{{_ 'add-card-to-top-of-list'}}")
+            a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
+          if currentUser.isBoardAdmin
+            if showDesktopDragHandles
+              a.list-header-handle.handle.fa.fa-arrows.js-list-handle
 
 
 template(name="editListTitleForm")
 template(name="editListTitleForm")
   .list-composer
   .list-composer
@@ -116,8 +116,9 @@ template(name="listMorePopup")
       input.inline-input(type="text" readonly value="{{ rootUrl }}")
       input.inline-input(type="text" readonly value="{{ rootUrl }}")
     | {{_ 'added'}}
     | {{_ 'added'}}
     span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
     span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
-    unless currentUser.isWorker
-      a.js-delete {{_ 'delete'}}
+    //unless currentUser.isWorker
+    //  if currentUser.isBoardAdmin
+    //    a.js-delete {{_ 'delete'}}
 
 
 template(name="listDeletePopup")
 template(name="listDeletePopup")
   p {{_ "list-delete-pop"}}
   p {{_ "list-delete-pop"}}
@@ -152,7 +153,7 @@ template(name="setListColorPopup")
   form.edit-label
   form.edit-label
     .palette-colors: each colors
     .palette-colors: each colors
       // note: we use the swimlane palette to have more than just the border
       // note: we use the swimlane palette to have more than just the border
-      span.card-label.palette-color.js-palette-color(class="swimlane-{{color}}")
+      span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
         if(isSelected color)
         if(isSelected color)
           i.fa.fa-check
           i.fa.fa-check
     button.primary.confirm.js-submit {{_ 'save'}}
     button.primary.confirm.js-submit {{_ 'save'}}

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

@@ -1,5 +1,3 @@
-import { Cookies } from 'meteor/ostrio:cookies';
-const cookies = new Cookies();
 let listsColors;
 let listsColors;
 Meteor.startup(() => {
 Meteor.startup(() => {
   listsColors = Lists.simpleSchema()._schema.color.allowedValues;
   listsColors = Lists.simpleSchema()._schema.color.allowedValues;
@@ -74,9 +72,17 @@ BlazeComponent.extendComponent({
     );
     );
   },
   },
 
 
+  exceededWipLimit() {
+    const list = Template.currentData();
+    return (
+      list.getWipLimit('enabled') &&
+      list.getWipLimit('value') < list.cards().count()
+    );
+  },
+
   showCardsCountForList(count) {
   showCardsCountForList(count) {
     const limit = this.limitToShowCardsCount();
     const limit = this.limitToShowCardsCount();
-    return limit > 0 && count > limit;
+    return limit >= 0 && count >= limit;
   },
   },
 
 
   events() {
   events() {
@@ -106,11 +112,15 @@ BlazeComponent.extendComponent({
 }).register('listHeader');
 }).register('listHeader');
 
 
 Template.listHeader.helpers({
 Template.listHeader.helpers({
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   showDesktopDragHandles() {
   showDesktopDragHandles() {
     currentUser = Meteor.user();
     currentUser = Meteor.user();
     if (currentUser) {
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
       return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (cookies.has('showDesktopDragHandles')) {
+    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
       return true;
       return true;
     } else {
     } else {
       return false;
       return false;
@@ -119,6 +129,10 @@ Template.listHeader.helpers({
 });
 });
 
 
 Template.listActionPopup.helpers({
 Template.listActionPopup.helpers({
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   isWipLimitEnabled() {
   isWipLimitEnabled() {
     return Template.currentData().getWipLimit('enabled');
     return Template.currentData().getWipLimit('enabled');
   },
   },
@@ -223,12 +237,45 @@ BlazeComponent.extendComponent({
 Template.listMorePopup.events({
 Template.listMorePopup.events({
   'click .js-delete': Popup.afterConfirm('listDelete', function() {
   'click .js-delete': Popup.afterConfirm('listDelete', function() {
     Popup.close();
     Popup.close();
-    this.allCards().map(card => Cards.remove(card._id));
-    Lists.remove(this._id);
+    // TODO how can we avoid the fetch call?
+    const allCards = this.allCards().fetch();
+    const allCardIds = _.pluck(allCards, '_id');
+    // it's okay if the linked cards are on the same list
+    if (
+      Cards.find({
+        $and: [
+          { listId: { $ne: this._id } },
+          { linkedId: { $in: allCardIds } },
+        ],
+      }).count() === 0
+    ) {
+      allCardIds.map(_id => Cards.remove(_id));
+      Lists.remove(this._id);
+    } else {
+      // TODO: Figure out more informative message.
+      // Popup with a hint that the list cannot be deleted as there are
+      // linked cards. We can adapt the query above so we can list the linked
+      // cards.
+      // Related:
+      //   client/components/cards/cardDetails.js about line 969
+      //   https://github.com/wekan/wekan/issues/2785
+      const message = `${TAPi18n.__(
+        'delete-linked-cards-before-this-list',
+      )} linkedId: ${
+        this._id
+      } at client/components/lists/listHeader.js and https://github.com/wekan/wekan/issues/2785`;
+      alert(message);
+    }
     Utils.goBoardId(this.boardId);
     Utils.goBoardId(this.boardId);
   }),
   }),
 });
 });
 
 
+Template.listHeader.helpers({
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+});
+
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
     this.currentList = this.currentData();
     this.currentList = this.currentData();
@@ -240,7 +287,11 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   isSelected(color) {
   isSelected(color) {
-    return this.currentColor.get() === color;
+    if (this.currentColor.get() === null) {
+      return color === 'white';
+    } else {
+      return this.currentColor.get() === color;
+    }
   },
   },
 
 
   events() {
   events() {

+ 17 - 0
client/components/main/brokenCards.jade

@@ -0,0 +1,17 @@
+template(name="brokenCardsHeaderBar")
+  h1
+    | {{_ 'broken-cards'}}
+
+template(name="brokenCards")
+  if currentUser
+    if searching.get
+      +spinner
+    else if hasResults.get
+      .global-search-results-list-wrapper
+        if hasQueryErrors.get
+          div
+            each msg in errorMessages
+              span.global-search-error-messages
+                = msg
+        else
+          +resultsPaged(this)

+ 18 - 0
client/components/main/brokenCards.js

@@ -0,0 +1,18 @@
+import { CardSearchPagedComponent } from '../../lib/cardSearch';
+
+BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar');
+
+Template.brokenCards.helpers({
+  userId() {
+    return Meteor.userId();
+  },
+});
+
+class BrokenCardsComponent extends CardSearchPagedComponent {
+  onCreated() {
+    super.onCreated();
+
+    Meteor.subscribe('brokenCards', this.sessionId);
+  }
+}
+BrokenCardsComponent.register('brokenCards');

+ 31 - 0
client/components/main/brokenCards.styl

@@ -0,0 +1,31 @@
+.broken-cards-card-wrapper
+  margin-top: 0
+  margin-bottom: 10px
+  border-width: 3px !important
+  border-color: grey !important
+  border-style: solid
+  border-radius: 5px
+  padding: 1.5rem
+  background-color: white
+
+.broken-cards-wrapper
+  max-width: 500px
+  margin-right: auto
+  margin-left: auto
+
+.broken-cards-card-title
+  font-weight: bold
+  //padding: 10px
+
+.broken-cards-context
+  display: inline-block
+
+.broken-cards-context-separator
+  font-weight: bold
+
+.broken-cards-context-list
+  //margin-bottom: 0.7rem
+
+.broken-cards-null
+  color: darkred
+  font-style: italic

+ 57 - 0
client/components/main/dueCards.jade

@@ -0,0 +1,57 @@
+template(name="dueCardsHeaderBar")
+  if currentUser
+    h1
+      i.fa.fa-calendar
+      | {{_ 'dueCards-title'}}
+
+    .board-header-btns.left
+      a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}")
+        i.fa.fa-caret-down
+        if $eq dueCardsView 'me'
+          i.fa.fa-user
+          | {{_ 'dueCardsViewChange-choice-me'}}
+        if $eq dueCardsView 'all'
+          i.fa.fa-users
+          | {{_ 'dueCardsViewChange-choice-all'}}
+
+template(name="dueCardsModalTitle")
+  if currentUser
+    h2
+      i.fa.fa-keyboard-o
+      | {{_ 'dueCards-title'}}
+
+template(name="dueCards")
+  if currentUser
+    if searching.get
+      +spinner
+    else if hasResults.get
+      .global-search-results-list-wrapper
+        if hasQueryErrors.get
+          div
+            each msg in errorMessages
+              span.global-search-error-messages
+                = msg
+        else
+          +resultsPaged(this)
+
+template(name="dueCardsViewChangePopup")
+  if currentUser
+    ul.pop-over-list
+      li
+        with "dueCardsViewChange-choice-me"
+          a.js-due-cards-view-me
+            i.fa.fa-user.colorful
+            | {{_ 'dueCardsViewChange-choice-me'}}
+            if $eq Utils.dueCardsView "me"
+              i.fa.fa-check
+      hr
+      li
+        with "dueCardsViewChange-choice-all"
+          a.js-due-cards-view-all
+            i.fa.fa-users.colorful
+            | {{_ 'dueCardsViewChange-choice-all'}}
+            span.sub-name
+              +viewer
+                | {{_ 'dueCardsViewChange-choice-all-description' }}
+            if $eq Utils.dueCardsView "all"
+              i.fa.fa-check

+ 111 - 0
client/components/main/dueCards.js

@@ -0,0 +1,111 @@
+import { CardSearchPagedComponent } from '../../lib/cardSearch';
+import {
+  OPERATOR_HAS,
+  OPERATOR_SORT,
+  OPERATOR_USER,
+  ORDER_ASCENDING,
+  PREDICATE_DUE_AT,
+} from '../../../config/search-const';
+import { QueryParams } from '../../../config/query-classes';
+
+// const subManager = new SubsManager();
+
+BlazeComponent.extendComponent({
+  dueCardsView() {
+    // eslint-disable-next-line no-console
+    // console.log('sort:', Utils.dueCardsView());
+    return Utils.dueCardsView();
+  },
+
+  events() {
+    return [
+      {
+        'click .js-due-cards-view-change': Popup.open('dueCardsViewChange'),
+      },
+    ];
+  },
+}).register('dueCardsHeaderBar');
+
+Template.dueCards.helpers({
+  userId() {
+    return Meteor.userId();
+  },
+});
+
+BlazeComponent.extendComponent({
+  events() {
+    return [
+      {
+        'click .js-due-cards-view-me'() {
+          Utils.setDueCardsView('me');
+          Popup.close();
+        },
+
+        'click .js-due-cards-view-all'() {
+          Utils.setDueCardsView('all');
+          Popup.close();
+        },
+      },
+    ];
+  },
+}).register('dueCardsViewChangePopup');
+
+class DueCardsComponent extends CardSearchPagedComponent {
+  onCreated() {
+    super.onCreated();
+
+    const queryParams = new QueryParams();
+    queryParams.addPredicate(OPERATOR_HAS, {
+      field: PREDICATE_DUE_AT,
+      exists: true,
+    });
+    // queryParams[OPERATOR_LIMIT] = 5;
+    queryParams.addPredicate(OPERATOR_SORT, {
+      name: PREDICATE_DUE_AT,
+      order: ORDER_ASCENDING,
+    });
+
+    if (Utils.dueCardsView() !== 'all') {
+      queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
+    }
+
+    this.runGlobalSearch(queryParams);
+  }
+
+  dueCardsView() {
+    // eslint-disable-next-line no-console
+    //console.log('sort:', Utils.dueCardsView());
+    return Utils.dueCardsView();
+  }
+
+  sortByBoard() {
+    return this.dueCardsView() === 'board';
+  }
+
+  dueCardsList() {
+    const results = this.getResults();
+    console.log('results:', results);
+    const cards = [];
+    if (results) {
+      results.forEach(card => {
+        cards.push(card);
+      });
+    }
+
+    cards.sort((a, b) => {
+      const x = a.dueAt === null ? new Date('2100-12-31') : a.dueAt;
+      const y = b.dueAt === null ? new Date('2100-12-31') : b.dueAt;
+
+      if (x > y) return 1;
+      else if (x < y) return -1;
+
+      return 0;
+    });
+
+    // eslint-disable-next-line no-console
+    console.log('cards:', cards);
+    return cards;
+  }
+}
+
+DueCardsComponent.register('dueCards');

+ 4 - 0
client/components/main/dueCards.styl

@@ -0,0 +1,4 @@
+.due-cards-dueat-list-wrapper
+  max-width: 500px
+  margin-right: auto
+  margin-left: auto

+ 59 - 12
client/components/main/editor.js

@@ -49,8 +49,8 @@ Template.editor.onRendered(() => {
           ['para', ['ul', 'ol', 'paragraph']],
           ['para', ['ul', 'ol', 'paragraph']],
           ['table', ['table']],
           ['table', ['table']],
           //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
           //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
-          //['insert', ['link', 'picture']], // modal popup has issue somehow :(
-          ['view', ['fullscreen', 'help']],
+          ['insert', ['link']], //, 'picture']], // modal popup has issue somehow :(
+          ['view', ['fullscreen', 'codeview', 'help']],
         ];
         ];
     const cleanPastedHTML = function(input) {
     const cleanPastedHTML = function(input) {
       const badTags = [
       const badTags = [
@@ -91,6 +91,7 @@ Template.editor.onRendered(() => {
     };
     };
     const editor = '.editor';
     const editor = '.editor';
     const selectors = [
     const selectors = [
+      `.js-new-description-form ${editor}`,
       `.js-new-comment-form ${editor}`,
       `.js-new-comment-form ${editor}`,
       `.js-edit-comment ${editor}`,
       `.js-edit-comment ${editor}`,
     ].join(','); // only new comment and edit comment
     ].join(','); // only new comment and edit comment
@@ -144,6 +145,7 @@ Template.editor.onRendered(() => {
                 const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
                 const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
                 const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
                 const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
                 const insertImage = src => {
                 const insertImage = src => {
+                  // process all image upload types to the description/comment window
                   const img = document.createElement('img');
                   const img = document.createElement('img');
                   img.src = src;
                   img.src = src;
                   img.setAttribute('width', '100%');
                   img.setAttribute('width', '100%');
@@ -209,7 +211,16 @@ Template.editor.onRendered(() => {
                 }
                 }
               }
               }
             },
             },
-            onPaste() {
+            onPaste(e) {
+              var clipboardData = e.clipboardData;
+              var pastedData = clipboardData.getData('Text');
+
+              //if pasted data is an image, exit
+              if (!pastedData.length) {
+                e.preventDefault();
+                return;
+              }
+
               // clear up unwanted tag info when user pasted in text
               // clear up unwanted tag info when user pasted in text
               const thisNote = this;
               const thisNote = this;
               const updatePastedText = function(object) {
               const updatePastedText = function(object) {
@@ -233,17 +244,17 @@ Template.editor.onRendered(() => {
             },
             },
           },
           },
           dialogsInBody: true,
           dialogsInBody: true,
-          disableDragAndDrop: true,
+          spellCheck: true,
+          disableGrammar: false,
+          disableDragAndDrop: false,
           toolbar,
           toolbar,
           popover: {
           popover: {
             image: [
             image: [
-              [
-                'image',
-                ['resizeFull', 'resizeHalf', 'resizeQuarter', 'resizeNone'],
-              ],
+              ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
               ['float', ['floatLeft', 'floatRight', 'floatNone']],
               ['float', ['floatLeft', 'floatRight', 'floatNone']],
               ['remove', ['removeMedia']],
               ['remove', ['removeMedia']],
             ],
             ],
+            link: [['link', ['linkDialogShow', 'unlink']]],
             table: [
             table: [
               ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
               ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
               ['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
               ['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
@@ -262,7 +273,38 @@ Template.editor.onRendered(() => {
   }
   }
 });
 });
 
 
-import sanitizeXss from 'xss';
+import DOMPurify from 'dompurify';
+
+// Additional  safeAttrValue function to allow for other specific protocols
+// See https://github.com/leizongmin/js-xss/issues/52#issuecomment-241354114
+
+/*
+function mySafeAttrValue(tag, name, value, cssFilter) {
+  // only when the tag is 'a' and attribute is 'href'
+  // then use your custom function
+  if (tag === 'a' && name === 'href') {
+    // only filter the value if starts with 'cbthunderlink:' or 'aodroplink'
+    if (
+      /^thunderlink:/gi.test(value) ||
+      /^cbthunderlink:/gi.test(value) ||
+      /^aodroplink:/gi.test(value) ||
+      /^onenote:/gi.test(value) ||
+      /^file:/gi.test(value) ||
+      /^abasurl:/gi.test(value) ||
+      /^conisio:/gi.test(value) ||
+      /^mailspring:/gi.test(value)
+    ) {
+      return value;
+    } else {
+      // use the default safeAttrValue function to process all non cbthunderlinks
+      return sanitizeXss.safeAttrValue(tag, name, value, cssFilter);
+    }
+  } else {
+    // use the default safeAttrValue function to process it
+    return sanitizeXss.safeAttrValue(tag, name, value, cssFilter);
+  }
+}
+*/
 
 
 // XXX I believe we should compute a HTML rendered field on the server that
 // XXX I believe we should compute a HTML rendered field on the server that
 // would handle markdown and user mentions. We can simply have two
 // would handle markdown and user mentions. We can simply have two
@@ -277,7 +319,10 @@ Blaze.Template.registerHelper(
     const view = this;
     const view = this;
     let content = Blaze.toHTML(view.templateContentBlock);
     let content = Blaze.toHTML(view.templateContentBlock);
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
-    if (!currentBoard) return HTML.Raw(sanitizeXss(content));
+    if (!currentBoard)
+      return HTML.Raw(
+        DOMPurify.sanitize(content, { ALLOW_UNKNOWN_PROTOCOLS: true }),
+      );
     const knowedUsers = currentBoard.members.map(member => {
     const knowedUsers = currentBoard.members.map(member => {
       const u = Users.findOne(member.userId);
       const u = Users.findOne(member.userId);
       if (u) {
       if (u) {
@@ -321,7 +366,9 @@ Blaze.Template.registerHelper(
       content = content.replace(fullMention, Blaze.toHTML(link));
       content = content.replace(fullMention, Blaze.toHTML(link));
     }
     }
 
 
-    return HTML.Raw(sanitizeXss(content));
+    return HTML.Raw(
+      DOMPurify.sanitize(content, { ALLOW_UNKNOWN_PROTOCOLS: true }),
+    );
   }),
   }),
 );
 );
 
 
@@ -330,7 +377,7 @@ Template.viewer.events({
   // the corresponding text). Clicking a link shouldn't fire these actions, stop
   // the corresponding text). Clicking a link shouldn't fire these actions, stop
   // we stop these event at the viewer component level.
   // we stop these event at the viewer component level.
   'click a'(event, templateInstance) {
   'click a'(event, templateInstance) {
-    let prevent = true;
+    const prevent = true;
     const userId = event.currentTarget.dataset.userid;
     const userId = event.currentTarget.dataset.userid;
     if (userId) {
     if (userId) {
       Popup.open('member').call({ userId }, event, templateInstance);
       Popup.open('member').call({ userId }, event, templateInstance);

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