mirror of
https://github.com/wekan/wekan.git
synced 2025-12-24 03:10:12 +01:00
merged with wekan master @ v5.38
This commit is contained in:
commit
cb418f5e23
743 changed files with 117634 additions and 43043 deletions
12
.babelrc
Normal file
12
.babelrc
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-stage-3"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"COVERAGE": {
|
||||||
|
"plugins": [
|
||||||
|
"istanbul"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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
36
.dockerignore
Normal 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
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
198
.future-snap/old-rebuild-wekan.sh
Executable 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
|
||||||
|
|
@ -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
|
||||||
12
.github/ISSUE_TEMPLATE.md
vendored
12
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -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
62
.github/workflows/codeql-analysis.yml
vendored
Normal 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
160
.github/workflows/test_suite.yml
vendored
Normal 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
4
.gitignore
vendored
|
|
@ -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
10
.gitpod.Dockerfile
vendored
Normal 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
4
.gitpod.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
tasks:
|
||||||
|
- init: npm install
|
||||||
|
image:
|
||||||
|
file: .gitpod.Dockerfile
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
METEOR@1.10.1
|
METEOR@2.2
|
||||||
|
|
|
||||||
190
.meteor/versions
190
.meteor/versions
|
|
@ -1,7 +1,7 @@
|
||||||
3stack:presence@1.1.2
|
3stack:presence@1.1.2
|
||||||
accounts-base@1.6.0
|
accounts-base@1.9.0
|
||||||
accounts-oauth@1.2.0
|
accounts-oauth@1.2.0
|
||||||
accounts-password@1.6.0
|
accounts-password@1.7.1
|
||||||
aldeed:collection2@2.10.0
|
aldeed:collection2@2.10.0
|
||||||
aldeed:collection2-core@1.2.0
|
aldeed:collection2-core@1.2.0
|
||||||
aldeed:schema-deny@1.1.0
|
aldeed:schema-deny@1.1.0
|
||||||
|
|
@ -10,37 +10,20 @@ aldeed:simple-schema@1.5.4
|
||||||
allow-deny@1.1.0
|
allow-deny@1.1.0
|
||||||
arillo:flow-router-helpers@0.5.2
|
arillo:flow-router-helpers@0.5.2
|
||||||
audit-argument-checks@1.0.7
|
audit-argument-checks@1.0.7
|
||||||
autoupdate@1.6.0
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
2
.sandstorm-meteor-1.8/.meteor/.gitignore
vendored
2
.sandstorm-meteor-1.8/.meteor/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
dev_bundle
|
|
||||||
local
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
server
|
|
||||||
browser
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
METEOR@1.8.3
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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'] = {};
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
};
|
|
||||||
4361
.sandstorm-meteor-1.8/package-lock.json
generated
4361
.sandstorm-meteor-1.8/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
2914
CHANGELOG.md
2914
CHANGELOG.md
File diff suppressed because it is too large
Load diff
44
Dockerfile
44
Dockerfile
|
|
@ -1,13 +1,19 @@
|
||||||
FROM ubuntu:rolling
|
FROM quay.io/wekan/ubuntu:groovy-20210115
|
||||||
LABEL maintainer="wekan"
|
LABEL maintainer="wekan"
|
||||||
|
|
||||||
|
# 2020-12-03:
|
||||||
|
# - Above Ubuntu base image copied from Docker Hub ubuntu:groovy-20201125.2
|
||||||
|
# to Quay to avoid Docker Hub rate limits.
|
||||||
|
|
||||||
# Set the environment variables (defaults where required)
|
# Set the environment variables (defaults where required)
|
||||||
# DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
|
# DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
|
||||||
# ENV BUILD_DEPS="paxctl"
|
# ENV BUILD_DEPS="paxctl"
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
|
ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
|
||||||
DEBUG=false \
|
DEBUG=false \
|
||||||
NODE_VERSION=v12.16.1 \
|
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
77
Dockerfile.arm64v8
Normal 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"]
|
||||||
25
README.md
25
README.md
|
|
@ -1,3 +1,5 @@
|
||||||
|
[](https://gitpod.io/#https://github.com/wekan/wekan)
|
||||||
|
|
||||||
# Wekan - Open Source kanban
|
# Wekan - Open Source kanban
|
||||||
|
|
||||||
[](https://github.com/wekan/wekan/graphs/contributors)
|
[](https://github.com/wekan/wekan/graphs/contributors)
|
||||||
|
|
@ -10,6 +12,7 @@
|
||||||
[](https://david-dm.org/wekan/wekan)
|
[](https://david-dm.org/wekan/wekan)
|
||||||
[](https://www.openhub.net/p/wekan)
|
[](https://www.openhub.net/p/wekan)
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_shield)
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_shield)
|
||||||
|
[](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
|
|
||||||
|
|
||||||
|
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_large)
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fwekan%2Fwekan?ref=badge_large)
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
291
api.py
Executable 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
6
client/00-startup.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// PWA
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
navigator.serviceWorker.register('/pwa-service-worker.js');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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 }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,16 @@
|
||||||
|
|
||||||
.activity
|
.activity
|
||||||
margin: 0.5px 0
|
margin: 0.5px 0
|
||||||
|
padding: 6px 0;
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
.member
|
.member
|
||||||
width: 24px
|
width: 32px
|
||||||
height: @width
|
height: @width
|
||||||
|
|
||||||
|
.activity-member
|
||||||
|
font-weight: 700
|
||||||
|
|
||||||
.activity-desc
|
.activity-desc
|
||||||
word-wrap: break-word
|
word-wrap: break-word
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
|
||||||
|
|
@ -1,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}}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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}}")
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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'}}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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' }}
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
display: block
|
display: block
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
padding: 1px 3px
|
padding: 1px 3px
|
||||||
|
|
||||||
background-color: #dbdbdb
|
background-color: #dbdbdb
|
||||||
&:hover, &.is-active
|
&:hover, &.is-active
|
||||||
background-color: #b3b3b3
|
background-color: #b3b3b3
|
||||||
|
|
||||||
&.current, &.almost-due, &.due, &.long-overdue
|
&.current, &.almost-due, &.due, &.long-overdue
|
||||||
color: #fff
|
color: #fff
|
||||||
|
|
||||||
|
|
@ -14,17 +14,17 @@
|
||||||
background-color: #5ba639
|
background-color: #5ba639
|
||||||
&:hover, &.is-active
|
&:hover, &.is-active
|
||||||
background-color: darken(#5ba639, 10)
|
background-color: darken(#5ba639, 10)
|
||||||
|
|
||||||
&.almost-due
|
&.almost-due
|
||||||
background-color: #edc909
|
background-color: #edc909
|
||||||
&:hover, &.is-active
|
&:hover, &.is-active
|
||||||
background-color: darken(#edc909, 10)
|
background-color: darken(#edc909, 10)
|
||||||
|
|
||||||
&.due
|
&.due
|
||||||
background-color: #fa3f00
|
background-color: #fa3f00
|
||||||
&:hover, &.is-active
|
&:hover, &.is-active
|
||||||
background-color: darken(#fa3f00, 10)
|
background-color: darken(#fa3f00, 10)
|
||||||
|
|
||||||
&.long-overdue
|
&.long-overdue
|
||||||
background-color: #fd5d47
|
background-color: #fd5d47
|
||||||
&:hover, &.is-active
|
&:hover, &.is-active
|
||||||
|
|
@ -57,3 +57,7 @@
|
||||||
-webkit-font-smoothing: antialiased
|
-webkit-font-smoothing: antialiased
|
||||||
margin-right: 0.3em
|
margin-right: 0.3em
|
||||||
|
|
||||||
|
.customfield-date
|
||||||
|
display: block
|
||||||
|
border-radius: 4px
|
||||||
|
padding: 1px 3px
|
||||||
|
|
|
||||||
7
client/components/cards/cardDescription.jade
Normal file
7
client/components/cards/cardDescription.jade
Normal 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}}
|
||||||
34
client/components/cards/cardDescription.js
Normal file
34
client/components/cards/cardDescription.js
Normal 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');
|
||||||
59
client/components/cards/cardDescription.styl
Normal file
59
client/components/cards/cardDescription.styl
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,9 @@
|
||||||
width: 11px
|
width: 11px
|
||||||
height: @width
|
height: @width
|
||||||
border-radius: 2px
|
border-radius: 2px
|
||||||
margin-left: 3px
|
margin-right: 3px
|
||||||
|
margin-bottom: 3px
|
||||||
|
|
||||||
.minicard-custom-fields
|
.minicard-custom-fields
|
||||||
display:block;
|
display:block;
|
||||||
.minicard-custom-field
|
.minicard-custom-field
|
||||||
|
|
@ -161,15 +163,18 @@
|
||||||
line-height: 12px
|
line-height: 12px
|
||||||
|
|
||||||
.minicard-members,
|
.minicard-members,
|
||||||
.minicard-assignees
|
.minicard-assignees,
|
||||||
|
.minicard-creator
|
||||||
float: right
|
float: right
|
||||||
margin: 2px -8px 12px 0
|
margin-left: 5px
|
||||||
|
margin-bottom: 4px
|
||||||
|
|
||||||
.member
|
.member
|
||||||
float: right
|
float: right
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
height: 28px
|
height: 28px
|
||||||
width: @height
|
width: @height
|
||||||
|
margin-bottom: 4px
|
||||||
|
|
||||||
.assignee
|
.assignee
|
||||||
float: right
|
float: right
|
||||||
|
|
@ -178,7 +183,13 @@
|
||||||
width: @height
|
width: @height
|
||||||
|
|
||||||
+ .badges
|
+ .badges
|
||||||
margin-top: 10px
|
margin-top: 5px
|
||||||
|
|
||||||
|
.minicard-assignees
|
||||||
|
border-bottom: 1px solid red
|
||||||
|
|
||||||
|
.minicard-creator
|
||||||
|
border-bottom: 1px solid green
|
||||||
|
|
||||||
.minicard-members:empty,
|
.minicard-members:empty,
|
||||||
.minicard-assignees:empty
|
.minicard-assignees:empty
|
||||||
|
|
@ -299,3 +310,8 @@ minicard-color(background, color...)
|
||||||
|
|
||||||
.minicard-indigo
|
.minicard-indigo
|
||||||
minicard-color(#4b0082, #ffffff) //White text for better visibility
|
minicard-color(#4b0082, #ffffff) //White text for better visibility
|
||||||
|
|
||||||
|
.text-red
|
||||||
|
color:red
|
||||||
|
.text-green
|
||||||
|
color:green
|
||||||
|
|
|
||||||
44
client/components/cards/resultCard.jade
Normal file
44
client/components/cards/resultCard.jade
Normal 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
|
||||||
11
client/components/cards/resultCard.js
Normal file
11
client/components/cards/resultCard.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
Template.resultCard.helpers({
|
||||||
|
userId() {
|
||||||
|
return Meteor.userId();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
BlazeComponent.extendComponent({
|
||||||
|
events() {
|
||||||
|
return [{}];
|
||||||
|
},
|
||||||
|
}).register('resultCard');
|
||||||
24
client/components/cards/resultCard.styl
Normal file
24
client/components/cards/resultCard.styl
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
37
client/components/import/csvMembersMapper.js
Normal file
37
client/components/import/csvMembersMapper.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
4
client/components/import/import.styl
vendored
4
client/components/import/import.styl
vendored
|
|
@ -47,3 +47,7 @@
|
||||||
|
|
||||||
a.show-mapping
|
a.show-mapping
|
||||||
text-decoration underline
|
text-decoration underline
|
||||||
|
|
||||||
|
.import-members-map-note
|
||||||
|
font-size: 90%
|
||||||
|
font-weight: bold
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export function getMembersToMap(data) {
|
export function trelloGetMembersToMap(data) {
|
||||||
// we will work on the list itself (an ordered array of objects) when a
|
// we will work on the list itself (an ordered array of objects) when a
|
||||||
// mapping is done, we add a 'wekan' field to the object representing the
|
// mapping is done, we add a 'wekan' field to the object representing the
|
||||||
// imported member
|
// imported member
|
||||||
|
|
|
||||||
|
|
@ -1,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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
| (
|
| (
|
||||||
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'}}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
17
client/components/main/brokenCards.jade
Normal file
17
client/components/main/brokenCards.jade
Normal 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)
|
||||||
18
client/components/main/brokenCards.js
Normal file
18
client/components/main/brokenCards.js
Normal 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');
|
||||||
31
client/components/main/brokenCards.styl
Normal file
31
client/components/main/brokenCards.styl
Normal 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
|
||||||
57
client/components/main/dueCards.jade
Normal file
57
client/components/main/dueCards.jade
Normal 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
|
||||||
111
client/components/main/dueCards.js
Normal file
111
client/components/main/dueCards.js
Normal 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');
|
||||||
4
client/components/main/dueCards.styl
Normal file
4
client/components/main/dueCards.styl
Normal 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
71
client/components/main/editor.js
Executable file → Normal 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
Loading…
Add table
Add a link
Reference in a new issue