Pārlūkot izejas kodu

merged with wekan master @ v5.38

Stefan Maaßen 3 gadi atpakaļ
vecāks
revīzija
cb418f5e23
100 mainītis faili ar 8449 papildinājumiem un 8743 dzēšanām
  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"
 
-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 \
     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 \
     METEOR_EDGE=1.5-beta.17 \
     NPM_VERSION=latest \
@@ -15,16 +15,20 @@ ENV \
     ARCHITECTURE=linux-x64 \
     SRC_PATH=./ \
     WITH_API=true \
+    RESULTS_PER_PAGE="" \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
     ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
-    RICHER_CARD_COMMENT_EDITOR=true \
+    RICHER_CARD_COMMENT_EDITOR=false \
+    CARD_OPENED_WEBHOOK_ENABLED=false \
+    ATTACHMENTS_STORE_PATH="" \
     MAX_IMAGE_PIXEL="" \
     IMAGE_COMPRESS_RATIO="" \
-    BIGEVENTS_PATTERN="" \
+    NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
+    BIGEVENTS_PATTERN=NONE \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     NOTIFY_DUE_AT_HOUR_OF_DAY="" \
     EMAIL_NOTIFICATION_TIMEOUT=30000 \
@@ -36,6 +40,8 @@ ENV \
     TRUSTED_URL="" \
     WEBHOOKS_ATTRIBUTES="" \
     OAUTH2_ENABLED=false \
+    OAUTH2_CA_CERT="" \
+    OAUTH2_ADFS_ENABLED=false \
     OAUTH2_LOGIN_STYLE=redirect \
     OAUTH2_CLIENT_ID="" \
     OAUTH2_SECRET="" \
@@ -108,23 +114,40 @@ ENV \
     CORS="" \
     CORS_ALLOW_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
 RUN set -o xtrace \
   && useradd --user-group -m --system --home-dir /home/wekan wekan \
   && 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
 RUN set -o xtrace \
   && 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 \
   && mkdir -p /usr/local/lib/node_modules/fibers/.node-gyp /root/.node-gyp/${NODE_VERSION} /home/wekan/.config \
   && npm install -g npm@${NPM_VERSION} \
@@ -146,17 +169,65 @@ RUN set -o xtrace \
 
 ENV PATH=$PATH:/home/wekan/.meteor/
 
-# Copy source dir
 USER root
 
 RUN echo "export PATH=$PATH" >> /etc/environment
 
+USER wekan
+
+# Copy source dir
 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 \
   && chown -R wekan:wekan /home/wekan/app /home/wekan/.meteor
 
 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:
 
   wekandb-dev:
-    image: mongo:4.0.12
+    image: mongo:4.4
     container_name: wekan-dev-db
     restart: unless-stopped
-    command: mongod --smallfiles --oplogSize 128
+    command: mongod --oplogSize 128
     networks:
       - wekan-dev-tier
     expose:
       - 27017
     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:
     container_name: wekan-dev-app
@@ -35,9 +36,13 @@ services:
     depends_on:
       - wekandb-dev
     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:
   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,
     "meteor": true
   },
