merged with wekan master @ v5.38

This commit is contained in:
Stefan Maaßen 2021-07-20 13:33:42 +02:00
commit cb418f5e23
743 changed files with 117634 additions and 43043 deletions

12
.babelrc Normal file
View file

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

View file

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

View file

@ -3,17 +3,18 @@ version: '3.7'
services: services:
wekandb-dev: wekandb-dev:
image: mongo:4.0.12 image: mongo:4.4
container_name: wekan-dev-db container_name: wekan-dev-db
restart: unless-stopped restart: unless-stopped
command: mongod --smallfiles --oplogSize 128 command: mongod --oplogSize 128
networks: networks:
- wekan-dev-tier - wekan-dev-tier
expose: expose:
- 27017 - 27017
volumes: volumes:
- wekan-dev-db:/data/db - ./volumes/wekan-db:/data/db
- wekan-dev-db-dump:/dump - ./volumes/wekan-db-dump:/dump
- /etc/localtime:/etc/localtime:ro
wekan-dev: wekan-dev:
container_name: wekan-dev-app container_name: wekan-dev-app
@ -35,9 +36,13 @@ services:
depends_on: depends_on:
- wekandb-dev - wekandb-dev
volumes: volumes:
- ..:/app:delegated - ../client:/home/wekan/app/client
command: - ../models:/home/wekan/app/models
sleep infinity - ../config:/home/wekan/app/config
- ../i18n:/home/wekan/app/i18n
- ../server:/home/wekan/app/server
- ../public:/home/wekan/app/public
- /etc/localtime:/etc/localtime:ro
volumes: volumes:
wekan-dev-db: wekan-dev-db:

36
.dockerignore Normal file
View file

@ -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

View file

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

View file

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

198
.future-snap/old-rebuild-wekan.sh Executable file
View file

@ -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

View file

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

View file

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

62
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -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
.github/workflows/test_suite.yml vendored Normal file
View file

@ -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!

4
.gitignore vendored
View file

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

10
.gitpod.Dockerfile vendored Normal file
View file

@ -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
.gitpod.yml Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

@ -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

View file

@ -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'] = {};
})();

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
};

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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;
}
}

View file

@ -1,9 +1,9 @@
dist: eoan dist: focal
sudo: required sudo: required
env: env:
TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0 TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
TRAVIS_NODE_VERSION: 12.15.0 TRAVIS_NODE_VERSION: 12.22.3
TRAVIS_NPM_VERSION: latest TRAVIS_NPM_VERSION: latest
before_install: before_install:

View file

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

File diff suppressed because it is too large Load diff

View file

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

77
Dockerfile.arm64v8 Normal file
View file

@ -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"]

View file

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

View file

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

View file

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

291
api.py Executable file
View file

@ -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
client/00-startup.js Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
const subManager = new SubsManager(); const subManager = new SubsManager();
const { calculateIndex, enableClickOnTouch } = Utils;
Template.boardListHeaderBar.events({ Template.boardListHeaderBar.events({
'click .js-open-archived-board'() { 'click .js-open-archived-board'() {
@ -7,6 +8,9 @@ Template.boardListHeaderBar.events({
}); });
Template.boardListHeaderBar.helpers({ Template.boardListHeaderBar.helpers({
title() {
return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public';
},
templatesBoardId() { templatesBoardId() {
return Meteor.user() && Meteor.user().getTemplatesBoardId(); return Meteor.user() && Meteor.user().getTemplatesBoardId();
}, },
@ -18,22 +22,91 @@ Template.boardListHeaderBar.helpers({
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { onCreated() {
Meteor.subscribe('setting'); Meteor.subscribe('setting');
let currUser = Meteor.user();
let userLanguage;
if(currUser && currUser.profile){
userLanguage = currUser.profile.language
}
if (userLanguage) {
TAPi18n.setLanguage(userLanguage);
T9n.setLanguage(userLanguage);
}
},
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');
},
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() { boards() {
return Boards.find( const query = {
{ archived: false,
archived: false, //type: { $in: ['board','template-container'] },
'members.userId': Meteor.userId(), type: 'board',
type: 'board', };
}, if (FlowRouter.getRouteName() === 'home')
{ sort: ['title'] }, query['members.userId'] = Meteor.userId();
); else query.permission = 'public';
return Boards.find(query, {
sort: { sort: 1 /* boards default sorting */ },
});
}, },
isStarred() { isStarred() {
const user = Meteor.user(); const user = Meteor.user();
return user && user.hasStarred(this.currentData()._id); return user && user.hasStarred(this.currentData()._id);
}, },
isAdministrable() {
const user = Meteor.user();
return user && user.isBoardAdmin(this.currentData()._id);
},
hasOvertimeCards() { hasOvertimeCards() {
subManager.subscribe('board', this.currentData()._id, false); subManager.subscribe('board', this.currentData()._id, false);
@ -61,9 +134,13 @@ BlazeComponent.extendComponent({
}, },
'click .js-clone-board'(evt) { 'click .js-clone-board'(evt) {
Meteor.call( Meteor.call(
'cloneBoard', 'copyBoard',
this.currentData()._id, this.currentData()._id,
Session.get('fromBoard'), {
sort: Boards.find({ archived: false }).count(),
type: 'board',
title: Boards.findOne(this.currentData()._id).title,
},
(err, res) => { (err, res) => {
if (err) { if (err) {
this.setError(err.error); this.setError(err.error);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,96 +1,4 @@
// Edit received, start, due & end dates import { DatePicker } from '/client/lib/datepicker';
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();
},
},
];
},
});
Template.dateBadge.helpers({ Template.dateBadge.helpers({
canModifyCard() { canModifyCard() {
@ -279,7 +187,7 @@ class CardStartDate extends CardDate {
// if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged // if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt))) if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
classes += 'long-overdue'; classes += 'long-overdue';
else if (theDate.isBefore(now, 'minute')) classes += 'almost-due'; else if (theDate.isAfter(now)) classes += '';
else classes += 'current'; else classes += 'current';
return classes; return classes;
} }
@ -363,6 +271,33 @@ class CardEndDate extends CardDate {
} }
CardEndDate.register('cardEndDate'); CardEndDate.register('cardEndDate');
class CardCustomFieldDate extends CardDate {
template() {
return 'dateCustomField';
}
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().value));
});
}
classes() {
return 'customfield-date';
}
showTitle() {
return '';
}
events() {
return [];
}
}
CardCustomFieldDate.register('cardCustomFieldDate');
(class extends CardReceivedDate { (class extends CardReceivedDate {
showDate() { showDate() {
return this.date.get().format('l'); return this.date.get().format('l');
@ -386,3 +321,63 @@ CardEndDate.register('cardEndDate');
return this.date.get().format('l'); return this.date.get().format('l');
} }
}.register('minicardEndDate')); }.register('minicardEndDate'));
(class extends CardCustomFieldDate {
showDate() {
return this.date.get().format('l');
}
}.register('minicardCustomFieldDate'));
class VoteEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getVoteEnd()));
});
}
classes() {
const classes = 'end-date' + ' ';
return classes;
}
showDate() {
return this.date.get().format('l LT');
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
}
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editVoteEndDate'),
});
}
}
VoteEndDate.register('voteEndDate');
class PokerEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getPokerEnd()));
});
}
classes() {
const classes = 'end-date' + ' ';
return classes;
}
showDate() {
return this.date.get().format('l LT');
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
}
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editPokerEndDate'),
});
}
}
PokerEndDate.register('pokerEndDate');

View file

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

View file

@ -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}}

View file

@ -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');

View file

@ -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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -44,9 +44,20 @@
align-items: center align-items: center
justify-content: center justify-content: center
.card-label-white
background-color: #ffffff
color: #000000 //Black text for better visibility
border: 1px solid #c0c0c0
.card-label-white:hover
color: #aaaaaa //grey text for better visibility
.card-label-green .card-label-green
background-color: #3cb500 background-color: #3cb500
.card-label-green:hover
color: #000000 //Black hover text for better visibility
.card-label-yellow .card-label-yellow
background-color: #fad900 background-color: #fad900
color: #000000 //Black text for better visibility color: #000000 //Black text for better visibility
@ -158,6 +169,8 @@
.edit-labels-pop-over .edit-labels-pop-over
margin-bottom: 8px margin-bottom: 8px
.card-label .viewer p
margin: 0
.edit-labels-pop-over .shortcut .edit-labels-pop-over .shortcut
display: inline-block display: inline-block

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -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');

View file

@ -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

View file

@ -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

View file

@ -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');

View file

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

71
client/components/main/editor.js Executable file → Normal file
View file

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

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