+  "parser": "babel-eslint",
   "parserOptions": {
     "ecmaVersion": 2018,
     "sourceType": "module"
@@ -44,7 +45,7 @@
     "no-spaced-func": 2,
     "no-trailing-spaces": 2,
     "operator-linebreak": 2,
-    "quotes": [2, "single"],
+    "quotes": [2, "single", { "avoidEscape": true }],
     "semi-spacing": 2,
     "space-unary-ops": 2,
     "arrow-spacing": 2,

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

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

+ 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:
         source: .
         plugin: nodejs
-        node-engine: 12.14.1
+        node-engine: 12.22.3
         node-packages:
             - node-gyp
             - node-pre-gyp

+ 10 - 2
.github/ISSUE_TEMPLATE.md

@@ -1,8 +1,16 @@
 ## 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:
-- 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
 
 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/
 node_modules/
 npm-debug.log
+.gitmodules
 .vscode/
 .idea/
 .build/*
@@ -30,5 +31,6 @@ Thumbs.db
 ehthumbs.db
 .eslintcache
 .meteor/local
-.meteor-1.6-snap/.meteor/local
 .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
 
 # 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
 mquandalle:jade
 coffeescript@2.4.1!
@@ -17,13 +17,13 @@ es5-shim@4.8.0
 
 # Collections
 aldeed:collection2
-cfs:standard-packages
+wekan-cfs-standard-packages
 cottz:publish-relations
 dburles:collection-helpers
 idmontie:migrations
 matb33:collection-hooks
 matteodem:easy-search
-mongo@1.9.0
+mongo@1.11.0
 mquandalle:collection-mutations
 
 # Account system
@@ -70,24 +70,19 @@ rajit:bootstrap3-datepicker
 shell-server@0.5.0
 simple:rest-accounts-password
 useraccounts:core
-email@1.2.3
+email@2.0.0
 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
 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
@@ -95,6 +90,60 @@ meteorhacks:aggregate@1.3.0
 wekan-markdown
 konecty:mongo-counter
 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
-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
-accounts-base@1.6.0
+accounts-base@1.9.0
 accounts-oauth@1.2.0
-accounts-password@1.6.0
+accounts-password@1.7.1
 aldeed:collection2@2.10.0
 aldeed:collection2-core@1.2.0
 aldeed:schema-deny@1.1.0
@@ -10,37 +10,20 @@ aldeed:simple-schema@1.5.4
 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.5.3
+autoupdate@1.7.0
+babel-compiler@7.6.1
 babel-runtime@1.5.0
 base64@1.0.12
 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-framing@1.1.0
 caching-compiler@1.2.2
-caching-html-compiler@1.1.3
+caching-html-compiler@1.2.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-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
@@ -49,20 +32,20 @@ coffeescript-compiler@2.4.1
 cottz:publish-relations@2.0.8
 dburles:collection-helpers@1.1.0
 ddp@1.4.0
-ddp-client@2.3.3
+ddp-client@2.4.1
 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
 diff-sequence@1.1.1
-dynamic-import@0.5.2
+dynamic-import@0.6.0
 easylogic:summernote@0.8.8
-ecmascript@0.14.3
+ecmascript@0.15.1
 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
-email@1.2.3
+email@2.0.0
 es5-shim@4.8.0
 fastclick@1.0.13
 fetch@0.1.1
@@ -70,10 +53,10 @@ 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
+html-tools@1.1.2
+htmljs@1.1.1
+http@1.4.4
+id-map@1.1.1
 idmontie:migrations@1.0.3
 inter-process-messaging@0.1.1
 jquery@1.11.11
@@ -84,14 +67,13 @@ 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.2.0
+launch-screen@1.2.1
 livedata@1.0.18
+lmieulet:meteor-coverage@3.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
 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
@@ -101,19 +83,22 @@ 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.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
 minifiers@1.1.8-faster-rebuild.0
-minimongo@1.5.0
+minimongo@1.6.2
 mobile-status-bar@1.1.0
 modern-browsers@0.1.5
-modules@0.15.0
+modules@0.16.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-id@1.0.7
+mongo-id@1.0.8
 mongo-livedata@1.0.12
 mousetrap:mousetrap@1.4.6_1
 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: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.7.0
-oauth@1.3.0
+npm-bcrypt@0.9.4
+npm-mongo@3.9.0
+oauth@1.3.2
 oauth2@1.3.0
 observe-sequence@1.0.16
 ongoworks:speakingurl@1.1.0
 ordered-dict@1.1.0
-ostrio:cookies@2.6.0
+pascoual:pdfkit@1.0.7
 peerlibrary:assert@0.3.0
 peerlibrary:base-component@0.16.0
 peerlibrary:blaze-components@0.15.1
@@ -144,11 +128,60 @@ promise@0.11.2
 raix:eventemitter@0.1.3
 raix:handlebar-helpers@0.2.5
 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
 rate-limit@1.0.9
+react-fast-refresh@0.1.1
 reactive-dict@1.3.0
 reactive-var@1.0.11
-reload@1.3.0
+reload@1.3.1
 retry@1.1.0
 routepolicy@1.1.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-bearer-token-parser@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
-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
-staringatlights:fast-render@3.2.0
+staringatlights:fast-render@3.3.0
 staringatlights:inject-data@2.3.0
+steffo:meteor-accounts-saml@0.0.18
 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
+templating@1.4.0
+templating-compiler@1.4.1
+templating-runtime@1.4.0
+templating-tools@1.2.0
 tracker@1.2.0
 twbs:bootstrap@3.3.6
 ui@1.0.13
 underscore@1.0.10
-url@1.2.0
+url@1.3.2
 useraccounts:core@1.14.2
 useraccounts:flow-routing@1.14.2
 useraccounts:unstyled@1.14.2
 verron:autosize@3.0.8
-webapp@1.9.1
-webapp-hashing@1.0.9
+webapp@1.10.1
+webapp-hashing@1.1.0
 wekan-accounts-cas@0.1.0
+wekan-accounts-lockout@1.0.0
 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-markdown@1.0.7
+wekan-markdown@1.0.9
 wekan-oidc@1.0.12
-wekan-scrollbar@3.1.3
 yasaricli:slugify@0.0.7
 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
 
 env:
   TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
-  TRAVIS_NODE_VERSION: 12.15.0
+  TRAVIS_NODE_VERSION: 12.22.3
   TRAVIS_NPM_VERSION: latest
 
 before_install:

+ 1 - 1
.tx/config

@@ -39,7 +39,7 @@ host = https://www.transifex.com
 # tap:i18n requires us to use `-` separator in the language identifiers whereas
 # Transifex uses a `_` separator, without an option to customize it on one side
 # or the other, so we need to do a Manual mapping.
-lang_map = 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]
 file_filter = i18n/<lang>.i18n.json

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2910 - 0
CHANGELOG.md


+ 37 - 7
Dockerfile

@@ -1,13 +1,19 @@
-FROM ubuntu:rolling
+FROM quay.io/wekan/ubuntu:groovy-20210115
 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)
 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
 # 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" \
     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 \
     METEOR_EDGE=1.5-beta.17 \
     NPM_VERSION=latest \
@@ -15,6 +21,7 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     ARCHITECTURE=linux-x64 \
     SRC_PATH=./ \
     WITH_API=true \
+    RESULTS_PER_PAGE="" \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
     ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
     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="" \
     MAX_IMAGE_PIXEL="" \
     IMAGE_COMPRESS_RATIO="" \
+    NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
     BIGEVENTS_PATTERN=NONE \
     NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
     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="" \
     WEBHOOKS_ATTRIBUTES="" \
     OAUTH2_ENABLED=false \
+    OAUTH2_CA_CERT="" \
+    OAUTH2_ADFS_ENABLED=false \
     OAUTH2_LOGIN_STYLE=redirect \
     OAUTH2_CLIENT_ID="" \
     OAUTH2_SECRET="" \
@@ -111,8 +121,24 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
     CORS_ALLOW_HEADERS="" \
     CORS_EXPOSE_HEADERS="" \
     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 ${SRC_PATH} /home/wekan/app
@@ -250,11 +276,12 @@ RUN \
     mkdir -p /home/wekan/.npm && \
     chown wekan --recursive /home/wekan/.npm /home/wekan/.config /home/wekan/.meteor && \
     #gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
+    chmod u+w *.json && \
     gosu wekan:wekan npm install && \
     gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
-    cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
+    #cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
     #rm /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs && \
-    chown wekan /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.
     #https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
     #https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
@@ -267,8 +294,11 @@ RUN \
     #find . -name "*phantomjs*" | xargs rm -rf && \
     #
     cd /home/wekan/app_build/bundle/programs/server/ && \
+    chmod u+w *.json && \
     gosu wekan:wekan npm install && \
     #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 && \
     \
     # 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
 
 [![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)
 [![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)
+[![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)
 
@@ -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)
 
-Please add most of your questions as GitHub issue: [Wekan feature requests and bugs](https://github.com/wekan/wekan/issues).
+Please add most of your questions as GitHub issue: [Wekan Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
 It's better than at chat where details get lost when chat scrolls up.
 
 ## Chat
 
-[![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)
 
+## 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
 
-**NOTE**: 
+**NOTE**:
 - Please read the [FAQ](https://github.com/wekan/wekan/wiki/FAQ) first
-- Please don't feed the trolls and spammers that are mentioned in the FAQ :)
+- 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
 
@@ -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 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.
 - [Platforms][platforms]: Wekan supports many platforms.
   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.
 - 1 GB RAM minimum free for Wekan. Production server should have minimum total 4 GB RAM.
   For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/master/docker-compose.yml): 3 frontend servers,
-  each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.  
+  each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
 - Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
 - SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
   Old versions have security issues because of old versions Node.js etc. Only newest Wekan is supported.
@@ -112,8 +119,6 @@ with [Meteor](https://www.meteor.com).
 [translate_wekan]: https://www.transifex.com/wekan/wekan/
 [open_source]: https://en.wikipedia.org/wiki/Open-source_software
 [free_software]: https://en.wikipedia.org/wiki/Free_software
-[vanila_badge]: https://vanila.io/img/join-chat-button2.png
-[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)

+ 3 - 3
SECURITY.md

@@ -1,10 +1,10 @@
 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.
 
 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?
 

+ 1 - 1
Stackerfile.yml

@@ -1,5 +1,5 @@
 appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
-appVersion: "v3.90.0"
+appVersion: "v5.38.0"
 files:
   userUploads:
     - 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
     +activity(activity=activityData card=card mode=mode)
 
+template(name="editOrDeleteComment")
+  = ' - '
+  a.js-open-inlined-form {{_ "edit"}}
+  = ' - '
+  a.js-delete-comment {{_ "delete"}}
+
 template(name="activity")
   .activity
     +userAvatar(userId=activity.user._id)
     p.activity-desc
-      +memberName(user=activity.user)
+      span.activity-member
+        +memberName(user=activity.user)
 
       //- attachment activity -------------------------------------------------
       if($eq activity.activityType 'deleteAttachment')
@@ -34,38 +41,38 @@ template(name="activity")
       //- board activity ------------------------------------------------------
       if($eq mode 'board')
         if($eq activity.activityType 'createBoard')
-          | {{_ 'activity-created' boardLabel}}.
+          | {{{_ 'activity-created' boardLabelLink}}}.
 
         if($eq activity.activityType 'importBoard')
-          | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
+          | {{{_ 'activity-imported-board' boardLabelLink sourceLink}}}.
 
         if($eq activity.activityType 'addBoardMember')
-          | {{{_ 'activity-added' memberLink boardLabel}}}.
+          | {{{_ 'activity-added' memberLink boardLabelLink}}}.
 
         if($eq activity.activityType 'removeBoardMember')
-          | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+          | {{{_ 'activity-excluded' memberLink boardLabelLink}}}.
 
       //- card activity -------------------------------------------------------
       if($eq activity.activityType 'createCard')
         if($eq mode 'card')
-          | {{{_ 'activity-added' cardLabel activity.listName}}}.
+          | {{{_ 'activity-added' cardLabelLink (sanitize activity.listName)}}}.
         else
-          | {{{_ 'activity-added' cardLabel boardLabel}}}.
+          | {{{_ 'activity-added' cardLabelLink boardLabelLink}}}.
 
       if($eq activity.activityType 'importCard')
-        | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
+        | {{{_ 'activity-imported' cardLink boardLabelLink sourceLink}}}.
 
       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')
-        | {{{_ 'activity-moved' cardLink activity.oldBoardName activity.boardName}}}.
+        | {{{_ 'activity-moved' cardLink (sanitize activity.oldBoardName) (sanitize activity.boardName)}}}.
 
       if($eq activity.activityType 'archivedCard')
         | {{{_ 'activity-archived' cardLink}}}.
 
       if($eq activity.activityType 'restoredCard')
-        | {{{_ 'activity-sent' cardLink boardLabel}}}.
+        | {{{_ 'activity-sent' cardLink boardLabelLink}}}.
 
       //- checklist activity --------------------------------------------------
       if($eq activity.activityType 'addChecklist')
@@ -75,7 +82,7 @@ template(name="activity")
             +viewer
               = activity.checklist.title
         else
-          a.activity-checklist(href="{{ activity.card.absoluteUrl }}")
+          a.activity-checklist(href="{{ activity.card.originRelativeUrl }}")
             +viewer
               = activity.checklist.title
 
@@ -83,25 +90,25 @@ template(name="activity")
         | {{{_ 'activity-checklist-removed' cardLink}}}.
 
       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')
-        | {{{_ 'activity-checklist-uncompleted' activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-checklist-uncompleted' (sanitize activity.checklist.title) cardLink}}}.
 
       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')
-        | {{{_ 'activity-unchecked-item' checkItem activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-unchecked-item' (sanitize checkItem) (sanitize activity.checklist.title) cardLink}}}.
 
       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
             = activity.checklistItem.title
 
       if($eq activity.activityType 'removedChecklistItem')
-        | {{{_ 'activity-checklist-item-removed' activity.checklist.title cardLink}}}.
+        | {{{_ 'activity-checklist-item-removed' (sanitize activity.checklist.title) cardLink}}}.
 
       //- comment activity ----------------------------------------------------
       if($eq mode 'card')
@@ -118,11 +125,10 @@ template(name="activity")
               +viewer
                 = activity.comment.text
             span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
-              if ($eq currentUser._id activity.comment.userId)
-                = ' - '
-                a.js-open-inlined-form {{_ "edit"}}
-                = ' - '
-                a.js-delete-comment {{_ "delete"}}
+              if($eq currentUser._id activity.comment.userId)
+                +editOrDeleteComment
+              else if currentUser.isBoardAdmin
+                +editOrDeleteComment
 
         if($eq activity.activityType 'deleteComment')
           | {{{_ '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($eq activity.activityType 'addComment')
           | {{{_ 'activity-on' cardLink}}}
-          a.activity-comment(href="{{ activity.card.absoluteUrl }}")
+          a.activity-comment(href="{{ activity.card.originRelativeUrl }}")
             +viewer
               = 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 ------------------------------------------------
       if($eq mode 'board')
         if($eq activity.activityType 'createCustomField')
           | {{_ 'activity-customfield-created' customField}}.
 
         if($eq activity.activityType 'setCustomField')
-          | {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
+          | {{{_ 'activity-set-customfield' (sanitize lastCustomField) (sanitize lastCustomFieldValue) cardLink}}}.
 
         if($eq activity.activityType 'unsetCustomField')
-          | {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
+          | {{{_ 'activity-unset-customfield' (sanitize lastCustomField) cardLink}}}.
 
       //- label activity ------------------------------------------------------
       if($eq activity.activityType 'addedLabel')
-        | {{{_ 'activity-added-label' lastLabel cardLink}}}.
+        | {{{_ 'activity-added-label' (sanitize lastLabel) cardLink}}}.
 
       if($eq activity.activityType 'removedLabel')
-        | {{{_ 'activity-removed-label' lastLabel cardLink}}}.
+        | {{{_ 'activity-removed-label' (sanitize lastLabel) cardLink}}}.
 
       //- list activity -------------------------------------------------------
       if($neq mode 'card')
         if($eq activity.activityType 'createList')
-          | {{{_ 'activity-added' listLabel boardLabel}}}.
+          | {{{_ 'activity-added' (sanitize listLabel) boardLabelLink}}}.
 
         if($eq activity.activityType 'importList')
-          | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
+          | {{{_ 'activity-imported' (sanitize listLabel) boardLabelLink sourceLink}}}.
 
         if($eq activity.activityType 'removeList')
-          | {{{_ 'activity-removed' activity.title boardLabel}}}.
+          | {{{_ 'activity-removed' (sanitize activity.title) boardLabelLink}}}.
 
         if($eq activity.activityType 'archivedList')
-          | {{_ 'activity-archived' listLabel}}.
+          | {{_ 'activity-archived' (sanitize listLabel)}}.
 
       //- member activity ----------------------------------------------------
       if($eq activity.activityType 'joinMember')
@@ -185,15 +218,15 @@ template(name="activity")
       //- swimlane activity --------------------------------------------------
       if($neq mode 'card')
         if($eq activity.activityType 'createSwimlane')
-          | {{{_ 'activity-added' activity.swimlane.title boardLabel}}}.
+          | {{_ 'activity-added' (sanitize activity.swimlane.title) boardLabelLink}}.
 
         if($eq activity.activityType 'archivedSwimlane')
-          | {{_ 'activity-archived' activity.swimlane.title}}.
+          | {{_ 'activity-archived' (sanitize activity.swimlane.title)}}.
 
 
       //- I don't understand this part ----------------------------------------
       if(currentData.timeKey)
-        | {{{_ activity.activityType }}}
+        | {{_ activity.activityType }}
         = ' '
         i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
         if (currentData.timeOldValue)
@@ -203,6 +236,6 @@ template(name="activity")
             i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
         = ' @'
       else if(currentData.timeValue)
-        | {{{_ activity.activityType currentData.timeValue}}}
+        | {{_ activity.activityType currentData.timeValue}}
 
       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({
   onCreated() {
     // XXX Should we use ReactiveNumber?
     this.page = new ReactiveVar(1);
     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(() => {
       let mode = this.data().mode;
       const capitalizedMode = Utils.capitalize(mode);
@@ -27,6 +30,8 @@ BlazeComponent.extendComponent({
       this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
         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
         // activities, and we can stop calling new subscriptions.
         // XXX This is hacky! We need to know excatly and reactively how many
@@ -41,23 +46,22 @@ BlazeComponent.extendComponent({
       });
     });
   },
-}).register('activities');
-
-BlazeComponent.extendComponent({
   loadNextPage() {
     if (this.loadNextPageLocked === false) {
       this.page.set(this.page.get() + 1);
       this.loadNextPageLocked = true;
     }
   },
+}).register('activities');
 
+BlazeComponent.extendComponent({
   checkItem() {
     const checkItemId = this.currentData().activity.checklistItemId;
     const checkItem = ChecklistItems.findOne({ _id: checkItemId });
     return checkItem && checkItem.title;
   },
 
-  boardLabel() {
+  boardLabelLink() {
     const data = this.currentData();
     if (data.mode !== 'board') {
       return createBoardLink(data.activity.board(), data.activity.listName);
@@ -65,10 +69,10 @@ BlazeComponent.extendComponent({
     return TAPi18n.__('this-board');
   },
 
-  cardLabel() {
+  cardLabelLink() {
     const data = this.currentData();
     if (data.mode !== 'card') {
-      return createCardLink(this.currentData().activity.card());
+      return createCardLink(data.activity.card());
     }
     return TAPi18n.__('this-card');
   },
@@ -77,6 +81,30 @@ BlazeComponent.extendComponent({
     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() {
     const lastLabelId = this.currentData().activity.labelId;
     if (!lastLabelId) return null;
@@ -134,11 +162,15 @@ BlazeComponent.extendComponent({
             {
               href: source.url,
             },
-            source.system,
+            DOMPurify.sanitize(source.system, {
+              ALLOW_UNKNOWN_PROTOCOLS: true,
+            }),
           ),
         );
       } else {
-        return source.system;
+        return DOMPurify.sanitize(source.system, {
+          ALLOW_UNKNOWN_PROTOCOLS: true,
+        });
       }
     }
     return null;
@@ -162,10 +194,10 @@ BlazeComponent.extendComponent({
               href: attachment.url({ download: true }),
               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
         'click .js-delete-comment'() {
-          const commentId = this.currentData().commentId;
+          const commentId = this.currentData().activity.commentId;
           CardComments.remove(commentId);
         },
         'submit .js-edit-comment'(evt) {
@@ -188,7 +220,7 @@ BlazeComponent.extendComponent({
           const commentText = this.currentComponent()
             .getValue()
             .trim();
-          const commentId = Template.parentData().commentId;
+          const commentId = Template.parentData().activity.commentId;
           if (commentText) {
             CardComments.update(commentId, {
               $set: {
@@ -202,16 +234,23 @@ BlazeComponent.extendComponent({
   },
 }).register('activity');
 
+Template.activity.helpers({
+  sanitize(value) {
+    return DOMPurify.sanitize(value, { ALLOW_UNKNOWN_PROTOCOLS: true });
+  },
+});
+
 function createCardLink(card) {
+  if (!card) return '';
   return (
     card &&
     Blaze.toHTML(
       HTML.A(
         {
-          href: card.absoluteUrl(),
+          href: card.originRelativeUrl(),
           class: 'action-card',
         },
-        card.title,
+        DOMPurify.sanitize(card.title, { ALLOW_UNKNOWN_PROTOCOLS: true }),
       ),
     )
   );
@@ -225,10 +264,10 @@ function createBoardLink(board, list) {
     Blaze.toHTML(
       HTML.A(
         {
-          href: board.absoluteUrl(),
+          href: board.originRelativeUrl(),
           class: 'action-board',
         },
-        text,
+        DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
       ),
     )
   );

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

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

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

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

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

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

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

@@ -3,11 +3,15 @@ BlazeComponent.extendComponent({
     this.subscribe('archivedBoards');
   },
 
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   archivedBoards() {
     return Boards.find(
       { 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")
   .board-wrapper(class=currentBoard.colorClass)
     +sidebar
-    .board-canvas.js-swimlanes.js-perfect-scrollbar(
+    .board-canvas.js-swimlanes(
       class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
       class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
       class="{{#if draggingActive.get}}is-dragging-active{{/if}}")

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

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

@@ -15,7 +15,8 @@ setBoardColor(color)
   .is-selected .minicard
     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%)
 
   &.pop-over .pop-over-list li a:not(.disabled):hover,
@@ -293,3 +294,770 @@ setBoardColor(color)
 
   //.header-quick-access
   //  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")
   h1.header-board-menu
     with currentBoard
-      a(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}")
+      if $eq title 'Templates'
+        | {{_ 'templates'}}
+      else
         +viewer
           = title
 
@@ -9,6 +11,10 @@ template(name="boardHeaderBar")
     unless isMiniScreen
       if currentBoard
         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}}"
             title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
             i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
@@ -31,6 +37,12 @@ template(name="boardHeaderBar")
             if $eq watchLevel "muted"
               i.fa.fa-bell-slash
             span {{_ watchLevel}}
+          a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
+            i.fa.fa-sort
+            span {{#if isSortActive }}{{_ 'Sort is on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
+            if isSortActive
+              a.board-header-btn-close.js-sort-reset(title="Remove Sort")
+                i.fa.fa-times-thin
 
         else
           a.board-header-btn.js-log-in(
@@ -42,6 +54,10 @@ template(name="boardHeaderBar")
     if currentBoard
       if isMiniScreen
         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}}"
             title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
             i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
@@ -99,13 +115,13 @@ template(name="boardHeaderBar")
         a.board-header-btn.js-toggle-board-view(
           title="{{_ 'board-view'}}")
           i.fa.fa-caret-down
-          if $eq boardView 'board-view-lists'
-            i.fa.fa-trello
           if $eq boardView 'board-view-swimlanes'
             i.fa.fa-th-large
+          if $eq boardView 'board-view-lists'
+            i.fa.fa-trello
           if $eq boardView 'board-view-cal'
             i.fa.fa-calendar
-          span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-lists'}}{{/if}}
+          span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}}
 
       if canModifyBoard
         a.board-header-btn.js-multiselection-activate(
@@ -118,7 +134,7 @@ template(name="boardHeaderBar")
               i.fa.fa-times-thin
 
       .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
 
 template(name="boardVisibilityList")
@@ -172,13 +188,6 @@ template(name="boardChangeWatchPopup")
 
 template(name="boardChangeViewPopup")
   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
       with "board-view-swimlanes"
         a.js-open-swimlanes-view
@@ -186,6 +195,13 @@ template(name="boardChangeViewPopup")
           | {{_ 'board-view-swimlanes'}}
           if $eq Utils.boardView "board-view-swimlanes"
             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
       with "board-view-cal"
         a.js-open-cal-view
@@ -212,6 +228,9 @@ template(name="createBoard")
           = " "
           | {{{_ 'board-private-info'}}}
         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'}}")
     span.quiet
       | {{_ 'or'}}
@@ -247,3 +266,19 @@ template(name="boardChangeTitlePopup")
 template(name="boardCreateRulePopup")
   p {{_ 'close-board-pop'}}
   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 UPCLS = 'fa-sort-up';
 */
+const sortCardsBy = new ReactiveVar('');
 Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
   'click .js-custom-fields'() {
@@ -33,22 +34,6 @@ Template.boardMenuPopup.events({
   '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({
   submit(event, templateInstance) {
     const newTitle = templateInstance
@@ -126,6 +111,7 @@ BlazeComponent.extendComponent({
         'click .js-open-filter-view'() {
           Sidebar.setView('filter');
         },
+        'click .js-sort-cards': Popup.open('cardsSort'),
         /*
         'click .js-open-sort-view'(evt) {
           const target = evt.target;
@@ -143,6 +129,9 @@ BlazeComponent.extendComponent({
           Sidebar.setView();
           Filter.reset();
         },
+        'click .js-sort-reset'() {
+          Session.set('sortBy', '');
+        },
         'click .js-open-search-view'() {
           Sidebar.setView('search');
         },
@@ -176,6 +165,9 @@ Template.boardHeaderBar.helpers({
   boardView() {
     return Utils.boardView();
   },
+  isSortActive() {
+    return Session.get('sortBy') ? true : false;
+  },
 });
 
 Template.boardChangeViewPopup.events({
@@ -217,24 +209,79 @@ const CreateBoard = BlazeComponent.extendComponent({
     this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
   },
 
+  toggleAddTemplateContainer() {
+    $('#add-template-container').toggleClass('is-checked');
+  },
+
   onSubmit(event) {
     event.preventDefault();
     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() {
@@ -248,6 +295,7 @@ const CreateBoard = BlazeComponent.extendComponent({
         submit: this.onSubmit,
         'click .js-import-board': Popup.open('chooseBoardSource'),
         'click .js-board-template': Popup.open('searchElement'),
+        'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
       },
     ];
   },
@@ -384,3 +432,44 @@ BlazeComponent.extendComponent({
   },
 }).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")
   .wrapper
-    ul.board-list.clearfix
+    ul.board-list.clearfix.js-boards
       li.js-add-board
-        a.board-list-item.label {{_ 'add-board'}}
+        a.board-list-item.label(title="{{_ 'add-board'}}")
+          | {{_ 'add-board'}}
       each boards
-        li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
+        li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
           if isInvited
             .board-list-item
               span.details
@@ -16,50 +17,97 @@ template(name="boardList")
                 button.js-accept-invite.primary {{_ 'accept'}}
                 button.js-decline-invite {{_ 'decline'}}
           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")
-  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 { calculateIndex, enableClickOnTouch } = Utils;
 
 Template.boardListHeaderBar.events({
   'click .js-open-archived-board'() {
@@ -7,6 +8,9 @@ Template.boardListHeaderBar.events({
 });
 
 Template.boardListHeaderBar.helpers({
+  title() {
+    return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public';
+  },
   templatesBoardId() {
     return Meteor.user() && Meteor.user().getTemplatesBoardId();
   },
@@ -18,22 +22,91 @@ Template.boardListHeaderBar.helpers({
 BlazeComponent.extendComponent({
   onCreated() {
     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() {
     const user = Meteor.user();
     return user && user.hasStarred(this.currentData()._id);
   },
+  isAdministrable() {
+    const user = Meteor.user();
+    return user && user.isBoardAdmin(this.currentData()._id);
+  },
 
   hasOvertimeCards() {
     subManager.subscribe('board', this.currentData()._id, false);
@@ -61,9 +134,13 @@ BlazeComponent.extendComponent({
         },
         'click .js-clone-board'(evt) {
           Meteor.call(
-            'cloneBoard',
+            'copyBoard',
             this.currentData()._id,
-            Session.get('fromBoard'),
+            {
+              sort: Boards.find({ archived: false }).count(),
+              type: 'board',
+              title: Boards.findOne(this.currentData()._id).title,
+            },
             (err, res) => {
               if (err) {
                 this.setError(err.error);

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

@@ -7,10 +7,23 @@ $spaceBetweenTiles = 16px
 
   li
     float: left
-    width: 25%
+    width: 20%
     box-sizing: border-box
     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
       .fa-star,
       .fa-star-o
@@ -20,17 +33,20 @@ $spaceBetweenTiles = 16px
     overflow: hidden;
     background-color: #999
     color: #f6f6f6
-    height: 90px
+    min-height: 100px
     font-size: 16px
     line-height: 22px
     border-radius: 3px
     display: block
     font-weight: 700
-    min-height: 18px
     padding: 8px
     margin: ($spaceBetweenTiles/2)
     position: relative
     text-decoration: none
+    word-wrap: break-word
+
+    &.template-container
+      border: 4px solid #fff
 
     &.tile
       background-size: auto
@@ -55,7 +71,7 @@ $spaceBetweenTiles = 16px
 
     .label
       font-weight: normal
-      line-height:90px
+      line-height: 56px
 
     :hover
       background-color:#939393
@@ -183,7 +199,7 @@ $spaceBetweenTiles = 16px
     overflow: scroll
 
     li
-      width: 50% 
+      width: 50%
 
     .board-list-item
       overflow: hidden
@@ -194,6 +210,22 @@ $spaceBetweenTiles = 16px
       top: -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)
     li
       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'}}
                       else
                         | {{_ '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
       unless currentUser.isCommentOnly
         unless currentUser.isWorker
           //li.attachment-item.add-attachment
-          a.js-add-attachment
+          a.js-add-attachment(title="{{_ 'add-attachment' }}")
             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({
   'click .js-large-image-clicked'() {
     Popup.close();

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

@@ -4,8 +4,7 @@ template(name="cardCustomFieldsPopup")
             li.item(class="")
                 a.name.js-select-field(href="#")
                     span.full-name
-                      +viewer
-                        = name
+                      = name
                     if hasCustomField
                       i.fa.fa-check
     hr
@@ -53,6 +52,31 @@ template(name="cardCustomField-number")
         if 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")
     if canModifyCard
         a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
@@ -95,3 +119,24 @@ template(name="cardCustomField-dropdown")
         if value
             +viewer
                 = 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({
   hasCustomField() {
     const card = Cards.findOne(Session.get('currentCard'));
@@ -80,6 +83,56 @@ CardCustomField.register('cardCustomField');
   }
 }.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
 (class extends CardCustomField {
   onCreated() {
@@ -184,3 +237,90 @@ CardCustomField.register('cardCustomField');
     ];
   }
 }.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}}")
         | {{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({
   canModifyCard() {
@@ -279,7 +187,7 @@ class CardStartDate extends CardDate {
     // if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
     if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
       classes += 'long-overdue';
-    else if (theDate.isBefore(now, 'minute')) classes += 'almost-due';
+    else if (theDate.isAfter(now)) classes += '';
     else classes += 'current';
     return classes;
   }
@@ -363,6 +271,33 @@ class CardEndDate extends CardDate {
 }
 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 {
   showDate() {
     return this.date.get().format('l');
@@ -386,3 +321,63 @@ CardEndDate.register('cardEndDate');
     return this.date.get().format('l');
   }
 }.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
   border-radius: 4px
   padding: 1px 3px
-  
+
   background-color: #dbdbdb
   &:hover, &.is-active
     background-color: #b3b3b3
-  
+
   &.current, &.almost-due, &.due, &.long-overdue
     color: #fff
 
@@ -14,17 +14,17 @@
     background-color: #5ba639
     &:hover, &.is-active
       background-color: darken(#5ba639, 10)
-  
+
   &.almost-due
     background-color: #edc909
     &:hover, &.is-active
       background-color: darken(#edc909, 10)
-  
+
   &.due
     background-color: #fa3f00
     &:hover, &.is-active
       background-color: darken(#fa3f00, 10)
-  
+
   &.long-overdue
     background-color: #fd5d47
     &:hover, &.is-active
@@ -57,3 +57,7 @@
       -webkit-font-smoothing: antialiased
       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")
-  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}}')
       +inlinedForm(classNames="js-card-details-title")
         +editCardTitleForm
       else
         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
-            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(
               class="fa-link"
               title="{{_ 'copy-card-link-to-clipboard'}}"
-              value="{{ absoluteUrl }}"
             )
         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
-            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
         h2.card-details-title.js-card-title(
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
@@ -32,7 +35,7 @@ template(name="cardDetails")
           // else
             {{_ 'top-level-card'}}
         if isLinkedCard
-          h3.linked-card-location
+          a.linked-card-location.js-go-to-linked-card
             +viewer
               | {{getBoardTitle}} > {{getTitle}}
 
@@ -42,242 +45,501 @@ template(name="cardDetails")
       else
         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
               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
 
-      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
               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
 
-      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
-              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
 
-      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
               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
+                | {{_ '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
-                  if getAssignedBy
+                  if getDescription
                     +viewer
-                      = getAssignedBy
+                      = getDescription
                   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
           hr
-          h3
-            i.fa.fa-align-left
-            card-details-item-title {{_ 'description'}}
+          h3.card-details-item-title {{_ 'description'}}
         if currentBoard.allowsDescriptionText
-          +inlinedCardDescription(classNames="card-description js-card-description")
-            +editor(autofocus=true)
-              | {{getUnsavedValue 'cardDescription' _id getDescription}}
-            .edit-controls.clearfix
-              button.primary(type="submit") {{_ 'save'}}
-              a.fa.fa-times-thin.js-close-inlined-form
-          else
-            if currentBoard.allowsDescriptionText
-              a.js-open-inlined-form
-                if getDescription
-                  +viewer
-                    = getDescription
-                else
-                  | {{_ 'edit'}}
-              if (hasUnsavedValue 'cardDescription' _id)
-                p.quiet
-                  | {{_ 'unsaved-description'}}
-                  a.js-open-inlined-form {{_ 'view-it'}}
-                  = ' - '
-                  a.js-close-inlined-form {{_ 'discard'}}
-    else if getDescription
-      if currentBoard.allowsDescriptionTitle
-        hr
-        h3.card-details-item-title {{_ 'description'}}
-      if currentBoard.allowsDescriptionText
-        +viewer
-          = getDescription
-
-    .card-checklist-attachmentGalerys
-      .card-checklist-attachmentGalery.card-checklists
-        if currentBoard.allowsChecklists
-          hr
-          +checklists(cardId = _id)
-        if currentBoard.allowsSubtasks
+          +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
-          +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
-          .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")
   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'}}
     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")
   ul.pop-over-list
     li
@@ -308,17 +576,26 @@ template(name="cardDetailsActionsPopup")
         else
           i.fa.fa-eye-slash
           |  {{_ 'watch'}}
+  hr
   if canModifyCard
     unless currentUser.isWorker
-      hr
       ul.pop-over-list
         //li: a.js-members {{_ 'card-edit-members'}}
         //li: a.js-labels {{_ 'card-edit-labels'}}
         //li: a.js-attachments {{_ 'card-edit-attachments'}}
         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-start-date {{_ 'editCardStartDatePopup-title'}}
         //li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
@@ -331,48 +608,63 @@ template(name="cardDetailsActionsPopup")
           a.js-set-card-color
             i.fa.fa-paint-brush
             | {{_ '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
-        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
+      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
       ul.pop-over-list
         li
-          a.js-move-card
+          a.js-archive
             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")
   +boardsAndLists
@@ -390,13 +682,14 @@ template(name="copyChecklistToManyCardsPopup")
   +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'}}:
   select.js-select-swimlanes
@@ -437,31 +730,14 @@ template(name="cardAssigneesPopup")
               i.fa.fa-check
   if currentUser.isWorker
     ul.pop-over-list.js-card-assignee-list
-        li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
-          a.name.js-select-assignee(href="#")
-            +userAvatar(userId=currentUser._id)
-            span.full-name
-              = currentUser.profile.fullname
-              | (<span class="username">{{ currentUser.username }}</span>)
-            if currentUser.isCardAssignee
-              i.fa.fa-check
-
-template(name="userAvatarAssignee")
-  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")
   .board-assignee-menu
@@ -480,18 +756,14 @@ template(name="cardAssigneePopup")
           with currentUser
             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")
   p.quiet
     span.clearfix
       span {{_ 'link-card'}}
       = ' '
       i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
-      input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}" 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
     br
     h2 {{_ 'change-card-parent'}}
@@ -521,7 +793,8 @@ template(name="cardMorePopup")
     br
     | {{_ 'added'}}
     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")
   form.edit-label
@@ -536,5 +809,83 @@ template(name="setCardColorPopup")
 template(name="cardDeletePopup")
   p {{_ "card-delete-pop"}}
   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'}}
+
+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({
   mixins() {
-    return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
+    return [Mixins.InfiniteScrolling];
   },
 
   calculateNextPeak() {
@@ -47,6 +54,10 @@ BlazeComponent.extendComponent({
     return Meteor.user().hasHiddenSystemMessages();
   },
 
+  cardMaximized() {
+    return Meteor.user().hasCardMaximized();
+  },
+
   canModifyCard() {
     return (
       Meteor.user() &&
@@ -57,12 +68,19 @@ BlazeComponent.extendComponent({
   },
 
   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.
     if (bodyBoardComponent === null) return;
     const $cardView = this.$(this.firstNode());
     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 cardContainerWidth = $cardContainer.width();
 
@@ -107,7 +125,7 @@ BlazeComponent.extendComponent({
     if (card) {
       const board = Boards.findOne(card.boardId);
       if (board) {
-        result = FlowRouter.url('card', {
+        result = FlowRouter.path('card', {
           boardId: card.boardId,
           slug: board.slug,
           cardId: card._id,
@@ -117,6 +135,24 @@ BlazeComponent.extendComponent({
     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() {
     if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
       // Send Webhook but not create Activities records ---
@@ -138,15 +174,13 @@ BlazeComponent.extendComponent({
       }).fetch();
 
       if (integrations.length > 0) {
-        integrations.forEach(integration => {
+        integrations.forEach((integration) => {
           Meteor.call(
             'outgoingWebhooks',
             integration,
             'CardSelected',
             params,
-            () => {
-              return;
-            },
+            () => {},
           );
         });
       }
@@ -155,13 +189,6 @@ BlazeComponent.extendComponent({
 
     if (!Utils.isMiniScreen()) {
       Meteor.setTimeout(() => {
-        $('.card-details').mCustomScrollbar({
-          theme: 'minimal-dark',
-          setWidth: false,
-          setLeft: 0,
-          scrollbarPosition: 'outside',
-          mouseWheel: true,
-        });
         this.scrollParentContainer();
       }, 500);
     }
@@ -200,9 +227,6 @@ BlazeComponent.extendComponent({
       },
     });
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.card-checklist-items .js-checklist');
-
     const $subtasksDom = this.$('.card-subtasks-items');
 
     $subtasksDom.sortable({
@@ -238,26 +262,24 @@ BlazeComponent.extendComponent({
       },
     });
 
-    // ugly touch event hotfix
-    enableClickOnTouch('.card-subtasks-items .js-subtasks');
-
     function userIsMember() {
       return Meteor.user() && Meteor.user().isBoardMember();
     }
 
     // Disable sorting if the current user is not a board member
     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);
         },
         '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();
           if (document.execCommand('copy')) {
             StringToCopyElement.blur();
@@ -316,9 +340,7 @@ BlazeComponent.extendComponent({
         },
         'submit .js-card-details-title'(event) {
           event.preventDefault();
-          const title = this.currentComponent()
-            .getValue()
-            .trim();
+          const title = this.currentComponent().getValue().trim();
           if (title) {
             this.data().setTitle(title);
           } else {
@@ -327,9 +349,7 @@ BlazeComponent.extendComponent({
         },
         'submit .js-card-details-assigner'(event) {
           event.preventDefault();
-          const assigner = this.currentComponent()
-            .getValue()
-            .trim();
+          const assigner = this.currentComponent().getValue().trim();
           if (assigner) {
             this.data().setAssignedBy(assigner);
           } else {
@@ -338,15 +358,26 @@ BlazeComponent.extendComponent({
         },
         'submit .js-card-details-requester'(event) {
           event.preventDefault();
-          const requester = this.currentComponent()
-            .getValue()
-            .trim();
+          const requester = this.currentComponent().getValue().trim();
           if (requester) {
             this.data().setRequestedBy(requester);
           } else {
             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-add-members': Popup.open('cardMembers'),
         'click .js-assignee': Popup.open('cardAssignee'),
@@ -356,6 +387,8 @@ BlazeComponent.extendComponent({
         'click .js-start-date': Popup.open('editCardStartDate'),
         'click .js-due-date': Popup.open('editCardDueDate'),
         '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'() {
           const parentComponent = this.parentComponent().parentComponent();
           //on mobile view parent is Board, not BoardBody.
@@ -379,125 +412,144 @@ BlazeComponent.extendComponent({
         'click #toggleButton'() {
           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
@@ -546,6 +598,10 @@ Template.cardDetailsActionsPopup.helpers({
     return this.findWatcher(Meteor.userId());
   },
 
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   canModifyCard() {
     return (
       Meteor.user() &&
@@ -556,10 +612,13 @@ Template.cardDetailsActionsPopup.helpers({
 });
 
 Template.cardDetailsActionsPopup.events({
+  'click .js-export-card': Popup.open('exportCard'),
   'click .js-members': Popup.open('cardMembers'),
   'click .js-assignees': Popup.open('cardAssignees'),
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
+  'click .js-start-voting': Popup.open('cardStartVoting'),
+  'click .js-start-planning-poker': Popup.open('cardStartPlanningPoker'),
   'click .js-custom-fields': Popup.open('cardCustomFields'),
   'click .js-received-date': Popup.open('editCardReceivedDate'),
   'click .js-start-date': Popup.open('editCardStartDate'),
@@ -575,7 +634,7 @@ Template.cardDetailsActionsPopup.events({
     const minOrder = _.min(
       this.list()
         .cards(this.swimlaneId)
-        .map(c => c.sort),
+        .map((c) => c.sort),
     );
     this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
   },
@@ -584,7 +643,7 @@ Template.cardDetailsActionsPopup.events({
     const maxOrder = _.max(
       this.list()
         .cards(this.swimlaneId)
-        .map(c => c.sort),
+        .map((c) => c.sort),
     );
     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'));
 });
 
@@ -617,7 +676,7 @@ Template.editCardTitleForm.events({
   },
 });
 
-Template.editCardRequesterForm.onRendered(function() {
+Template.editCardRequesterForm.onRendered(function () {
   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'));
 });
 
@@ -649,7 +708,11 @@ Template.moveCardPopup.events({
     // instead from a “component” state.
     const card = Cards.findOne(Session.get('currentCard'));
     const bSelect = $('.js-select-boards')[0];
-    const boardId = bSelect.options[bSelect.selectedIndex].value;
+    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 listId = lSelect.options[lSelect.selectedIndex].value;
     const slSelect = $('.js-select-swimlanes')[0];
@@ -665,17 +728,16 @@ BlazeComponent.extendComponent({
   },
 
   boards() {
-    const boards = Boards.find(
+    return Boards.find(
       {
         archived: false,
         'members.userId': Meteor.userId(),
         _id: { $ne: Meteor.user().getTemplatesBoardId() },
       },
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
     );
-    return boards;
   },
 
   swimlanes() {
@@ -704,7 +766,7 @@ Template.copyCardPopup.events({
   'click .js-done'() {
     const card = Cards.findOne(Session.get('currentCard'));
     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 swimlaneId = slSelect.options[slSelect.selectedIndex].value;
     const bSelect = $('.js-select-boards')[0];
@@ -712,9 +774,7 @@ Template.copyCardPopup.events({
     const textarea = $('#copy-card-title');
     const title = textarea.val().trim();
     // 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) {
       card.title = title;
@@ -745,9 +805,7 @@ Template.copyChecklistToManyCardsPopup.events({
     const textarea = $('#copy-card-title');
     const titleEntry = textarea.val().trim();
     // 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) {
       const titleList = JSON.parse(titleEntry);
@@ -764,13 +822,13 @@ Template.copyChecklistToManyCardsPopup.events({
         Filter.addException(_id);
 
         // copy checklists
-        Checklists.find({ cardId: oldId }).forEach(ch => {
+        Checklists.find({ cardId: oldId }).forEach((ch) => {
           ch.copy(_id);
         });
 
         // copy subtasks
-        cursor = Cards.find({ parentId: oldId });
-        cursor.forEach(function() {
+        const cursor = Cards.find({ parentId: oldId });
+        cursor.forEach(function () {
           'use strict';
           const subtask = arguments[0];
           subtask.parentId = _id;
@@ -779,7 +837,7 @@ Template.copyChecklistToManyCardsPopup.events({
         });
 
         // copy card comments
-        CardComments.find({ cardId: oldId }).forEach(cmt => {
+        CardComments.find({ cardId: oldId }).forEach((cmt) => {
           cmt.copy(_id);
         });
       }
@@ -795,7 +853,7 @@ BlazeComponent.extendComponent({
   },
 
   colors() {
-    return cardColors.map(color => ({ color, name: '' }));
+    return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
   },
 
   isSelected(color) {
@@ -839,7 +897,7 @@ BlazeComponent.extendComponent({
   },
 
   boards() {
-    const boards = Boards.find(
+    return Boards.find(
       {
         archived: false,
         'members.userId': Meteor.userId(),
@@ -848,10 +906,9 @@ BlazeComponent.extendComponent({
         },
       },
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
     );
-    return boards;
   },
 
   cards() {
@@ -919,9 +976,25 @@ BlazeComponent.extendComponent({
             }
           }
         },
-        'click .js-delete': Popup.afterConfirm('cardDelete', function() {
+        'click .js-delete': Popup.afterConfirm('cardDelete', function () {
           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);
         }),
         'change .js-field-parent-board'(event) {
@@ -945,6 +1018,528 @@ BlazeComponent.extendComponent({
   },
 }).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
 EscapeActions.register(
   'detailsPane',

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

@@ -10,6 +10,9 @@ avatar-radius = 50%
   left: -2000px
   top: 0px
 
+#clipboard
+  white-space: normal
+
 .assignee
   border-radius: 3px
   display: block
@@ -37,6 +40,8 @@ avatar-radius = 50%
       position: absolute
 
     &.avatar-image
+      object-fit: cover;
+      object-position: center;
       height: 100%
       width: @height
 
@@ -84,7 +89,7 @@ avatar-radius = 50%
 .card-details
   padding: 0
   flex-shrink: 0
-  flex-basis: 510px
+  flex-basis: 600px
   will-change: flex-basis
   overflow-y: scroll
   overflow-x: hidden
@@ -94,25 +99,24 @@ avatar-radius = 50%
   animation: flexGrowIn 0.1s
   box-shadow: 0 0 7px 0 darken(white, 30%)
   transition: flex-basis 0.1s
+  box-sizing: border-box
 
   .mCustomScrollBox
     padding-left: 0
 
-  .ps-scrollbar-y-rail
-    pointer-event: all
-    position: absolute;
-
   .card-details-canvas
-    width: 470px
-    padding-left: 20px;
+    width: auto
+    padding: 0 20px
 
   .card-details-header
     margin: 0 -20px 5px
-    padding 7px 16px
+    padding: 7px 20px
     background: darken(white, 7%)
     border-bottom: 1px solid darken(white, 14%)
 
     .close-card-details,
+    .maximize-card-details,
+    .minimize-card-details,
     .card-details-menu,
     .card-copy-button,
     .card-copy-mobile-button,
@@ -120,9 +124,11 @@ avatar-radius = 50%
     .card-details-menu-mobile-web
       float: right
 
-    .close-card-details
+    .close-card-details,
+    .maximize-card-details,
+    .minimize-card-details
       font-size: 24px
-      padding: 5px
+      padding: 5px 10px 5px 10px
       margin-right: -8px
 
     .close-card-details-mobile-web
@@ -196,23 +202,33 @@ avatar-radius = 50%
       margin-right: 0.5em
       &:last-child
         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-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-start,
       &.card-details-item-due,
-      &.card-details-item-end,
-      &.card-details-item-customfield,
-      &.card-details-item-name
+      &.card-details-item-end
         display: block
         word-wrap: break-word
-        max-width: 48%
+        max-width: 28%
         flex-grow: 1
 
   .card-details-item-title
     font-size: 16px
-    color: #000
+    font-weight: bold
+    color: #4d4d4d
 
   .card-label
     padding-top: 5px
@@ -221,6 +237,43 @@ avatar-radius = 50%
   .activities
     padding-top: 10px
 
+.card-details-maximized
+  padding: 0
+  flex-shrink: 0
+  flex-basis: calc(100% - 20px)
+  will-change: flex-basis
+  overflow-y: scroll
+  overflow-x: scroll
+  background: darken(white, 3%)
+  border-radius: bottom 3px
+  z-index: 1000 !important
+  animation: flexGrowIn 0.1s
+  box-shadow: 0 0 7px 0 darken(white, 30%)
+  transition: flex-basis 0.1s
+  box-sizing: border-box
+  position: absolute
+  top: 0
+  left: 0
+  height: calc(100% - 20px)
+  width: calc(100% - 20px)
+  float: left
+
+  .card-details-left
+    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
   float: left
   margin: 0 0 8px
@@ -241,14 +294,20 @@ input[type="submit"].attachment-add-link-submit
 
     .card-details-canvas
       width: 100%
-      padding-left: 0px;
+      padding-left: 0px
 
     .card-details-header
       .close-card-details
         margin-right: 0px
 
       .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...)
   background: background !important
@@ -330,3 +389,146 @@ card-details-color(background, color...)
 
 .card-details-indigo
   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")
-  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
     .board-overlay#card-details-overlay
     +checklistDeleteDialog(checklist = checklistToDelete)
@@ -15,9 +25,8 @@ template(name="checklists")
     +inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
       +addChecklistItemForm
     else
-      a.js-open-inlined-form
+      a.js-open-inlined-form(title="{{_ 'add-checklist'}}")
         i.fa.fa-plus
-        | {{_ 'add-checklist'}}...
 
 template(name="checklistDetail")
   .js-checklist.checklist
@@ -31,6 +40,8 @@ template(name="checklistDetail")
 
         if canModifyCard
           h2.title.js-open-inlined-form.is-editable
+            if isMiniScreenOrShowDesktopDragHandles
+              span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
             +viewer
               = checklist.title
         else
@@ -81,14 +92,16 @@ template(name="checklistItems")
       +inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
         +addChecklistItemForm
       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
-          | {{_ 'add-checklist-item'}}...
 
 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
-      .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}}")
         +viewer
           = item.title

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

@@ -1,4 +1,4 @@
-const { calculateIndexData, enableClickOnTouch } = Utils;
+const { calculateIndexData, capitalize } = Utils;
 
 function initSorting(items) {
   items.sortable({
@@ -6,7 +6,7 @@ function initSorting(items) {
     helper: 'clone',
     items: '.js-checklist-item:not(.placeholder)',
     connectWith: '.js-checklist-items',
-    appendTo: '.board-canvas',
+    appendTo: 'parent',
     distance: 7,
     placeholder: 'checklist-item placeholder',
     scroll: false,
@@ -36,9 +36,6 @@ function initSorting(items) {
       checklistItem.move(checklistId, sortIndex.base);
     },
   });
-
-  // ugly touch event hotfix
-  enableClickOnTouch('.js-checklist-item:not(.placeholder)');
 }
 
 BlazeComponent.extendComponent({
@@ -54,14 +51,16 @@ BlazeComponent.extendComponent({
       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(() => {
       const $itemsDom = $(self.itemsDom);
-      if ($itemsDom.data('sortable')) {
+      if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
         $(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,
         checklistId: checklist._id,
         cardId: checklist.cardId,
-        sort: checklist.itemCount(),
+        sort: Utils.calculateIndexData(checklist.lastItem()).base,
       });
     }
     // 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() {
     const events = {
       'click .toggle-delete-checklist-dialog'(event) {
@@ -185,6 +194,9 @@ BlazeComponent.extendComponent({
         }
         this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
       },
+      'click #toggleHideCheckedItemsButton'() {
+        Meteor.call('toggleHideCheckedItems');
+      },
     };
 
     return [
@@ -196,12 +208,29 @@ BlazeComponent.extendComponent({
         'submit .js-edit-checklist-item': this.editChecklistItem,
         'click .js-delete-checklist-item': this.deleteItem,
         'click .confirm-checklist-delete': this.deleteChecklist,
+        'focus .js-add-checklist-item': this.focusChecklistItem,
         keydown: this.pressKey,
       },
     ];
   },
 }).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(() => {
   const $cardDetails = this.$('.card-details');
   this.scrollState = {
@@ -237,6 +266,11 @@ Template.checklistItemDetail.helpers({
       !Meteor.user().isWorker()
     );
   },
+  hideCheckedItems() {
+    const user = Meteor.user();
+    if (user) return user.hasHideCheckedItems();
+    return false;
+  },
 });
 
 BlazeComponent.extendComponent({
@@ -250,7 +284,7 @@ BlazeComponent.extendComponent({
   events() {
     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
     color: inherit
 
+.checklists-title
+  display: flex
+  justify-content: space-between
+
 .checklist-title
   .checkbox
     float: left
@@ -38,6 +42,11 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   .js-delete-checklist
     @extends .delete-text
 
+  span.fa.checklist-handle
+    padding-right: 20px
+    padding-top: 3px
+    float: left
+
 
 .js-confirm-checklist-delete
   background-color: darken(white, 3%)
@@ -99,6 +108,17 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   margin-top: 3px
   display: flex
   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
     background: darken(white, 20%)
@@ -113,6 +133,9 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
   &:hover
     background-color: darken(white, 8%)
 
+  .check-box-container
+    padding-right: 10px;
+
   .check-box
     margin: 0.1em 0 0 0;
     &.is-checked
@@ -121,10 +144,10 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
 
   .item-title
     flex: 1
-    padding-left: 10px;
     &.is-checked
       color: #8c8c8c
       font-style: italic
+      text-decoration: line-through
     & .viewer
       p
         margin-bottom: 2px
@@ -132,6 +155,10 @@ textarea.js-add-checklist-item, textarea.js-edit-checklist-item
         word-wrap: break-word
         max-width: 420px
 
+  span.fa.checklistitem-handle
+    padding-top: 2px
+    padding-right: 10px;
+
 .js-delete-checklist-item
   margin: 0 0 0.5em 1.33em
   @extends .delete-text

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

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

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

@@ -4,8 +4,8 @@ template(name="minicard")
     class="{{#if isLinkedBoard}}linked-board{{/if}}"
     class="minicard-{{colorClass}}")
     if isMiniScreen
-      //.handle
-      //  .fa.fa-arrows
+      .handle
+        .fa.fa-arrows
     unless isMiniScreen
       if showDesktopDragHandles
         .handle
@@ -74,20 +74,35 @@ template(name="minicard")
                   +viewer
                     = definition.name
               .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
       .minicard-assignees.js-minicard-assignees
         each getAssignees
           +userAvatar(userId=this)
-        hr
 
     if getMembers
       .minicard-members.js-minicard-members
         each getMembers
           +userAvatar(userId=this)
 
+    if showCreator
+      .minicard-creator
+        +userAvatar(userId=this.userId noRemove=true)
+
     .badges
       unless currentUser.isNoComments
         if comments.count
@@ -100,6 +115,17 @@ template(name="minicard")
       if getDescription
         .badge.badge-state-image-only(title=getDescription)
           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
         .badge
           span.badge-icon.fa.fa-paperclip
@@ -108,3 +134,12 @@ template(name="minicard")
         .badge(class="{{#if checklistFinished}}is-finished{{/if}}")
           span.badge-icon.fa.fa-check-square-o
           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({
 //   'click .member': Popup.open('cardMember')
 // });
@@ -9,6 +7,48 @@ BlazeComponent.extendComponent({
     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() {
     return [
       {
@@ -20,10 +60,10 @@ BlazeComponent.extendComponent({
       },
       {
         '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 {
-            cookies.set('hiddenMinicardLabelText', 'true'); //true
+            window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
           }
         },
       },
@@ -36,7 +76,7 @@ Template.minicard.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (cookies.has('showDesktopDragHandles')) {
+    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
       return true;
     } else {
       return false;
@@ -46,7 +86,7 @@ Template.minicard.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).hiddenMinicardLabelText;
-    } else if (cookies.has('hiddenMinicardLabelText')) {
+    } else if (window.localStorage.getItem('hiddenMinicardLabelText')) {
       return true;
     } else {
       return false;

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

@@ -87,7 +87,9 @@
       width: 11px
       height: @width
       border-radius: 2px
-      margin-left: 3px
+      margin-right: 3px
+      margin-bottom: 3px
+
   .minicard-custom-fields
     display:block;
   .minicard-custom-field
@@ -161,15 +163,18 @@
         line-height: 12px
 
   .minicard-members,
-  .minicard-assignees
+  .minicard-assignees,
+  .minicard-creator
     float: right
-    margin: 2px -8px 12px 0
+    margin-left: 5px
+    margin-bottom: 4px
 
     .member
       float: right
       border-radius: 50%
       height: 28px
       width: @height
+      margin-bottom: 4px
 
     .assignee
       float: right
@@ -178,7 +183,13 @@
       width: @height
 
     + .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-assignees:empty
@@ -299,3 +310,8 @@ minicard-color(background, color...)
 
 .minicard-indigo
   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")
-  h3
+  h3.card-details-item-title
     i.fa.fa-sitemap
     | {{_ '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
     each subtask in currentCard.subtasks
@@ -15,9 +15,8 @@ template(name="subtasks")
     +inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
       +addSubtaskItemForm
     else
-      a.js-open-inlined-form
+      a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
         i.fa.fa-plus
-        | {{_ 'add-subtask'}}...
 
 template(name="subtaskDetail")
   .js-subtasks.subtask
@@ -28,7 +27,8 @@ template(name="subtaskDetail")
         span
         a.js-view-subtask(title="{{ subtask.title }}") {{_ "view-it"}}
         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
           h2.title.js-open-inlined-form.is-editable
@@ -68,7 +68,8 @@ template(name="editSubtaskItemForm")
     a.fa.fa-times-thin.js-close-inlined-form
     span(title=createdAt) {{ moment createdAt }}
     if canModifyCard
-      a.js-delete-subtask-item {{_ "delete"}}...
+      if currentUser.isBoardAdmin
+        a.js-delete-subtask-item {{_ "delete"}}...
 
 template(name="subtasksItems")
   .subtasks-items.js-subtasks-items

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

@@ -22,11 +22,20 @@ BlazeComponent.extendComponent({
     const listId = targetBoard.getDefaultSubtasksListId();
 
     //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.
-    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.
-    const swimlaneId = targetSwimlane === undefined ? targetBoard.getDefaultSwimline()._id : targetSwimlane._id;
+    const swimlaneId =
+      targetSwimlane === undefined
+        ? targetBoard.getDefaultSwimline()._id
+        : targetSwimlane._id;
 
     if (title) {
       const _id = Cards.insert({

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

@@ -86,7 +86,7 @@ select
   margin-bottom: 8px
 
   &.inline
-  	width: 100%
+    width: 100%
 
 option[disabled]
   color: #8c8c8c
@@ -242,11 +242,11 @@ textarea
     margin: 3px 4px
 
 // 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
   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")
   form
     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}}
-    if isSandstorm
-      h1.warning {{_ 'import-sandstorm-backup-warning'}}
-      p.warning {{_ 'import-sandstorm-warning'}}
     input.primary.wide(type="submit" value="{{_ 'import'}}")
 
 template(name="importMapMembers")
   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")
   .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({
   title() {
@@ -30,20 +33,30 @@ BlazeComponent.extendComponent({
     }
   },
 
-  importData(evt) {
+  importData(evt, dataSource) {
     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.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;
     switch (importSource) {
       case 'trello':
-        membersToMap = trelloMembersMapper.getMembersToMap(dataObject);
+        membersToMap = trelloGetMembersToMap(dataObject);
         break;
       case 'wekan':
-        membersToMap = wekanMembersMapper.getMembersToMap(dataObject);
+        membersToMap = wekanGetMembersToMap(dataObject);
+        break;
+      case 'csv':
+        membersToMap = csvGetMembersToMap(dataObject);
         break;
     }
     return membersToMap;
@@ -109,11 +125,23 @@ BlazeComponent.extendComponent({
     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() {
     return [
       {
         submit(evt) {
-          return this.parentComponent().importData(evt);
+          return this.parentComponent().importData(
+            evt,
+            Session.get('importSource'),
+          );
         },
       },
     ];
@@ -122,14 +150,42 @@ BlazeComponent.extendComponent({
 
 BlazeComponent.extendComponent({
   onCreated() {
+    this.usersLoaded = new ReactiveVar(false);
+
     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
   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
   // mapping is done, we add a 'wekan' field to the object representing the
   // 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
   // mapping is done, we add a 'wekan' field to the object representing the
   // 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({
   // Proxy
@@ -74,18 +72,16 @@ BlazeComponent.extendComponent({
         const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
         const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
         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 (
           Utils.boardView() === 'board-view-swimlanes' ||
           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
         // to its new position, which disrupts Blaze reactive updates mechanism
@@ -98,9 +94,12 @@ BlazeComponent.extendComponent({
 
         if (MultiSelection.isActive()) {
           Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
+            const newSwimlaneId = targetSwimlaneId
+              ? targetSwimlaneId
+              : card.swimlaneId || defaultSwimlaneId;
             card.move(
               currentBoard._id,
-              swimlaneId,
+              newSwimlaneId,
               listId,
               sortIndex.base + i * sortIndex.increment,
             );
@@ -108,28 +107,28 @@ BlazeComponent.extendComponent({
         } else {
           const cardDomElement = ui.item.get(0);
           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);
       },
     });
 
-    // ugly touch event hotfix
-    enableClickOnTouch(itemsSelector);
-
     this.autorun(() => {
       let showDesktopDragHandles = false;
       currentUser = Meteor.user();
       if (currentUser) {
         showDesktopDragHandles = (currentUser.profile || {})
           .showDesktopDragHandles;
-      } else if (cookies.has('showDesktopDragHandles')) {
+      } else if (window.localStorage.getItem('showDesktopDragHandles')) {
         showDesktopDragHandles = true;
       } else {
         showDesktopDragHandles = false;
       }
 
-      if (!Utils.isMiniScreen() && showDesktopDragHandles) {
+      if (Utils.isMiniScreen() || showDesktopDragHandles) {
         $cards.sortable({
           handle: '.handle',
         });
@@ -139,27 +138,16 @@ BlazeComponent.extendComponent({
         });
       }
 
-      if ($cards.data('sortable')) {
+      if ($cards.data('uiSortable') || $cards.data('sortable')) {
         $cards.sortable(
           'option',
           'disabled',
-          // Disable drag-dropping when user is not member/is miniscreen
+          // Disable drag-dropping when user is not member
           !userIsMember(),
           // Not disable drag-dropping while in multi-selection mode
           // 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.
@@ -195,7 +183,7 @@ Template.list.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (cookies.has('showDesktopDragHandles')) {
+    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
       return true;
     } else {
       return false;

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

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

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

@@ -1,11 +1,11 @@
 template(name="listBody")
-  .list-body.js-perfect-scrollbar
+  .list-body
     .minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
       if cards.count
         +inlinedForm(autoclose=false position="top")
           +addCardForm(listId=_id position="top")
       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 MultiSelection.isSelected _id}}is-checked{{/if}}")
           if MultiSelection.isActive
@@ -19,19 +19,14 @@ template(name="listBody")
         +inlinedForm(autoclose=false position="bottom")
           +addCardForm(listId=_id position="bottom")
         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
-            | {{_ 'add-card'}}
 
 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")
-    .sk-rect1
-    .sk-rect2
-    .sk-rect3
-    .sk-rect4
-    .sk-rect5
+    +spinnerRaw
 
 template(name="addCardForm")
   .minicard.minicard-composer.js-composer
@@ -105,8 +100,10 @@ template(name="searchElementPopup")
         each boards
           option(value="{{_id}}") {{title}}
   form.js-search-term-form
+    label
+      | {{_ 'template'}}
     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
       if isBoardTemplateSearch
         each results

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

@@ -1,3 +1,5 @@
+import { Spinner } from '/client/lib/spinner';
+
 const subManager = new SubsManager();
 const InfiniteScrollIter = 10;
 
@@ -8,7 +10,7 @@ BlazeComponent.extendComponent({
   },
 
   mixins() {
-    return [Mixins.PerfectScrollbar];
+    return [];
   },
 
   openForm(options) {
@@ -77,7 +79,7 @@ BlazeComponent.extendComponent({
       else if (
         Utils.boardView() === 'board-view-lists' ||
         Utils.boardView() === 'board-view-cal' ||
-        !Utils.boardView
+        !Utils.boardView()
       )
         swimlaneId = board.getDefaultSwimline()._id;
 
@@ -116,8 +118,6 @@ BlazeComponent.extendComponent({
       if (position === 'bottom') {
         this.scrollToBottom();
       }
-
-      formComponent.reset();
     }
   },
 
@@ -168,13 +168,16 @@ BlazeComponent.extendComponent({
 
   cardsWithLimit(swimlaneId) {
     const limit = this.cardlimit.get();
+    const defaultSort = { sort: 1 };
+    const sortBy = Session.get('sortBy') ? Session.get('sortBy') : defaultSort;
     const selector = {
       listId: this.currentData()._id,
       archived: false,
     };
     if (swimlaneId) selector.swimlaneId = swimlaneId;
     return Cards.find(Filter.mongoSelector(selector), {
-      sort: ['sort'],
+      // sort: ['sort'],
+      sort: sortBy,
       limit,
     });
   },
@@ -239,7 +242,7 @@ BlazeComponent.extendComponent({
         .customFields()
         .fetch(),
       function(field) {
-        if (field.automaticallyOnCard)
+        if (field.automaticallyOnCard || field.alwaysOnCard)
           arr.push({ _id: field._id, value: null });
       },
     );
@@ -411,7 +414,7 @@ BlazeComponent.extendComponent({
         type: 'board',
       },
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
     );
     return boards;
@@ -523,7 +526,7 @@ BlazeComponent.extendComponent({
 
 BlazeComponent.extendComponent({
   mixins() {
-    return [Mixins.PerfectScrollbar];
+    return [];
   },
 
   onCreated() {
@@ -549,7 +552,7 @@ BlazeComponent.extendComponent({
       board = Boards.findOne((Meteor.user().profile || {}).templatesBoardId);
     } else {
       // Prefetch first non-current board id
-      board = Boards.findOne({
+      board = Boards.find({
         archived: false,
         'members.userId': Meteor.userId(),
         _id: {
@@ -597,7 +600,7 @@ BlazeComponent.extendComponent({
         type: 'board',
       },
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
     );
     return boards;
@@ -658,10 +661,7 @@ BlazeComponent.extendComponent({
               _id = element.copy(this.boardId, this.swimlaneId, this.listId);
               // 1.B Linked card
             } else {
-              delete element._id;
-              element.type = 'cardType-linkedCard';
-              element.linkedId = element.linkedId || element._id;
-              _id = Cards.insert(element);
+              _id = element.link(this.boardId, this.swimlaneId, this.listId);
             }
             Filter.addException(_id);
             // List insertion
@@ -675,15 +675,21 @@ BlazeComponent.extendComponent({
             element.sort = Boards.findOne(this.boardId)
               .swimlanes()
               .count();
-            element.type = 'swimlalne';
+            element.type = 'swimlane';
             _id = element.copy(this.boardId);
           } else if (this.isBoardTemplateSearch) {
-            board = Boards.findOne(element.linkedId);
-            board.sort = Boards.find({ archived: false }).count();
-            board.type = 'board';
-            board.title = element.title;
-            delete board.slug;
-            _id = board.copy();
+            Meteor.call(
+              'copyBoard',
+              element.linkedId,
+              {
+                sort: Boards.find({ archived: false }).count(),
+                type: 'board',
+                title: element.title,
+              },
+              (err, data) => {
+                _id = data;
+              },
+            );
           }
           Popup.close();
         },
@@ -692,7 +698,7 @@ BlazeComponent.extendComponent({
   },
 }).register('searchElementPopup');
 
-BlazeComponent.extendComponent({
+(class extends Spinner {
   onCreated() {
     this.cardlimit = this.parentComponent().cardlimit;
 
@@ -720,11 +726,11 @@ BlazeComponent.extendComponent({
         .parentComponent()
         .data()._id;
     }
-  },
+  }
 
   onRendered() {
     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(
       `scroll.spinner_${this.swimlaneId}_${this.listId}`,
@@ -735,47 +741,58 @@ BlazeComponent.extendComponent({
     );
 
     this.updateList();
-  },
+  }
 
   onDestroyed() {
     $(this.container).off(`scroll.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() {
     // Use fallback when requestIdleCallback is not available on iOS and Safari
     // https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/
-    checkIdleTime =
-      window.requestIdleCallback ||
-      function(handler) {
-        const startTime = Date.now();
-        return setTimeout(function() {
-          handler({
-            didTimeout: false,
-            timeRemaining() {
-              return Math.max(0, 50.0 - (Date.now() - startTime));
-            },
-          });
-        }, 1);
-      };
 
     if (this.spinnerInView()) {
       this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
-      checkIdleTime(() => this.updateList());
+      this.checkIdleTime(() => this.updateList());
     }
-  },
+  }
 
   spinnerInView() {
+    // spinner deleted
+    if (!this.spinner.offsetTop) {
+      return false;
+    }
+
     const parentViewHeight = this.container.clientHeight;
     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")
   .list-header.js-list-header(
     class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}"
-    class="{{#if colorClass}}list-header-{{colorClass}}{{/if}}")
+    class=colorClass)
     +inlinedForm
       +editListTitleForm
     else
@@ -15,7 +15,7 @@ template(name="listHeader")
           = title
         if wipLimit.enabled
          |&nbsp;(
-         span(class="{{#if reachedWipLimit}}highlight{{/if}}") {{cards.count}}
+         span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.count}}
          |/#{wipLimit.value})
 
         if showCardsCountForList cards.count
@@ -28,12 +28,11 @@ template(name="listHeader")
           div.list-header-menu
             unless currentUser.isCommentOnly
               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
           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
         if isWatching
           i.list-header-watch-icon.fa.fa-eye
@@ -42,10 +41,11 @@ template(name="listHeader")
             //if isBoardAdmin
             //  a.fa.js-list-star.list-header-plus-icon(class="fa-star{{#unless starred}}-o{{/unless}}")
             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")
   .list-composer
@@ -116,8 +116,9 @@ template(name="listMorePopup")
       input.inline-input(type="text" readonly value="{{ rootUrl }}")
     | {{_ 'added'}}
     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")
   p {{_ "list-delete-pop"}}
@@ -152,7 +153,7 @@ template(name="setListColorPopup")
   form.edit-label
     .palette-colors: each colors
       // note: we use the swimlane palette to have more than just the border
-      span.card-label.palette-color.js-palette-color(class="swimlane-{{color}}")
+      span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
         if(isSelected color)
           i.fa.fa-check
     button.primary.confirm.js-submit {{_ 'save'}}

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

@@ -1,5 +1,3 @@
-import { Cookies } from 'meteor/ostrio:cookies';
-const cookies = new Cookies();
 let listsColors;
 Meteor.startup(() => {
   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) {
     const limit = this.limitToShowCardsCount();
-    return limit > 0 && count > limit;
+    return limit >= 0 && count >= limit;
   },
 
   events() {
@@ -106,11 +112,15 @@ BlazeComponent.extendComponent({
 }).register('listHeader');
 
 Template.listHeader.helpers({
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   showDesktopDragHandles() {
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
-    } else if (cookies.has('showDesktopDragHandles')) {
+    } else if (window.localStorage.getItem('showDesktopDragHandles')) {
       return true;
     } else {
       return false;
@@ -119,6 +129,10 @@ Template.listHeader.helpers({
 });
 
 Template.listActionPopup.helpers({
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+
   isWipLimitEnabled() {
     return Template.currentData().getWipLimit('enabled');
   },
@@ -223,12 +237,45 @@ BlazeComponent.extendComponent({
 Template.listMorePopup.events({
   'click .js-delete': Popup.afterConfirm('listDelete', function() {
     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);
   }),
 });
 
+Template.listHeader.helpers({
+  isBoardAdmin() {
+    return Meteor.user().isBoardAdmin();
+  },
+});
+
 BlazeComponent.extendComponent({
   onCreated() {
     this.currentList = this.currentData();
@@ -240,7 +287,11 @@ BlazeComponent.extendComponent({
   },
 
   isSelected(color) {
-    return this.currentColor.get() === color;
+    if (this.currentColor.get() === null) {
+      return color === 'white';
+    } else {
+      return this.currentColor.get() === color;
+    }
   },
 
   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']],
           ['table', ['table']],
           //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
-          //['insert', ['link', 'picture']], // modal popup has issue somehow :(
-          ['view', ['fullscreen', 'help']],
+          ['insert', ['link']], //, 'picture']], // modal popup has issue somehow :(
+          ['view', ['fullscreen', 'codeview', 'help']],
         ];
     const cleanPastedHTML = function(input) {
       const badTags = [
@@ -91,6 +91,7 @@ Template.editor.onRendered(() => {
     };
     const editor = '.editor';
     const selectors = [
+      `.js-new-description-form ${editor}`,
       `.js-new-comment-form ${editor}`,
       `.js-edit-comment ${editor}`,
     ].join(','); // only new comment and edit comment
@@ -144,6 +145,7 @@ Template.editor.onRendered(() => {
                 const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
                 const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
                 const insertImage = src => {
+                  // process all image upload types to the description/comment window
                   const img = document.createElement('img');
                   img.src = src;
                   img.setAttribute('width', '100%');
@@ -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
               const thisNote = this;
               const updatePastedText = function(object) {
@@ -233,17 +244,17 @@ Template.editor.onRendered(() => {
             },
           },
           dialogsInBody: true,
-          disableDragAndDrop: true,
+          spellCheck: true,
+          disableGrammar: false,
+          disableDragAndDrop: false,
           toolbar,
           popover: {
             image: [
-              [
-                'image',
-                ['resizeFull', 'resizeHalf', 'resizeQuarter', 'resizeNone'],
-              ],
+              ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
               ['float', ['floatLeft', 'floatRight', 'floatNone']],
               ['remove', ['removeMedia']],
             ],
+            link: [['link', ['linkDialogShow', 'unlink']]],
             table: [
               ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
               ['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
 // would handle markdown and user mentions. We can simply have two
@@ -277,7 +319,10 @@ Blaze.Template.registerHelper(
     const view = this;
     let content = Blaze.toHTML(view.templateContentBlock);
     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 u = Users.findOne(member.userId);
       if (u) {
@@ -321,7 +366,9 @@ Blaze.Template.registerHelper(
       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
   // we stop these event at the viewer component level.
   'click a'(event, templateInstance) {
-    let prevent = true;
+    const prevent = true;
     const userId = event.currentTarget.dataset.userid;
     if (userId) {
       Popup.open('member').call({ userId }, event, templateInstance);

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels