Compare commits

..

No commits in common. "main" and "v1.70" have entirely different histories.
main ... v1.70

1457 changed files with 45775 additions and 597445 deletions

View file

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

View file

@ -1,291 +0,0 @@
FROM ubuntu:24.04
LABEL maintainer="wekan"
LABEL org.opencontainers.image.ref.name="ubuntu"
LABEL org.opencontainers.image.version="24.04"
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# 2022-04-25:
# - gyp does not yet work with Ubuntu 22.04 ubuntu:rolling,
# so changing to 21.10. https://github.com/wekan/wekan/issues/4488
ENV BUILD_DEPS="apt-utils gnupg gosu wget bzip2 g++ iproute2 apt-transport-https libarchive-tools"
ENV DEV_DEPS="curl python3 ca-certificates build-essential git"
ARG DEBIAN_FRONTEND=noninteractive
ENV \
DEBUG=false \
NODE_VERSION=v14.21.4 \
METEOR_RELEASE=METEOR@2.14 \
USE_EDGE=false \
METEOR_EDGE=1.5-beta.17 \
NPM_VERSION=6.14.17 \
FIBERS_VERSION=4.0.1 \
ARCHITECTURE=linux-x64 \
SRC_PATH=./ \
WITH_API=true \
RESULTS_PER_PAGE="" \
DEFAULT_BOARD_ID="" \
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90 \
ATTACHMENTS_UPLOAD_EXTERNAL_PROGRAM="" \
ATTACHMENTS_UPLOAD_MIME_TYPES="" \
ATTACHMENTS_UPLOAD_MAX_SIZE=0 \
AVATARS_UPLOAD_EXTERNAL_PROGRAM="" \
AVATARS_UPLOAD_MIME_TYPES="" \
AVATARS_UPLOAD_MAX_SIZE=0 \
RICHER_CARD_COMMENT_EDITOR=false \
CARD_OPENED_WEBHOOK_ENABLED=false \
MAX_IMAGE_PIXEL="" \
IMAGE_COMPRESS_RATIO="" \
NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
BIGEVENTS_PATTERN=NONE \
NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
NOTIFY_DUE_AT_HOUR_OF_DAY="" \
EMAIL_NOTIFICATION_TIMEOUT=30000 \
MATOMO_ADDRESS="" \
MATOMO_SITE_ID="" \
MATOMO_DO_NOT_TRACK=true \
MATOMO_WITH_USERNAME=false \
METRICS_ALLOWED_IP_ADDRESSES="" \
BROWSER_POLICY_ENABLED=true \
TRUSTED_URL="" \
WEBHOOKS_ATTRIBUTES="" \
OAUTH2_ENABLED=false \
OIDC_REDIRECTION_ENABLED=false \
OAUTH2_CA_CERT="" \
OAUTH2_ADFS_ENABLED=false \
OAUTH2_B2C_ENABLED=false \
OAUTH2_LOGIN_STYLE=redirect \
OAUTH2_CLIENT_ID="" \
OAUTH2_SECRET="" \
OAUTH2_SERVER_URL="" \
OAUTH2_AUTH_ENDPOINT="" \
OAUTH2_USERINFO_ENDPOINT="" \
OAUTH2_TOKEN_ENDPOINT="" \
OAUTH2_ID_MAP="" \
OAUTH2_USERNAME_MAP="" \
OAUTH2_FULLNAME_MAP="" \
OAUTH2_ID_TOKEN_WHITELIST_FIELDS="" \
OAUTH2_REQUEST_PERMISSIONS='openid profile email' \
OAUTH2_EMAIL_MAP="" \
LDAP_ENABLE=false \
LDAP_PORT=389 \
LDAP_HOST="" \
LDAP_AD_SIMPLE_AUTH="" \
LDAP_USER_AUTHENTICATION=false \
LDAP_USER_AUTHENTICATION_FIELD=uid \
LDAP_BASEDN="" \
LDAP_LOGIN_FALLBACK=false \
LDAP_RECONNECT=true \
LDAP_TIMEOUT=10000 \
LDAP_IDLE_TIMEOUT=10000 \
LDAP_CONNECT_TIMEOUT=10000 \
LDAP_AUTHENTIFICATION=false \
LDAP_AUTHENTIFICATION_USERDN="" \
LDAP_AUTHENTIFICATION_PASSWORD="" \
LDAP_LOG_ENABLED=false \
LDAP_BACKGROUND_SYNC=false \
LDAP_BACKGROUND_SYNC_INTERVAL="" \
LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false \
LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false \
LDAP_ENCRYPTION=false \
LDAP_CA_CERT="" \
LDAP_REJECT_UNAUTHORIZED=false \
LDAP_USER_SEARCH_FILTER="" \
LDAP_USER_SEARCH_SCOPE="" \
LDAP_USER_SEARCH_FIELD="" \
LDAP_SEARCH_PAGE_SIZE=0 \
LDAP_SEARCH_SIZE_LIMIT=0 \
LDAP_GROUP_FILTER_ENABLE=false \
LDAP_GROUP_FILTER_OBJECTCLASS="" \
LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE="" \
LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE="" \
LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT="" \
LDAP_GROUP_FILTER_GROUP_NAME="" \
LDAP_UNIQUE_IDENTIFIER_FIELD="" \
LDAP_UTF8_NAMES_SLUGIFY=true \
LDAP_USERNAME_FIELD="" \
LDAP_FULLNAME_FIELD="" \
LDAP_MERGE_EXISTING_USERS=false \
LDAP_EMAIL_FIELD="" \
LDAP_EMAIL_MATCH_ENABLE=false \
LDAP_EMAIL_MATCH_REQUIRE=false \
LDAP_EMAIL_MATCH_VERIFIED=false \
LDAP_SYNC_USER_DATA=false \
LDAP_SYNC_USER_DATA_FIELDMAP="" \
LDAP_SYNC_GROUP_ROLES="" \
LDAP_DEFAULT_DOMAIN="" \
LDAP_SYNC_ADMIN_STATUS="" \
LDAP_SYNC_ADMIN_GROUPS="" \
HEADER_LOGIN_ID="" \
HEADER_LOGIN_FIRSTNAME="" \
HEADER_LOGIN_LASTNAME="" \
HEADER_LOGIN_EMAIL="" \
LOGOUT_WITH_TIMER=false \
LOGOUT_IN="" \
LOGOUT_ON_HOURS="" \
LOGOUT_ON_MINUTES="" \
CORS="" \
CORS_ALLOW_HEADERS="" \
CORS_EXPOSE_HEADERS="" \
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="" \
ORACLE_OIM_ENABLED=false \
WAIT_SPINNER="" \
WRITABLE_PATH=/data \
S3=""
# NODE_OPTIONS="--max_old_space_size=4096"
#---------------------------------------------
# == at docker-compose.yml: AUTOLOGIN WITH OIDC/OAUTH2 ====
# https://github.com/wekan/wekan/wiki/autologin
#- OIDC_REDIRECTION_ENABLED=true
#---------------------------------------------------------------------
ENV PATH=$PATH:/home/wekan/.meteor/
RUN <<EOR
echo "export PATH=$PATH" >> /etc/environment
EOR
# Copy source dir
RUN <<EOR
set -o xtrace
mkdir -p /home/wekan/app/.meteor
mkdir -p /home/wekan/app/packages
EOR
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 \
tests \
/home/wekan/app/tests/
COPY \
packages \
/home/wekan/app/packages/
# Install OS
RUN <<EOR
set -o xtrace
# Add non-root user wekan
useradd --user-group --system --home-dir /home/wekan wekan
# OS dependencies
apt-get update --assume-yes
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS} ${DEV_DEPS}
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
cp $(which tar) $(which tar)~
ln -sf $(which bsdtar) $(which tar)
# Install NodeJS
cd /tmp
# Download nodejs
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz"
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
# Verify nodejs authenticity
grep "node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
rm -f "SHASUMS256.txt"
# Install Node
tar xzf "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
rm "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" "SHASUMS256.txt"
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/${NODE_VERSION} /home/wekan/.config"
# Install node dependencies
npm install -g npm@${NPM_VERSION}
chown --recursive wekan:wekan /home/wekan/.config
# Install Meteor
cd /home/wekan
chown --recursive wekan:wekan /home/wekan
echo "Starting meteor ${METEOR_RELEASE} installation... \n"
gosu wekan:wekan curl https://install.meteor.com/ | /bin/sh
mv /root/.meteor /home/wekan/
chown --recursive wekan:wekan /home/wekan/.meteor
# sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js
cd /home/wekan/.meteor
gosu wekan:wekan /home/wekan/.meteor/meteor -- help
# Build app (Development)
cd /home/wekan/app
gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js
gosu wekan:wekan /home/wekan/.meteor/meteor npm install
# Put back the original tar
mv $(which tar)~ $(which tar)
# Cleanup
apt-get remove --purge --assume-yes ${BUILD_DEPS}
apt-get install --assume-yes --no-install-recommends build-essential
apt-get autoremove --assume-yes
apt-get clean --assume-yes
rm -Rf /tmp/*
rm -Rf /var/lib/apt/lists/*
rm -Rf /var/cache/apt
rm -Rf /var/lib/apt/lists
rm -Rf /home/wekan/app_build
mkdir /data
chown wekan --recursive /data
EOR
USER wekan
ENV PORT=3000
EXPOSE $PORT
STOPSIGNAL SIGKILL
WORKDIR /home/wekan/app
#---------------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap:
# NODE_OPTIONS="--max_old_space_size=4096"
# Add more stack:
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
#---------------------------------------------------------------------
#
CMD ["/home/wekan/.meteor/meteor", "run", "--verbose", "--settings", "settings.json"]

View file

@ -1,14 +0,0 @@
#!/bin/bash
cd /home/wekan/app
rm -rf node_modules
/home/wekan/.meteor/meteor npm install
rm -rf .build
/home/wekan/.meteor/meteor build .build --directory
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
cd .build/bundle/programs/server
rm -rf node_modules
/home/wekan/.meteor/meteor npm install
cd node_modules/fibers
node build.js
cd /home/wekan/app

View file

@ -1,17 +0,0 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"dockerComposeFile": ["docker-compose.yml", "docker-compose.extend.yml"],
"service": "wekan-dev",
"workspaceFolder": "/home/wekan/app",
"extensions": [
"mutantdino.resourcemonitor",
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"codezombiech.gitignore",
"eamodio.gitlens",
"gruntfuggly.todo-tree",
"dotjoshjohnson.xml",
"redhat.vscode-yaml",
"vuhrmeister.vscode-meteor"
]
}

View file

@ -1,51 +0,0 @@
version: '3.7'
services:
wekandb-dev:
image: mongo:6
container_name: wekan-dev-db
restart: unless-stopped
command: mongod --oplogSize 128
networks:
- wekan-dev-tier
expose:
- 27017
volumes:
- /etc/localtime:/etc/localtime:ro
- ./volumes/wekan-db:/data/db
- ./volumes/wekan-db-dump:/dump
wekan-dev:
container_name: wekan-dev-app
restart: always
networks:
- wekan-dev-tier
build:
context: ..
dockerfile: .devcontainer/Dockerfile
ports:
- 3000:3000
- 9229:9229
environment:
- MONGO_URL=mongodb://wekandb-dev:27017/wekan
- ROOT_URL=http://localhost:3000
- WITH_API=true
- RICHER_CARD_COMMENT_EDITOR=true
- BROWSER_POLICY_ENABLED=true
- WRITABLE_PATH=/data
depends_on:
- wekandb-dev
volumes:
- /etc/localtime:/etc/localtime:ro
- ./volumes/data:/data
- ../client:/home/wekan/app/client
- ../models:/home/wekan/app/models
- ../config:/home/wekan/app/config
- ../imports:/home/wekan/app/imports
- ../server:/home/wekan/app/server
- ../public:/home/wekan/app/public
networks:
wekan-dev-tier:
driver: bridge

View file

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

View file

@ -8,20 +8,3 @@ end_of_line = lf
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.{js,html}]
charset = utf-8
end_of_line = lf
indent_brace_style = 1TBS
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
quote_type = auto
spaces_around_operators = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

View file

@ -1,2 +0,0 @@
packages/*
.snap-meteor-1.8/*

View file

@ -1,20 +1,16 @@
{ {
"extends": [ "extends": "eslint:recommended",
"eslint:recommended",
"plugin:meteor/recommended",
"prettier",
"prettier/standard"
],
"env": { "env": {
"es6": true, "es6": true,
"node": true, "node": true,
"browser": true, "browser": true
"meteor": true
}, },
"parser": "babel-eslint",
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018, "ecmaVersion": 6,
"sourceType": "module" "sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
}, },
"rules": { "rules": {
"strict": 0, "strict": 0,
@ -24,7 +20,7 @@
"consistent-return": 2, "consistent-return": 2,
"dot-notation": 2, "dot-notation": 2,
"eqeqeq": 2, "eqeqeq": 2,
"indent": 0, "indent": [2, 2],
"no-cond-assign": 2, "no-cond-assign": 2,
"no-constant-condition": 2, "no-constant-condition": 2,
"no-eval": 2, "no-eval": 2,
@ -32,12 +28,11 @@
"no-unneeded-ternary": 2, "no-unneeded-ternary": 2,
"radix": 2, "radix": 2,
"semi": [2, "always"], "semi": [2, "always"],
"camelcase": [2, { "properties": "never" }], "camelcase": [2, {"properties": "never"}],
"comma-spacing": 2, "comma-spacing": 2,
"comma-style": 2, "comma-style": 2,
"eol-last": 2, "eol-last": 2,
"linebreak-style": [2, "unix"], "linebreak-style": [2, "unix"],
"meteor/audit-argument-checks": 0,
"new-parens": 2, "new-parens": 2,
"no-lonely-if": 2, "no-lonely-if": 2,
"no-multiple-empty-lines": 2, "no-multiple-empty-lines": 2,
@ -45,9 +40,10 @@
"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", { "avoidEscape": true }], "quotes": [2, "single"],
"semi-spacing": 2, "semi-spacing": 2,
"space-unary-ops": 2, "space-unary-ops": 2,
"arrow-parens": 2,
"arrow-spacing": 2, "arrow-spacing": 2,
"no-class-assign": 2, "no-class-assign": 2,
"no-dupe-class-members": 2, "no-dupe-class-members": 2,
@ -56,27 +52,8 @@
"prefer-const": 2, "prefer-const": 2,
"prefer-spread": 2, "prefer-spread": 2,
"prefer-template": 2, "prefer-template": 2,
"no-unused-vars": "warn", "no-unused-vars" : "warn"
"prettier/prettier": [
"error",
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "all"
}
],
"meteor/no-session": 0
}, },
"settings": {
"import/resolver": {
"meteor": {
"extensions": [".js", ".jsx"]
}
}
},
"plugins": ["prettier", "meteor"],
"globals": { "globals": {
"Meteor": false, "Meteor": false,
"Session": false, "Session": false,
@ -122,9 +99,8 @@
"Activities": true, "Activities": true,
"Attachments": true, "Attachments": true,
"Boards": true, "Boards": true,
"CardCommentReactions": true,
"CardComments": true, "CardComments": true,
"DatePicker": true, "DatePicker" : true,
"Cards": true, "Cards": true,
"CustomFields": true, "CustomFields": true,
"Lists": true, "Lists": true,
@ -147,7 +123,6 @@
"allowIsBoardMemberByCard": true, "allowIsBoardMemberByCard": true,
"allowIsBoardMemberCommentOnly": true, "allowIsBoardMemberCommentOnly": true,
"allowIsBoardMemberNoComments": true, "allowIsBoardMemberNoComments": true,
"allowIsBoardMemberWorker": true,
"Emoji": true, "Emoji": true,
"Checklists": true, "Checklists": true,
"Settings": true, "Settings": true,
@ -157,7 +132,6 @@
"Integrations": true, "Integrations": true,
"HTTP": true, "HTTP": true,
"AccountSettings": true, "AccountSettings": true,
"TableVisibilityModeSettings": true,
"Announcements": true, "Announcements": true,
"Swimlanes": true, "Swimlanes": true,
"ChecklistItems": true, "ChecklistItems": true,

View file

@ -1,257 +0,0 @@
name: wekan
version: 0
version-script: git describe --tags | cut -c 2-
summary: The open-source kanban
description: |
Wekan is an open-source and collaborative kanban board application.
Whether youre maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
Depending on target environment, some configuration settings might need to be adjusted.
For full list of configuration options call:
$ wekan.help
confinement: strict
grade: stable
architectures:
- amd64
plugs:
mongodb-plug:
interface: content
target: $SNAP_DATA/shared
hooks:
configure:
plugs:
- network
- network-bind
slots:
mongodb-slot:
interface: content
write:
- $SNAP_DATA/share
apps:
wekan:
command: wekan-control
daemon: simple
plugs: [network, network-bind]
mongodb:
command: mongodb-control
daemon: simple
plugs: [network, network-bind]
caddy:
command: caddy-control
daemon: simple
plugs: [network, network-bind]
help:
command: wekan-help
database-backup:
command: mongodb-backup
plugs: [network, network-bind]
database-list-backups:
command: ls -al $SNAP_COMMON/db-backups/
database-restore:
command: mongodb-restore
plugs: [network, network-bind]
parts:
mongodb:
source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.6.tgz
plugin: dump
stage-packages: [libssl1.0.0, libcurl3]
filesets:
mongo:
- usr
- bin
- lib
stage:
- $mongo
prime:
- $mongo
wekan:
source: .
plugin: nodejs
node-engine: 14.21.3
node-packages:
- node-gyp
- node-pre-gyp
- fibers
build-packages:
- ca-certificates
- apt-utils
- python
- python3
- g++
- capnproto
- curl
- libcurl3
- execstack
- nodejs
- npm
stage-packages:
- libfontconfig1
override-build: |
echo "Cleaning environment first"
rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
# Create the OpenAPI specification
rm -rf .build
## Use Meteor 1.8.x on Snap
#rm -rf .meteor
#mv .snap-meteor-1.8/.meteor .
#mv .snap-meteor-1.8/package.json .
#mv .snap-meteor-1.8/package-lock.json .
## Meteor 1.9.x has changes to Buffer() => Buffer.alloc(), so reverting those
#mv .snap-meteor-1.8/cfs_access-point.txt fix-download-unicode/
#mv .snap-meteor-1.8/export.js models/
#mv .snap-meteor-1.8/wekanCreator.js models/
#mv .snap-meteor-1.8/ldap.js packages/wekan-ldap/server/ldap.js
#mv .snap-meteor-1.8/oidc_server.js packages/wekan-oidc/oidc_server.js
rm -rf .snap-meteor-1.8
#mkdir -p .build/python
#cd .build/python
#git clone --depth 1 -b master https://github.com/Kronuz/esprima-python
#cd esprima-python
#python3 setup.py install
#cd ../../..
#mkdir -p ./public/api
#python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml
# we temporary need api2html and mkdirp
#npm install -g api2html@0.3.0
#npm install -g mkdirp
#api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml
#npm uninstall -g mkdirp
#npm uninstall -g api2html
# Node Fibers 100% CPU usage issue:
# https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
# https://github.com/meteor/meteor/issues/9796#issuecomment-381676326
# https://github.com/sandstorm-io/sandstorm/blob/0f1fec013fe7208ed0fd97eb88b31b77e3c61f42/shell/server/00-startup.js#L99-L129
# Also see beginning of wekan/server/authentication.js
# import Fiber from "fibers";
# Fiber.poolSize = 1e9;
# OLD: Download node version 8.12.0 prerelease build => Official node 8.12.0 has been released
# Description at https://releases.wekan.team/node.txt
##echo "375bd8db50b9c692c0bbba6e96d4114cd29bee3770f901c1ff2249d1038f1348 node" >> node-SHASUMS256.txt.asc
##curl https://releases.wekan.team/node -o node
# Verify Fibers patched node authenticity
##echo "Fibers 100% CPU issue patched node authenticity:"
##grep node node-SHASUMS256.txt.asc | shasum -a 256 -c -
##rm -f node-SHASUMS256.txt.asc
##chmod +x node
##mv node `which node`
# DOES NOT WORK: paxctl fix.
# Removed from build-packages: - paxctl
#echo "Applying paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303"
#paxctl -mC `which node`
#echo "Installing npm"
#curl -L https://www.npmjs.com/install.sh | sh
echo "Installing meteor"
curl https://install.meteor.com/ -o install_meteor.sh
#sed -i "s|RELEASE=.*|RELEASE=\"1.8.1-beta.0\"|g" install_meteor.sh
chmod +x install_meteor.sh
sh install_meteor.sh
rm install_meteor.sh
# REPOS BELOW ARE INCLUDED TO WEKAN REPO
#if [ ! -d "packages" ]; then
# mkdir packages
#fi
#if [ ! -d "packages/kadira-flow-router" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router
# cd ..
#fi
#if [ ! -d "packages/meteor-useraccounts-core" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core
# sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' meteor-useraccounts-core/package.js
# cd ..
#fi
#if [ ! -d "packages/meteor-accounts-cas" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git meteor-accounts-cas
# cd ..
#fi
#if [ ! -d "packages/wekan-ldap" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git
# cd ..
#fi
#if [ ! -d "packages/wekan-scrollbar" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git
# cd ..
#fi
#if [ ! -d "packages/wekan_accounts-oidc" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.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
# cd ..
#fi
#if [ ! -d "packages/markdown" ]; then
# cd packages
# git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git
# cd ..
#fi
rm -rf .build
meteor add standard-minifier-js --allow-superuser
meteor npm install --allow-superuser
meteor npm install --allow-superuser --save babel-runtime
meteor build .build --directory --allow-superuser
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
#cd .build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt
#rm -rf node_modules/bcrypt
#meteor npm install --save bcrypt
# Change from npm-bcrypt directory back to .build/bundle/programs/server directory.
#cd ../../../../
# Change to directory .build/bundle/programs/server
cd .build/bundle/programs/server
npm install
npm install --allow-superuser --save babel-runtime
#meteor npm install --save bcrypt
# Change back to Wekan source directory
cd ../../../..
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
# Meteor 1.8.x additional .swp remove
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
organize:
README: README.wekan
prime:
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
helpers:
source: snap-src
plugin: dump
caddy:
plugin: dump
source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
source-type: tar
organize:
caddy: bin/caddy
CHANGES.txt: CADDY_CHANGES.txt
EULA.txt: CADDY_EULA.txt
LICENSES.txt: CADDY_LICENSES.txt
README.txt: CADDY_README.txt
stage:
- -init

View file

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

View file

@ -1,155 +0,0 @@
name: wekan
version: git
summary: The open-source kanban
description: |
Wekan is an open-source and collaborative kanban board application.
Whether youre maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
Depending on target environment, some configuration settings might need to be adjusted.
For full list of configuration options call:
$ wekan.help
confinement: strict
grade: stable
base: core18
architectures:
- amd64
plugs:
mongodb-plug:
interface: content
target: $SNAP_DATA/shared
hooks:
configure:
plugs:
- network
- network-bind
slots:
mongodb-slot:
interface: content
write:
- $SNAP_DATA/share
apps:
wekan:
command: wekan-control
daemon: simple
plugs: [network, network-bind]
mongodb:
command: mongodb-control
daemon: simple
plugs: [network, network-bind]
caddy:
command: caddy-control
daemon: simple
plugs: [network, network-bind]
help:
command: wekan-help
database-backup:
command: mongodb-backup
plugs: [network, network-bind]
database-list-backups:
command: ls -al $SNAP_COMMON/db-backups/
database-restore:
command: mongodb-restore
plugs: [network, network-bind]
parts:
mongodb:
source: https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.2/multiverse/binary-amd64/mongodb-org-server_4.2.2_amd64.deb
#https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.14.tgz
#https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
plugin: dump
stage-packages: [libssl1.0.0, libcurl3]
filesets:
mongo:
- usr
- bin
- lib
stage:
- $mongo
prime:
- $mongo
wekan:
source: .
plugin: nodejs
node-engine: 14.19.0
node-packages:
- node-gyp
- node-pre-gyp
- fibers
build-packages:
- ca-certificates
- apt-utils
- build-essential
- python
- python3
- g++
- capnproto
- curl
- libcurl3
- execstack
- nodejs
- npm
stage-packages:
- libfontconfig1
override-build: |
echo "Cleaning environment first"
rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
rm -rf .build
echo "Installing meteor"
curl https://install.meteor.com/ -o install_meteor.sh
chmod +x install_meteor.sh
sh install_meteor.sh
rm install_meteor.sh
rm -rf .build
meteor add standard-minifier-js --allow-superuser
meteor npm install --allow-superuser
meteor npm install --allow-superuser --save babel-runtime
meteor build .build --directory --allow-superuser
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
cd .build/bundle/programs/server
npm install
npm install --allow-superuser --save babel-runtime
# Change back to Wekan source directory
cd ../../../..
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
organize:
README: README.wekan
prime:
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
helpers:
source: snap-src
plugin: dump
caddy:
plugin: dump
source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
source-type: tar
organize:
caddy: bin/caddy
CHANGES.txt: CADDY_CHANGES.txt
EULA.txt: CADDY_EULA.txt
LICENSES.txt: CADDY_LICENSES.txt
README.txt: CADDY_README.txt
stage:
- -init

View file

@ -1,183 +0,0 @@
name: wekan
version: '6.21'
base: core20
summary: Open Source kanban
description: |
WeKan ® is an Open Source and collaborative kanban board application.
Whether youre maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
Depending on target environment, some configuration settings might need to be adjusted.
For full list of configuration options call:
$ wekan.help
confinement: strict
grade: stable
architectures:
- build-on: amd64
run-on: amd64
- build-on: arm64
run-on: arm64
- build-on: ppc64el
run-on: ppc64el
- build-on: s390x
run-on: s390x
plugs:
mongodb-plug:
interface: content
target: $SNAP_DATA/shared
hooks:
configure:
plugs:
- network
- network-bind
slots:
mongodb-slot:
interface: content
write:
- $SNAP_DATA/share
apps:
wekan:
command: wekan-control
daemon: simple
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
restart-condition: on-failure
mongodb:
command: mongodb-control
daemon: simple
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
restart-condition: on-failure
caddy:
command: caddy-control
daemon: simple
plugs: [network, network-bind]
help:
command: wekan-help
database-backup:
command: mongodb-backup
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
database-list-backups:
command: ls -al $SNAP_COMMON/db-backups/
database-restore:
command: mongodb-restore
plugs: [network, network-bind, mount-observe, system-observe, bluetooth-control]
parts:
mongodb:
plugin: dump
source:
- on amd64: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-amd64/mongodb-org-server_4.4.13_amd64.deb
- on arm64: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-arm64/mongodb-org-server_4.4.13_arm64.deb
- on ppc64el: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-ppc64el/mongodb-org-server_4.4.13_ppc64el.deb
- on s390x: https://repo.mongodb.org/apt/ubuntu/dists/focal/mongodb-org/4.4/multiverse/binary-s390x/mongodb-org-server_4.4.13_s390x.deb
stage-packages:
- libssl1.1
- libcurl3-dev
- libcurl4-openssl-dev
filesets:
mongo:
- usr
- bin
- lib
stage:
- $mongo
prime:
- $mongo
wekan:
#plugin: npm
plugin: dump
source:
# Fixed URLs to some allowed GitHub releases URL.
# Non-GitHub build server file urls are not allowed at 2022-03-02 and later.
- on amd64: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-amd64.zip
- on arm64: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-arm64.zip
- on ppc64el: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-ppc64el.zip
- on s390x: https://github.com/wekan/wekan/releases/download/v6.20/wekan-6.20-s390x.zip
# npm-node-version: 14.19.1
# node-packages:
# - node-gyp
# - node-pre-gyp
# - fibers
# build-packages:
# - npm
# - build-essential
# - ca-certificates
# - apt-utils
# - python
# - python3
# - g++
# - capnproto
# - curl
# - execstack
# - nodejs
# - npm
# - p7zip-full
# stage-packages:
# - libfontconfig1
override-build: |
cp -r bundle/* $SNAPCRAFT_PART_INSTALL/
cp bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
snapcraftctl build
organize:
README: README.wekan
prime:
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
- -lib/node_modules/weka*
helpers:
source: snap-src
plugin: dump
caddy:
plugin: dump
## Caddy v1 is not developed anymore. TODO: Sometime migrate to Caddy v2.
## https://caddy.community/t/caddyfile-v1-adapter/9129
## https://github.com/caddyserver/caddy/tree/v1
#source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
#source-type: tar
# Using last working binary that was downloaded from above URL to Wekan Snap,
# and .txt files from https://github.com/caddyserver/caddy/tree/v1/dist
source: https://wekan.github.io/caddy-v1-linux-amd64.7z
source-type: 7z
organize:
caddy: bin/caddy
CHANGES.txt: CADDY_CHANGES.txt
EULA.txt: CADDY_EULA.txt
LICENSES.txt: CADDY_LICENSES.txt
README.txt: CADDY_README.txt
stage:
- -init
caddy2:
plugin: dump
source:
# Fixed URLs to some allowed GitHub releases URL.
# Non-GitHub build server file urls are not allowed at 2022-03-02 and later.
- on amd64: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-amd64.zip
- on arm64: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-arm64.zip
- on ppc64el: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-ppc64el.zip
- on s390x: https://github.com/wekan/wekan/releases/download/v6.20/caddy-v2-s390x.zip
source-type: zip
organize:
caddy: bin/caddy
CHANGES.txt: CADDY_CHANGES.txt
EULA.txt: CADDY_EULA.txt
LICENSES.txt: CADDY_LICENSES.txt
README.txt: CADDY_README.txt
stage:
- -init

3
.gitattributes vendored
View file

@ -1,3 +0,0 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf

3
.github/FUNDING.yml vendored
View file

@ -1,3 +0,0 @@
# These are supported funding model platforms
custom: ['https://wekan.team/commercial-support/']

View file

@ -1,55 +1,22 @@
## Issue ## Issue
Please report these issues elsewhere: **Server Setup Information**:
- SECURITY ISSUES, PGP EMAIL: https://github.com/wekan/wekan/blob/main/SECURITY.md
- UCS: https://github.com/wekan/univention/issues
If WeKan Snap is slow, try this: https://github.com/wekan/wekan/wiki/Cron
**[PLEASE UPGRADE](https://github.com/wekan/wekan/wiki/Backup)** to the newest
WeKan ® before reporting an issue, if possible.
Please search existing Open and Closed issues, most questions have already been answered.
If you can not login for any reason: https://github.com/wekan/wekan/wiki/Forgot-Password
Email settings, only SMTP MAIL_URL and MAIL_FROM are in use:
https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
### Server Setup Information
Please anonymize info, and do not any of your Wekan board URLs, passwords,
API tokens etc to this public issue.
* Did you test in newest Wekan?: * Did you test in newest Wekan?:
* Did you configure root-url correctly so Wekan cards open correctly (see https://github.com/wekan/wekan/wiki/Settings)? * For new Wekan install, did you configure root-url correctly https://github.com/wekan/wekan/wiki/Settings ?
* Wekan version:
* If this is about old version of Wekan, what upgrade problem you have?:
* Operating System: * Operating System:
* Deployment Method (Snap/Docker/Sandstorm/bundle/source): * Deployment Method(snap/docker/sandstorm/mongodb bundle/source):
* Http frontend if any (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first): * Http frontend if any (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first):
* Node.js Version: * Node Version:
* MongoDB Version: * MongoDB Version:
* What webbrowser version are you using (Wekan should work on all modern browsers that support Javascript)? * ROOT_URL environment variable http(s)://(subdomain).example.com(/suburl):
### Problem description
Add a recorded animated gif (e.g. with https://github.com/phw/peek) about
how it works currently, and screenshot mockups how it should work.
#### Reproduction Steps
#### Logs
Check Right Click / Inspect / Console in you browser - generally Chromium
based browsers show more detailed info than Firefox based browsers.
Please anonymize logs.
Snap: sudo snap logs wekan.wekan
Docker: sudo docker logs wekan-app
If logs are very long, attach them in .zip file
**Problem description**:
- *REQUIRED: Add recorded animated gif about how it works currently, and screenshot mockups how it should work. Use peek to record animgif in Linux https://github.com/phw/peek*
- *Explain steps how to reproduce*
- *In webbrowser, what does show Right Click / Inspect / Console ? Chrome shows more detailed info than Firefox.*
- *If using Snap, what does show command `sudo snap logs wekan.wekan` ?*
- *If using Docker, what does show command `sudo docker logs wekan-app` ?*
- *If logs are very long, attach them in .zip file*

View file

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View file

@ -1,14 +0,0 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View file

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

View file

@ -1,20 +0,0 @@
name: Docker Image CI
on:
push:
branches:
- main
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build the Docker image
run: docker build . --file Dockerfile --tag wekan:$(date +%s)

View file

@ -1,30 +0,0 @@
name: Release Charts
on:
push:
branches:
- main
permissions:
contents: read
jobs:
release:
permissions:
contents: write # for helm/chart-releaser-action to push chart release and create a release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.7.0
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View file

@ -1,163 +0,0 @@
name: Test suite
on:
push:
branches:
- main
pull_request:
permissions:
contents: read # to fetch code (actions/checkout)
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@v4
#
# - 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@v4
#
# - 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@v4
#
# - 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@v4
# CACHING
- name: Install Meteor
id: cache-meteor-install
uses: actions/cache@v4
with:
path: ~/.meteor
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
restore-keys: |
v1-meteor-
- name: Cache NPM dependencies
id: cache-meteor-npm
uses: actions/cache@v4
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@v4
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-keys: |
v1-meteor_build_cache-
- name: Setup meteor
uses: meteorengineer/setup-meteor@v2
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@v4
with:
name: coverage-folder
path: .coverage/
coverage:
name: Coverage report
runs-on: ubuntu-latest
needs: [tests]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download coverage
uses: actions/download-artifact@v4
with:
name: coverage-folder
path: .coverage/
- name: Coverage Report
uses: VeryGoodOpenSource/very_good_coverage@v3.0.0
with:
path: ".coverage/lcov.info"
min_coverage: 1 # TODO add tests and increase to 95!

28
.gitignore vendored
View file

@ -1,14 +1,15 @@
*~ *~
*.sw* *.swp
.meteor-spk .meteor-spk
*.sublime-workspace *.sublime-workspace
tmp/ tmp/
node_modules/ node_modules/
npm-debug.log npm-debug.log
.gitmodules
.vscode/ .vscode/
.idea/ .idea/
.build/* .build/*
packages/
package-lock.json
**/parts/ **/parts/
**/stage **/stage
**/prime **/prime
@ -16,26 +17,3 @@ npm-debug.log
snap/.snapcraft/ snap/.snapcraft/
.idea .idea
.DS_Store .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*/
.coverage
# Helm chart
# Chart dependencies
/helm/wekan/**/*.tgz
/helm/wekan/charts

10
.gitpod.Dockerfile vendored
View file

@ -1,10 +0,0 @@
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/

View file

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

View file

@ -16,5 +16,3 @@ notices-for-facebook-graph-api-2
1.4.1-add-shell-server-package 1.4.1-add-shell-server-package
1.4.3-split-account-service-packages 1.4.3-split-account-service-packages
1.5-add-dynamic-import-package 1.5-add-dynamic-import-package
1.7-split-underscore-from-meteor-base
1.8.3-split-jquery-from-blaze

View file

@ -3,94 +3,89 @@
# 'meteor add' and 'meteor remove' will edit this file for you, # 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand. # but you can also edit it by hand.
meteor-base@1.5.1 meteor-base@1.2.0
# Build system # Build system
ecmascript@0.16.8 ecmascript
standard-minifier-js@2.8.1 stylus@2.513.13
standard-minifier-css@1.3.5
standard-minifier-js@2.2.0
mquandalle:jade mquandalle:jade
coffeescript@2.4.1!
# Polyfills # Polyfills
es5-shim@4.8.0 es5-shim@4.6.15
# Collections # Collections
aldeed:collection2 aldeed:collection2
cfs:standard-packages
cottz:publish-relations cottz:publish-relations
dburles:collection-helpers dburles:collection-helpers
idmontie:migrations idmontie:migrations
easy:search matb33:collection-hooks
mongo@1.16.8 matteodem:easy-search
mongo@1.3.1
mquandalle:collection-mutations mquandalle:collection-mutations
# Account system # Account system
accounts-password@2.4.0 kenton:accounts-sandstorm
useraccounts:core service-configuration@1.0.11
useraccounts:flow-routing
useraccounts:unstyled useraccounts:unstyled
simple:rest-accounts-password useraccounts:flow-routing
wekan-ldap salleman:accounts-oidc
wekan-accounts-cas
wekan-accounts-sandstorm
wekan-accounts-lockout
wekan-oidc
wekan-accounts-oidc
# Utilities # Utilities
check@1.3.2 check@1.2.5
jquery@3.0.0! jquery@1.11.10
random@1.2.1 random@1.0.10
reactive-dict@1.3.1 reactive-dict@1.2.0
session@1.2.1 session@1.1.7
tracker@1.3.3 tracker@1.1.3
underscore@1.0.13 underscore@1.0.10
3stack:presence
alethes:pages
arillo:flow-router-helpers arillo:flow-router-helpers
audit-argument-checks@1.0.7 audit-argument-checks@1.0.7
kadira:blaze-layout
kadira:dochead kadira:dochead
meteorhacks:picker
meteorhacks:subs-manager
mquandalle:autofocus mquandalle:autofocus
ongoworks:speakingurl ongoworks:speakingurl
raix:handlebar-helpers raix:handlebar-helpers
http@2.0.0! # force new http package tap:i18n
http@1.3.0
# Datepicker
wekan-bootstrap-datepicker
# UI components # UI components
ostrio:i18n blaze
reactive-var@1.0.12 reactive-var@1.0.11
fortawesome:fontawesome
mousetrap:mousetrap mousetrap:mousetrap
mquandalle:jquery-textcomplete mquandalle:jquery-textcomplete
mquandalle:jquery-ui-drag-drop-sort
mquandalle:mousetrap-bindglobal mquandalle:mousetrap-bindglobal
mquandalle:perfect-scrollbar
peerlibrary:blaze-components@=0.15.1
perak:markdown
templates:tabs templates:tabs
meteor-autosize verron:autosize
shell-server@0.5.0
email@2.2.5
dynamic-import@0.7.3
msavin:usercache
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
meteorhacks:subs-manager
meteorhacks:aggregate@1.3.0
wekan-markdown
konecty:mongo-counter
percolate:synced-cron
ostrio:cookies
ostrio:files@2.3.0
pascoual:pdfkit
lmieulet:meteor-coverage
meteortesting:mocha@2.0.3
aldeed:simple-schema
matb33:collection-hooks
simple:json-routes simple:json-routes
kadira:flow-router rajit:bootstrap3-datepicker
spacebars shell-server@0.3.0
service-configuration@1.3.2 simple:rest-accounts-password
communitypackages:picker useraccounts:core
minifier-css@1.6.4 email@1.2.3
blaze horka:swipebox
kadira:blaze-layout dynamic-import@0.2.0
peerlibrary:blaze-components staringatlights:fast-render
ejson@1.1.3
logging@1.3.3 mixmax:smart-disconnect
wekan-fullcalendar accounts-password@1.5.0
momentjs:moment@2.29.3 cfs:gridfs
wekan-fontawesome eluck:accounts-lockout
rzymek:fullcalendar
momentjs:moment@2.22.2
browser-policy-framing
mquandalle:moment
msavin:usercache
wekan:wekan-ldap
wekan:accounts-cas

View file

@ -1 +1 @@
METEOR@2.14 METEOR@1.6.0.1

View file

@ -1,90 +1,111 @@
accounts-base@2.2.10 3stack:presence@1.1.2
accounts-oauth@1.4.3 accounts-base@1.4.0
accounts-password@2.4.0 accounts-oauth@1.1.15
accounts-password@1.5.0
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
aldeed:schema-index@1.1.1 aldeed:schema-index@1.1.1
aldeed:simple-schema@1.5.4 aldeed:simple-schema@1.5.3
allow-deny@1.1.1 alethes:pages@1.8.6
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.8.0 autoupdate@1.3.12
babel-compiler@7.10.5 babel-compiler@6.24.7
babel-runtime@1.5.1 babel-runtime@1.1.1
base64@1.0.12 base64@1.0.10
binary-heap@1.0.11 binary-heap@1.0.10
blaze@2.7.1 blaze@2.3.2
blaze-tools@1.1.3 blaze-tools@1.0.10
boilerplate-generator@1.7.2 boilerplate-generator@1.3.1
caching-compiler@1.2.2 browser-policy-common@1.0.11
caching-html-compiler@1.2.1 browser-policy-framing@1.1.0
callback-hook@1.5.1 caching-compiler@1.1.9
check@1.3.2 caching-html-compiler@1.1.2
coffeescript@2.7.0 callback-hook@1.0.10
coffeescript-compiler@2.4.1 cfs:access-point@0.1.49
communitypackages:picker@1.1.1 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: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.9
cfs:storage-adapter@0.2.3
cfs:tempstore@0.1.5
cfs:upload-http@0.0.20
cfs:worker@0.1.4
check@1.2.5
chuangbo:cookie@1.1.0
coffeescript@1.12.7_3
coffeescript-compiler@1.12.7_3
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.1 ddp@1.4.0
ddp-client@2.6.1 ddp-client@2.2.0
ddp-common@1.4.0 ddp-common@1.3.0
ddp-rate-limiter@1.2.1 ddp-rate-limiter@1.0.7
ddp-server@2.7.0 ddp-server@2.1.1
deps@1.0.12 deps@1.0.12
diff-sequence@1.1.2 diff-sequence@1.0.7
dynamic-import@0.7.3 dynamic-import@0.2.1
easy:search@2.2.1 ecmascript@0.9.0
easysearch:components@2.2.2 ecmascript-runtime@0.5.0
easysearch:core@2.2.2 ecmascript-runtime-client@0.5.0
ecmascript@0.16.8 ecmascript-runtime-server@0.5.0
ecmascript-runtime@0.8.1 ejson@1.1.0
ecmascript-runtime-client@0.12.1 eluck:accounts-lockout@0.9.0
ecmascript-runtime-server@0.11.0 email@1.2.3
ejson@1.1.3 es5-shim@4.6.15
email@2.2.5 fastclick@1.0.13
es5-shim@4.8.0 fortawesome:fontawesome@4.7.0
fetch@0.1.4 geojson-utils@1.0.10
geojson-utils@1.0.11 horka:swipebox@1.0.2
hot-code-push@1.0.4 hot-code-push@1.0.4
html-tools@1.1.3 html-tools@1.0.11
htmljs@1.1.1 htmljs@1.0.11
http@2.0.0 http@1.3.0
id-map@1.1.1 id-map@1.0.9
idmontie:migrations@1.0.3 idmontie:migrations@1.0.3
inter-process-messaging@0.1.1 jquery@1.11.10
jquery@3.0.0
kadira:blaze-layout@2.3.0 kadira:blaze-layout@2.3.0
kadira:dochead@1.5.0 kadira:dochead@1.5.0
kadira:flow-router@2.12.1 kadira:flow-router@2.12.1
konecty:mongo-counter@0.0.5_3 kenton:accounts-sandstorm@0.7.0
lmieulet:meteor-coverage@1.1.4 launch-screen@1.1.1
livedata@1.0.18
localstorage@1.2.0 localstorage@1.2.0
logging@1.3.3 logging@1.1.19
matb33:collection-hooks@1.3.0 matb33:collection-hooks@0.8.4
matteodem:easy-search@1.6.4
mdg:validation-error@0.5.1 mdg:validation-error@0.5.1
meteor@1.11.5 meteor@1.8.2
meteor-autosize@5.0.1 meteor-base@1.2.0
meteor-base@1.5.1 meteor-platform@1.2.6
meteorhacks:aggregate@1.3.0 meteorhacks:aggregate@1.3.0
meteorhacks:collection-utils@1.2.0 meteorhacks:collection-utils@1.2.0
meteorhacks:meteorx@1.4.1
meteorhacks:picker@1.0.3 meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4 meteorhacks:subs-manager@1.6.4
meteortesting:browser-tests@1.4.2 meteorspark:util@0.2.0
meteortesting:mocha@2.1.0 minifier-css@1.2.16
meteortesting:mocha-core@8.0.1 minifier-js@2.2.2
minifier-css@1.6.4
minifier-js@2.7.5
minifiers@1.1.8-faster-rebuild.0 minifiers@1.1.8-faster-rebuild.0
minimongo@1.9.3 minimongo@1.4.3
modern-browsers@0.1.10 mixmax:smart-disconnect@0.0.4
modules@0.20.0 mobile-status-bar@1.0.14
modules-runtime@0.13.1 modules@0.11.0
momentjs:moment@2.29.3 modules-runtime@0.9.1
mongo@1.16.8 momentjs:moment@2.22.2
mongo-decimal@0.1.3 mongo@1.3.1
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.8 mongo-id@1.0.6
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
@ -92,75 +113,73 @@ mquandalle:collection-mutations@0.1.0
mquandalle:jade@0.4.9 mquandalle:jade@0.4.9
mquandalle:jade-compiler@0.4.5 mquandalle:jade-compiler@0.4.5
mquandalle:jquery-textcomplete@0.8.0_1 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:mousetrap-bindglobal@0.0.1
msavin:usercache@1.8.0 mquandalle:perfect-scrollbar@0.6.5_2
npm-mongo@4.17.2 msavin:usercache@1.0.0
oauth@2.2.1 npm-bcrypt@0.9.3
oauth2@1.3.2 npm-mongo@2.2.33
observe-sequence@1.0.21 oauth@1.2.1
oauth2@1.2.0
observe-sequence@1.0.16
ongoworks:speakingurl@1.1.0 ongoworks:speakingurl@1.1.0
ordered-dict@1.1.0 ordered-dict@1.0.9
ostrio:cookies@2.7.2 peerlibrary:assert@0.2.5
ostrio:cstorage@4.0.1 peerlibrary:base-component@0.16.0
ostrio:files@2.3.3 peerlibrary:blaze-components@0.15.1
ostrio:i18n@3.2.1 peerlibrary:computed-field@0.7.0
pascoual:pdfkit@1.0.7 peerlibrary:reactive-field@0.3.0
peerlibrary:assert@0.3.0 perak:markdown@1.0.5
peerlibrary:base-component@0.17.1 promise@0.10.0
peerlibrary:blaze-components@0.23.0
peerlibrary:computed-field@0.10.0
peerlibrary:data-lookup@0.3.0
peerlibrary:reactive-field@0.6.0
percolate:synced-cron@1.5.2
promise@0.12.2
raix:eventemitter@0.1.3 raix:eventemitter@0.1.3
raix:handlebar-helpers@0.2.5 raix:handlebar-helpers@0.2.5
random@1.2.1 rajit:bootstrap3-datepicker@1.7.1
rate-limit@1.1.1 random@1.0.10
react-fast-refresh@0.2.8 rate-limit@1.0.8
reactive-dict@1.3.1 reactive-dict@1.2.0
reactive-var@1.0.12 reactive-var@1.0.11
reload@1.3.1 reload@1.1.11
retry@1.1.0 retry@1.0.9
routepolicy@1.1.1 routepolicy@1.0.12
service-configuration@1.3.3 rzymek:fullcalendar@3.8.0
session@1.2.1 salleman:accounts-oidc@1.0.9
salleman:oidc@1.0.9
service-configuration@1.0.11
session@1.1.7
sha@1.0.9 sha@1.0.9
shell-server@0.5.0 shell-server@0.3.1
simple:authenticate-user-by-token@1.2.1 simple:authenticate-user-by-token@1.0.1
simple:json-routes@2.3.1 simple:json-routes@2.1.0
simple:rest-accounts-password@1.2.2 simple:rest-accounts-password@1.1.2
simple:rest-bearer-token-parser@1.1.1 simple:rest-bearer-token-parser@1.0.1
simple:rest-json-error-handler@1.1.1 simple:rest-json-error-handler@1.0.1
socket-stream-client@0.5.2 softwarerero:accounts-t9n@1.3.11
spacebars@1.4.1 spacebars@1.0.15
spacebars-compiler@1.3.1 spacebars-compiler@1.1.3
standard-minifier-js@2.8.1 srp@1.0.10
standard-minifier-css@1.3.5
standard-minifier-js@2.2.3
staringatlights:fast-render@2.16.5
staringatlights:inject-data@2.0.5
stylus@2.513.13
tap:i18n@1.8.2
templates:tabs@2.3.0 templates:tabs@2.3.0
templating@1.4.1 templating@1.3.2
templating-compiler@1.4.1 templating-compiler@1.3.3
templating-runtime@1.5.0 templating-runtime@1.3.2
templating-tools@1.2.2 templating-tools@1.1.2
tracker@1.3.3 tracker@1.1.3
typescript@4.9.5
ui@1.0.13 ui@1.0.13
underscore@1.0.13 underscore@1.0.10
url@1.3.2 url@1.1.0
useraccounts:core@1.16.2 useraccounts:core@1.14.2
useraccounts:flow-routing@1.15.0 useraccounts:flow-routing@1.14.2
useraccounts:unstyled@1.14.2 useraccounts:unstyled@1.14.2
webapp@1.13.6 verron:autosize@3.0.8
webapp-hashing@1.1.1 webapp@1.4.0
wekan-accounts-cas@0.1.0 webapp-hashing@1.0.9
wekan-accounts-lockout@1.0.0 wekan:accounts-cas@0.1.0
wekan-accounts-oidc@1.0.10 wekan:wekan-ldap@0.0.2
wekan-accounts-sandstorm@0.8.0
wekan-bootstrap-datepicker@1.10.0
wekan-fontawesome@6.4.2
wekan-fullcalendar@3.10.5
wekan-ldap@0.0.2
wekan-markdown@1.0.9
wekan-oidc@1.0.12
yasaricli:slugify@0.0.7 yasaricli:slugify@0.0.7
zimme:active-route@2.3.2 zimme:active-route@2.3.2
zodern:types@1.0.10

View file

@ -1,13 +0,0 @@
dependencies:
- libreadline-dev
- libssl-dev
- bsdtar
targets:
ubuntu-18.04:
ubuntu-16.04:
ubuntu-14.04:
centos-6:
centos-7:
debian-10:
sles-12:
sles-11:

View file

@ -1,8 +0,0 @@
packages/
node_modules/
.build/
.meteor/
.vscode/
.tx/
.github/
.snap-meteor-1.8/

View file

@ -1,8 +0,0 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}

View file

@ -1,10 +1,10 @@
dist: focal dist: trusty
sudo: required sudo: required
env: env:
TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0 TRAVIS_DOCKER_COMPOSE_VERSION: 1.17.0
TRAVIS_NODE_VERSION: 14.21.3 TRAVIS_NODE_VERSION: 8.9.3
TRAVIS_NPM_VERSION: latest TRAVIS_NPM_VERSION: 5.5.1
before_install: before_install:
- sudo apt-get update -y - sudo apt-get update -y

View file

@ -1,9 +1,55 @@
[main] # This is the configuration of the Transifex tool that we use to manage the
host = https://www.transifex.com # translations on Wekan. Documentation at: http://docs.transifex.com/client.
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, sl_SI: sl, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB #
# Push
# ====
#
# It is recommended that contributors use the Transifex web UI to create and
# edit translated strings. However in case a contributor has directly jumped
# into the code and made its translations in the corresponding i18n.json file
# we can push it using
#
# > tx push -t -l ar
#
# Where `ar` is the language identifier. In addition, the project maintainer
# should push the English source file to Transifex at least before each release
# candidate using:
#
# > tx push -s
#
# Pull
# ====
#
# The set of accepted language is directly managed in Transifex, the only
# restriction we define to bundle a new language in the application, is that its
# completion is at least at 75%.
#
# We use:
#
# > tx pull
#
# to download new versions of existing translations, and
#
# > tx pull -a --minimum-perc=75
#
# to download new sufficiently advanced translations.
[o:wekan:p:wekan:r:application] [main]
file_filter = imports/i18n/data/<lang>.i18n.json host = https://www.transifex.com
source_file = imports/i18n/data/en.i18n.json # tap:i18n requires us to use `-` separator in the language identifiers whereas
# Transifex uses a `_` separator, without an option to customize it on one side
# or the other, so we need to do a Manual mapping.
lang_map = bg_BG:bg, en_GB:en-GB, es_AR:es-AR, el_GR:el, fi_FI:fi, hu_HU:hu, id_ID:id, mn_MN:mn, no:nb, lv_LV:lv, pt_BR:pt-BR, ro_RO:ro, zh_CN:zh-CN, zh_TW:zh-TW
[wekan.application]
file_filter = i18n/<lang>.i18n.json
source_lang = en source_lang = en
type = KEYVALUEJSON type = KEYVALUEJSON
# We might have a dedicated second resource later to translate the “Welcome
# Board” data.
#
# [wekan.welcomeBoard]
# file_filter = private/welcomeBoard/<lang>.json
# source_lang = en
# type = KEYVALUEJSON

57
.vscode/launch.json vendored
View file

@ -1,57 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Meteor: Node",
"runtimeExecutable": "meteor",
"runtimeArgs": [
"--port=4000",
"--exclude-archs=web.browser.legacy,web.cordova",
"--raw-logs"
],
"env": {
"WRITABLE_PATH": "/tmp/uploads",
},
"outputCapture": "std",
"restart": true,
"timeout": 60000
},
{
"type": "chrome",
"request": "launch",
"name": "Meteor: Chrome",
"url": "http://localhost:4000",
"sourceMapPathOverrides": {
"meteor://💻app/*": "${workspaceFolder}/*"
},
"userDataDir": "${env:HOME}/.vscode/chrome"
},
{
"type": "node",
"request": "launch",
"name": "Test: Node",
"runtimeExecutable": "meteor",
"runtimeArgs": [
"test",
"--port=4040",
"--exclude-archs=web.browser.legacy,web.cordova",
"--driver-package=meteortesting:mocha",
"--settings=settings.json",
"--raw-logs"
],
"env": {
"TEST_WATCH": "1"
},
"outputCapture": "std",
"timeout": 60000
}
],
"compounds": [
{
"name": "Meteor: All",
"configurations": ["Meteor: Node", "Meteor: Chrome"]
}
]
}

10942
CHANGELOG.md

File diff suppressed because it is too large Load diff

View file

@ -1,22 +0,0 @@
# Code of Conduct
For all code at WeKan GitHub Organization https://github.com/wekan
- All code in pull requests need to have permission already to add it to WeKan with MIT license, and will become MIT license.
- All code xet7 add is MIT license.
- For any dependencies, permissive licenses like https://copyfree.org are preferred
- For anything currently that is non-permissive (like GPL, AGPL, SSPL), those will be replaced with permissive-licensed alternatives
# Reporting about violations or something else
## Private reports
- Email support@wekan.team
- Security issues: [SECURITY.md](SECURITY.md)
- License violations
- Anything private, sensitive or negative
## Public
- Feature Requests and Bug Reports https://github.com/wekan/wekan/issues
- Anything happy, positive, encouraging, helping, at friendly WeKan Global FOSS Community

View file

@ -1,87 +1,4 @@
## About money To get started, [please sign the Contributor License Agreement](https://www.clahub.com/agreements/wekan/wekan).
Not paid: [Then, please read documentation at wiki](https://github.com/wekan/wekan/wiki).
- Money is not paid for these, everyone uses their own time at their own cost:
- Security reports, see [SECURITY.md](SECURITY.md)
- Pull requests
- xet7 checking pull requests
- Public Community Support
- https://github.com/wekan/wekan/issues
Paid by customers of WeKan Team:
- Commercial Support at https://wekan.team/commercial-support/
- Support
- Private Chat
- Features
- Fixes
- Hosting
## Contributing Security related
For responsible security disclosure, please follow this process:
https://github.com/wekan/wekan/blob/main/SECURITY.md
CVE Hall of Fame is at https://wekan.github.io/hall-of-fame/
## Contributing to Documentation Wiki
Fork WeKan repo https://github.com/wekan/wekan ,
edit `docs` directory content at GitHub web interface,
and click send PR.
## Contributing code
[Building WeKan and sending PR](https://github.com/wekan/wekan/wiki/Emoji).
WeKan code contributors Hall of Fame is at ChangeLog, where
GitHub usernames are mentioned with changes added:
https://github.com/wekan/wekan/blob/main/CHANGELOG.md
Changes can be like typo fixes, bugfixes, features, or anything else
like for example at open GitHub issues https://github.com/wekan/wekan/issues .
Closed issues are already fixed or implemented.
Also see other docs at wiki, for example:
https://github.com/wekan/wekan/wiki/Developer-Documentation
Do not use code formatting or linting like eslist or prettier.
Only send minimal changed code lines, that are related to feature or fix.
WeKan code has MIT license.
About 300 persons have contributed to WeKan, stats at:
https://www.openhub.net/p/wekan
WeKan maintainer xet7 reviews PR for typos etc before accepting to WeKan,
so that WeKan code will still work OK.
## Contributing translations
Non-English translations are contributed only at
https://transifex.com/wekan/wekan
When adding new features, in your PR to
https://github.com/wekan/wekan/pulls
only add new English source language strings
to https://github.com/wekan/wekan/blob/main/imports/i18n/data/en.i18n.json
Maintainer of WeKan xet7 downloads all newest
translations from Transifex and adds
them to WeKan repo before making
new release.
## About WeKan Organization https://github.com/wekan
Only xet7 has write access to WeKan Organization.
xet7 reviews all PRs before merging.
There has been over 300 contributors to WeKan, newest stats at:
https://www.openhub.net/p/wekan

View file

@ -1,87 +1,100 @@
FROM ubuntu:24.04 FROM debian:buster-slim
LABEL maintainer="wekan" LABEL maintainer="wekan"
LABEL org.opencontainers.image.ref.name="ubuntu"
LABEL org.opencontainers.image.version="24.04"
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# 2022-04-25: # Declare Arguments
# - gyp does not yet work with Ubuntu 22.04 ubuntu:rolling, ARG NODE_VERSION
# so changing to 21.10. https://github.com/wekan/wekan/issues/4488 ARG METEOR_RELEASE
ARG METEOR_EDGE
ARG USE_EDGE
ARG NPM_VERSION
ARG FIBERS_VERSION
ARG ARCHITECTURE
ARG SRC_PATH
ARG WITH_API
ARG MATOMO_ADDRESS
ARG MATOMO_SITE_ID
ARG MATOMO_DO_NOT_TRACK
ARG MATOMO_WITH_USERNAME
ARG BROWSER_POLICY_ENABLED
ARG TRUSTED_URL
ARG WEBHOOKS_ATTRIBUTES
ARG OAUTH2_ENABLED
ARG OAUTH2_CLIENT_ID
ARG OAUTH2_SECRET
ARG OAUTH2_SERVER_URL
ARG OAUTH2_AUTH_ENDPOINT
ARG OAUTH2_USERINFO_ENDPOINT
ARG OAUTH2_TOKEN_ENDPOINT
ARG LDAP_ENABLE
ARG LDAP_PORT
ARG LDAP_HOST
ARG LDAP_BASEDN
ARG LDAP_LOGIN_FALLBACK
ARG LDAP_RECONNECT
ARG LDAP_TIMEOUT
ARG LDAP_IDLE_TIMEOUT
ARG LDAP_CONNECT_TIMEOUT
ARG LDAP_AUTHENTIFICATION
ARG LDAP_AUTHENTIFICATION_USERDN
ARG LDAP_AUTHENTIFICATION_PASSWORD
ARG LDAP_LOG_ENABLED
ARG LDAP_BACKGROUND_SYNC
ARG LDAP_BACKGROUND_SYNC_INTERVAL
ARG LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED
ARG LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS
ARG LDAP_ENCRYPTION
ARG LDAP_CA_CERT
ARG LDAP_REJECT_UNAUTHORIZED
ARG LDAP_USER_SEARCH_FILTER
ARG LDAP_USER_SEARCH_SCOPE
ARG LDAP_USER_SEARCH_FIELD
ARG LDAP_SEARCH_PAGE_SIZE
ARG LDAP_SEARCH_SIZE_LIMIT
ARG LDAP_GROUP_FILTER_ENABLE
ARG LDAP_GROUP_FILTER_OBJECTCLASS
ARG LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE
ARG LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE
ARG LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT
ARG LDAP_GROUP_FILTER_GROUP_NAME
ARG LDAP_UNIQUE_IDENTIFIER_FIELD
ARG LDAP_UTF8_NAMES_SLUGIFY
ARG LDAP_USERNAME_FIELD
ARG LDAP_MERGE_EXISTING_USERS
ARG LDAP_SYNC_USER_DATA
ARG LDAP_SYNC_USER_DATA_FIELDMAP
ARG LDAP_SYNC_GROUP_ROLES
ARG LDAP_DEFAULT_DOMAIN
# 2021-09-18: # Set the environment variables (defaults where required)
# - Above Ubuntu base image copied from Docker Hub ubuntu:hirsute-20210825 # DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
# to Quay to avoid Docker Hub rate limits. # ENV BUILD_DEPS="paxctl"
ARG DEBIAN_FRONTEND=noninteractive ENV BUILD_DEPS="apt-utils bsdtar gnupg gosu wget curl bzip2 build-essential python git ca-certificates gcc-7" \
NODE_VERSION=v8.12.0 \
ENV BUILD_DEPS="apt-utils gnupg gosu wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3" METEOR_RELEASE=1.6.0.1 \
ENV \
DEBUG=false \
NODE_VERSION=v14.21.4 \
METEOR_RELEASE=METEOR@2.14 \
USE_EDGE=false \ USE_EDGE=false \
METEOR_EDGE=1.5-beta.17 \ METEOR_EDGE=1.5-beta.17 \
NPM_VERSION=6.14.17 \ NPM_VERSION=latest \
FIBERS_VERSION=4.0.1 \ FIBERS_VERSION=2.0.0 \
ARCHITECTURE=linux-x64 \ ARCHITECTURE=linux-x64 \
SRC_PATH=./ \ SRC_PATH=./ \
WITH_API=true \ WITH_API=true \
RESULTS_PER_PAGE="" \
DEFAULT_BOARD_ID="" \
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD=60 \
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW=15 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS=90 \
ATTACHMENTS_UPLOAD_EXTERNAL_PROGRAM="" \
ATTACHMENTS_UPLOAD_MIME_TYPES="" \
ATTACHMENTS_UPLOAD_MAX_SIZE=0 \
AVATARS_UPLOAD_EXTERNAL_PROGRAM="" \
AVATARS_UPLOAD_MIME_TYPES="" \
AVATARS_UPLOAD_MAX_SIZE=72000 \
RICHER_CARD_COMMENT_EDITOR=false \
CARD_OPENED_WEBHOOK_ENABLED=false \
MAX_IMAGE_PIXEL="" \
IMAGE_COMPRESS_RATIO="" \
NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
BIGEVENTS_PATTERN=NONE \
NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
NOTIFY_DUE_AT_HOUR_OF_DAY="" \
EMAIL_NOTIFICATION_TIMEOUT=30000 \
MATOMO_ADDRESS="" \ MATOMO_ADDRESS="" \
MATOMO_SITE_ID="" \ MATOMO_SITE_ID="" \
MATOMO_DO_NOT_TRACK=true \ MATOMO_DO_NOT_TRACK=true \
MATOMO_WITH_USERNAME=false \ MATOMO_WITH_USERNAME=false \
METRICS_ALLOWED_IP_ADDRESSES="" \
BROWSER_POLICY_ENABLED=true \ BROWSER_POLICY_ENABLED=true \
TRUSTED_URL="" \ TRUSTED_URL="" \
WEBHOOKS_ATTRIBUTES="" \ WEBHOOKS_ATTRIBUTES="" \
OAUTH2_ENABLED=false \ OAUTH2_ENABLED=false \
OIDC_REDIRECTION_ENABLED=false \
OAUTH2_CA_CERT="" \
OAUTH2_ADFS_ENABLED=false \
OAUTH2_B2C_ENABLED=false \
OAUTH2_LOGIN_STYLE=redirect \
OAUTH2_CLIENT_ID="" \ OAUTH2_CLIENT_ID="" \
OAUTH2_SECRET="" \ OAUTH2_SECRET="" \
OAUTH2_SERVER_URL="" \ OAUTH2_SERVER_URL="" \
OAUTH2_AUTH_ENDPOINT="" \ OAUTH2_AUTH_ENDPOINT="" \
OAUTH2_USERINFO_ENDPOINT="" \ OAUTH2_USERINFO_ENDPOINT="" \
OAUTH2_TOKEN_ENDPOINT="" \ OAUTH2_TOKEN_ENDPOINT="" \
OAUTH2_ID_MAP="" \
OAUTH2_USERNAME_MAP="" \
OAUTH2_FULLNAME_MAP="" \
OAUTH2_ID_TOKEN_WHITELIST_FIELDS="" \
OAUTH2_REQUEST_PERMISSIONS='openid profile email' \
OAUTH2_EMAIL_MAP="" \
LDAP_ENABLE=false \ LDAP_ENABLE=false \
LDAP_PORT=389 \ LDAP_PORT=389 \
LDAP_HOST="" \ LDAP_HOST="" \
LDAP_AD_SIMPLE_AUTH="" \
LDAP_USER_AUTHENTICATION=false \
LDAP_USER_AUTHENTICATION_FIELD=uid \
LDAP_BASEDN="" \ LDAP_BASEDN="" \
LDAP_LOGIN_FALLBACK=false \ LDAP_LOGIN_FALLBACK=false \
LDAP_RECONNECT=true \ LDAP_RECONNECT=true \
@ -93,7 +106,7 @@ ENV \
LDAP_AUTHENTIFICATION_PASSWORD="" \ LDAP_AUTHENTIFICATION_PASSWORD="" \
LDAP_LOG_ENABLED=false \ LDAP_LOG_ENABLED=false \
LDAP_BACKGROUND_SYNC=false \ LDAP_BACKGROUND_SYNC=false \
LDAP_BACKGROUND_SYNC_INTERVAL="" \ LDAP_BACKGROUND_SYNC_INTERVAL=100 \
LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false \ LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED=false \
LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false \ LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS=false \
LDAP_ENCRYPTION=false \ LDAP_ENCRYPTION=false \
@ -113,164 +126,148 @@ ENV \
LDAP_UNIQUE_IDENTIFIER_FIELD="" \ LDAP_UNIQUE_IDENTIFIER_FIELD="" \
LDAP_UTF8_NAMES_SLUGIFY=true \ LDAP_UTF8_NAMES_SLUGIFY=true \
LDAP_USERNAME_FIELD="" \ LDAP_USERNAME_FIELD="" \
LDAP_FULLNAME_FIELD="" \
LDAP_MERGE_EXISTING_USERS=false \ LDAP_MERGE_EXISTING_USERS=false \
LDAP_EMAIL_FIELD="" \
LDAP_EMAIL_MATCH_ENABLE=false \
LDAP_EMAIL_MATCH_REQUIRE=false \
LDAP_EMAIL_MATCH_VERIFIED=false \
LDAP_SYNC_USER_DATA=false \ LDAP_SYNC_USER_DATA=false \
LDAP_SYNC_USER_DATA_FIELDMAP="" \ LDAP_SYNC_USER_DATA_FIELDMAP="" \
LDAP_SYNC_GROUP_ROLES="" \ LDAP_SYNC_GROUP_ROLES="" \
LDAP_DEFAULT_DOMAIN="" \ LDAP_DEFAULT_DOMAIN=""
LDAP_SYNC_ADMIN_STATUS="" \
LDAP_SYNC_ADMIN_GROUPS="" \
HEADER_LOGIN_ID="" \
HEADER_LOGIN_FIRSTNAME="" \
HEADER_LOGIN_LASTNAME="" \
HEADER_LOGIN_EMAIL="" \
LOGOUT_WITH_TIMER=false \
LOGOUT_IN="" \
LOGOUT_ON_HOURS="" \
LOGOUT_ON_MINUTES="" \
CORS="" \
CORS_ALLOW_HEADERS="" \
CORS_EXPOSE_HEADERS="" \
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="" \
ORACLE_OIM_ENABLED=false \
WAIT_SPINNER="" \
WRITABLE_PATH=/data \
S3=""
# NODE_OPTIONS="--max_old_space_size=4096"
#---------------------------------------------
# == at docker-compose.yml: AUTOLOGIN WITH OIDC/OAUTH2 ====
# https://github.com/wekan/wekan/wiki/autologin
#- OIDC_REDIRECTION_ENABLED=true
#---------------------------------------------------------------------
# Copy the app to the image # Copy the app to the image
COPY ${SRC_PATH} /home/wekan/app COPY ${SRC_PATH} /home/wekan/app
# Install OS RUN \
RUN <<EOR set -o xtrace && \
set -o xtrace # Add non-root user wekan
useradd --user-group --system --home-dir /home/wekan wekan && \
# Add non-root user wekan \
useradd --user-group --system --home-dir /home/wekan wekan # OS dependencies
# OS dependencies apt-get update -y && apt-get install -y --no-install-recommends ${BUILD_DEPS} && \
apt-get update --assume-yes \
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS} # Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing. cp $(which tar) $(which tar)~ && \
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389 ln -sf $(which bsdtar) $(which tar) && \
cp $(which tar) $(which tar)~ \
ln -sf $(which bsdtar) $(which tar) # Download nodejs
wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
# Install NodeJS wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
cd /tmp #---------------------------------------------------------------------------------------------
# Node Fibers 100% CPU usage issue:
# Download nodejs # https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz" # https://github.com/meteor/meteor/issues/9796#issuecomment-381676326
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt" # https://github.com/sandstorm-io/sandstorm/blob/0f1fec013fe7208ed0fd97eb88b31b77e3c61f42/shell/server/00-startup.js#L99-L129
# Also see beginning of wekan/server/authentication.js
# Verify nodejs authenticity # import Fiber from "fibers";
grep "node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c - # Fiber.poolSize = 1e9;
rm -f "SHASUMS256.txt" # OLD: Download node version 8.12.0 prerelease that has fix included, => Official 8.12.0 has been released
# Description at https://releases.wekan.team/node.txt
# Install Node #wget https://releases.wekan.team/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
tar xzf "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" -C /usr/local --strip-components=1 --no-same-owner #echo "1ed54adb8497ad8967075a0b5d03dd5d0a502be43d4a4d84e5af489c613d7795 node-v8.12.0-linux-x64.tar.gz" >> SHASUMS256.txt.asc && \
rm "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" "SHASUMS256.txt" \
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs" # Verify nodejs authenticity
mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/${NODE_VERSION} /home/wekan/.config" grep ${NODE_VERSION}-${ARCHITECTURE}.tar.gz SHASUMS256.txt.asc | shasum -a 256 -c - && \
#export GNUPGHOME="$(mktemp -d)" && \
# Install node dependencies #\
npm install -g npm@${NPM_VERSION} --production # Try other key servers if ha.pool.sks-keyservers.net is unreachable
chown --recursive wekan:wekan /home/wekan/.config # Code from https://github.com/chorrell/docker-node/commit/2b673e17547c34f17f24553db02beefbac98d23c
# gpg keys listed at https://github.com/nodejs/node#release-team
# Install Meteor # and keys listed here from previous version of this Dockerfile
cd /home/wekan #for key in \
chown --recursive wekan:wekan /home/wekan #9554F04D7259F04124DE6B476D5A82AC7E37093B \
echo "Starting meteor ${METEOR_RELEASE} installation... \n" #94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
gosu wekan:wekan curl https://install.meteor.com/ | /bin/sh #FD3A5288F042B6850C66B31F09FE44734EB7990E \
mv /root/.meteor /home/wekan/ #71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
chown --recursive wekan:wekan /home/wekan/.meteor #DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
#C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js #B9AE9905FFD7803F25714661B63B535A4C206CA9 \
cd /home/wekan/.meteor #; do \
gosu wekan:wekan /home/wekan/.meteor/meteor -- help #gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" || \
#gpg --keyserver pgp.mit.edu --recv-keys "$key" || \
# Build app (Production) #gpg --keyserver keyserver.pgp.com --recv-keys "$key" ; \
cd /home/wekan/app #done && \
mkdir -p /home/wekan/.npm #gpg --verify SHASUMS256.txt.asc && \
chown --recursive wekan:wekan /home/wekan/.npm # Ignore socket files then delete files then delete directories
chmod u+w *.json #find "$GNUPGHOME" -type f | xargs rm -f && \
gosu wekan:wekan meteor npm install --production #find "$GNUPGHOME" -type d | xargs rm -fR && \
gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build rm -f SHASUMS256.txt.asc && \
cd /home/wekan/app_build/bundle/programs/server/ \
chmod u+w *.json # Install Node
gosu wekan:wekan meteor npm install --production tar xvzf node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
cd node_modules/fibers rm node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz && \
node build.js mv node-${NODE_VERSION}-${ARCHITECTURE} /opt/nodejs && \
cd ../.. ln -s /opt/nodejs/bin/node /usr/bin/node && \
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc. ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy \
mv /home/wekan/app_build/bundle /build #DOES NOT WORK: paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303
#paxctl -mC `which node` && \
# Put back the original tar \
mv $(which tar)~ $(which tar) # Install Node dependencies
npm install -g npm@${NPM_VERSION} && \
# Cleanup npm install -g node-gyp && \
apt-get remove --purge --assume-yes ${BUILD_DEPS} npm install -g fibers@${FIBERS_VERSION} && \
npm uninstall -g api2html \
apt-get autoremove --assume-yes # Change user to wekan and install meteor
apt-get clean --assume-yes cd /home/wekan/ && \
rm -Rf /tmp/* chown wekan:wekan --recursive /home/wekan && \
rm -Rf /var/lib/apt/lists/* curl "https://install.meteor.com" -o /home/wekan/install_meteor.sh && \
rm -Rf /var/cache/apt #curl "https://install.meteor.com/?release=${METEOR_RELEASE}" -o /home/wekan/install_meteor.sh && \
rm -Rf /var/lib/apt/lists # OLD: sed -i "s|RELEASE=.*|RELEASE=${METEOR_RELEASE}\"\"|g" ./install_meteor.sh && \
rm -Rf /home/wekan/app_build # Install Meteor forcing its progress
rm -Rf /home/wekan/app sed -i 's/VERBOSITY="--silent"/VERBOSITY="--progress-bar"/' ./install_meteor.sh && \
rm -Rf /home/wekan/.meteor echo "Starting meteor ${METEOR_RELEASE} installation... \n" && \
chown wekan:wekan /home/wekan/install_meteor.sh && \
mkdir /data \
chown wekan --recursive /data # Check if opting for a release candidate instead of major release
EOR if [ "$USE_EDGE" = false ]; then \
gosu wekan:wekan sh /home/wekan/install_meteor.sh; \
USER wekan else \
gosu wekan:wekan git clone --recursive --depth 1 -b release/METEOR@${METEOR_EDGE} git://github.com/meteor/meteor.git /home/wekan/.meteor; \
fi; \
\
# Get additional packages
mkdir -p /home/wekan/app/packages && \
chown wekan:wekan --recursive /home/wekan && \
cd /home/wekan/app/packages && \
gosu wekan:wekan git clone --depth 1 -b master git://github.com/wekan/flow-router.git kadira-flow-router && \
gosu wekan:wekan git clone --depth 1 -b master git://github.com/meteor-useraccounts/core.git meteor-useraccounts-core && \
gosu wekan:wekan git clone --depth 1 -b master git://github.com/wekan/meteor-accounts-cas.git && \
gosu wekan:wekan git clone --depth 1 -b master git://github.com/wekan/wekan-ldap.git && \
sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js && \
cd /home/wekan/.meteor && \
gosu wekan:wekan /home/wekan/.meteor/meteor -- help; \
\
# Build app
cd /home/wekan/app && \
gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js && \
gosu wekan:wekan /home/wekan/.meteor/meteor npm install && \
gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build && \
cp /home/wekan/app/fix-download-unicode/cfs_access-point.txt /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
chown wekan:wekan /home/wekan/app_build/bundle/programs/server/packages/cfs_access-point.js && \
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
#cd /home/wekan/app_build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt && \
#gosu wekan:wekan rm -rf node_modules/bcrypt && \
#gosu wekan:wekan npm install bcrypt && \
cd /home/wekan/app_build/bundle/programs/server/ && \
gosu wekan:wekan npm install && \
#gosu wekan:wekan npm install bcrypt && \
mv /home/wekan/app_build/bundle /build && \
\
# Put back the original tar
mv $(which tar)~ $(which tar) && \
\
# Cleanup
apt-get remove --purge -y ${BUILD_DEPS} && \
apt-get autoremove -y && \
rm -R /var/lib/apt/lists/* && \
rm -R /home/wekan/.meteor && \
rm -R /home/wekan/app && \
rm -R /home/wekan/app_build && \
rm /home/wekan/install_meteor.sh
ENV PORT=8080 ENV PORT=8080
EXPOSE $PORT EXPOSE $PORT
USER wekan
STOPSIGNAL SIGKILL CMD ["node", "/build/main.js"]
WORKDIR /home/wekan/app
#---------------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap:
# NODE_OPTIONS="--max_old_space_size=4096"
# Add more stack:
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
#---------------------------------------------------------------------
#
# CMD ["node", "/build/main.js"]
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 /build/main.js"]
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /build/main.js"]
CMD ["bash", "-c", "ulimit -s 65500; exec node /build/main.js"]

View file

@ -1,93 +0,0 @@
FROM arm64v8/ubuntu:23.04 AS builder
#FROM amd64/alpine:latest AS builder
# Set the environment variables for builder
ENV QEMU_VERSION=v7.2.0-1 \
QEMU_ARCHITECTURE=aarch64 \
NODE_ARCHITECTURE=linux-arm64 \
NODE_VERSION=v14.21.4 \
WEKAN_VERSION=latest \
WEKAN_ARCHITECTURE=arm64
# Install dependencies
#RUN apk update && apk add ca-certificates outils-sha1 && \
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt update && apt install ca-certificates wget unzip -y && \
\
# 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://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
#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 | sha256sum -c - && \
\
# Extract node and remove tar.gz
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
# Build wekan dockerfile
FROM --platform=linux/arm64 arm64v8/ubuntu:23.04
LABEL maintainer="wekan"
# Set the environment variables (defaults where required)
ENV QEMU_ARCHITECTURE=aarch64 \
NODE_ARCHITECTURE=linux-arm64 \
NODE_VERSION=v14.21.4 \
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} && \
# \
# # Install Health Check dependencies
# #apk add curl
#
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
# CMD curl --fail "http://localhost:$PORT" || exit 1
EXPOSE $PORT
USER wekan
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /home/wekan/bundle/main.js"]
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]

View file

@ -1,94 +0,0 @@
FROM arm64v8/ubuntu:23.04 AS builder
#FROM --platform=linux/amd64 amd64/ubuntu:23.04 AS builder
#FROM --platform=linux/amd64 ghcr.io/wekan/wekan:main AS builder
#FROM arm64v8/ubuntu:23.04 AS builder
#FROM amd64/alpine:latest AS builder
# Set the environment variables for builder
ENV QEMU_VERSION=v7.2.0-1 \
QEMU_ARCHITECTURE=s390x \
NODE_ARCHITECTURE=linux-s390x \
NODE_VERSION=v14.21.4 \
WEKAN_VERSION=latest \
WEKAN_ARCHITECTURE=s390x
# Install dependencies
#RUN apk update && apk add ca-certificates outils-sha1 && \
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt update && apt install ca-certificates wget unzip -y && \
\
# 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/${WEKAN_ARCHITECTURE}/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/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://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
#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 | sha256sum -c - && \
\
# Extract node and remove tar.gz
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
# Build wekan dockerfile
FROM --platform=linux/s390x s390x/ubuntu:23.04
LABEL maintainer="wekan"
# Set the environment variables (defaults where required)
ENV QEMU_ARCHITECTURE=s390x \
NODE_ARCHITECTURE=linux-s390x \
NODE_VERSION=v14.21.4 \
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} && \
# \
# # Install Health Check dependencies
# #apk add curl
#
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
# CMD curl --fail "http://localhost:$PORT" || exit 1
EXPOSE $PORT
USER wekan
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]

View file

@ -1,40 +0,0 @@
# Future
## Moved Import/Export/Sync issues to Big Picture Roadmap wiki page
This change is limited to only Import/Export/Sync issues, while those are In Progress of being fixed.
2023-11-21 xet7 closed 261 issues that are linked at https://github.com/wekan/wekan/wiki/Sync ,
that is Roadmap of Import/Export/Sync in WeKan. It means, that those issues progress will be
updated at that wiki page, when xet7 and other WeKan contributors fix those.
Many of those issues are In Progress of being fixed and added.
## Platform Updates
Issues related to platforms are being closed, because only list of working platforms is mentioned now
at WeKan website https://wekan.github.io Install section and at [ChangeLog](https://github.com/wekan/wekan/blob/main/CHANGELOG.md)
where is this new text:
> Newest WeKan at amd64 platforms: Linux bundle, Snap Candidate, Docker, Kubernetes. Fixing other platforms In Progress.
Platform support changes often, because:
- There are many dependencies, that update or break or change often
- Node.js segfaults at some CPU/OS
- Some platforms have build errors
Roadmap is to update all existing platforms, and add more platforms.
Upcoming platform upgrades:
- Fix migrations, so that newest WeKan can be released to Snap Stable. (Currently newest is at Snap Candidate).
## WeKan features
Most Meteor WeKan features are listed here:
https://github.com/wekan/wekan/wiki/Deep-Dive-Into-WeKan
Remaining features and all changes are listed here:
https://github.com/wekan/wekan/blob/main/CHANGELOG.md

View file

@ -1,10 +0,0 @@
# Governance
Anyone can send pull request to https://github.com/wekan/wekan/wiki/pulls ,
if there is permission to add code to WeKan with MIT license.
As maintainer, xet7 checks all pull requests and merges them.
Only xet7 has write access to repo https://github.com/wekan/wekan

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014-2024 The Wekan Team Copyright (c) 2014-2018 The Wekan Team
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

150
README.md
View file

@ -1,141 +1,97 @@
[Gitpod Ready-to-Code](https://gitpod.io/#https://github.com/wekan/wekan) # Wekan
# WeKan ® - Open Source kanban ## Stable
## Downloads - master+devel branch. At release, devel is merged to master.
- Receives fixes and features that have been tested at edge that they work.
- If you want automatic updates, [use Snap](https://github.com/wekan/wekan-snap/wiki/Install).
- If you want to test before update, [use Docker quay.io release tags](https://github.com/wekan/wekan/wiki/Docker).
https://wekan.github.io / Install WeKan ® Server ## Edge
## Docker Containers - edge branch. All new fixes and features are added to here first. [Testing Edge](https://github.com/wekan/wekan-snap/wiki/Snap-Developer-Docs).
- [GitHub](https://github.com/wekan/wekan/pkgs/container/wekan) [![Translate Wekan at Transifex](https://img.shields.io/badge/Translate%20Wekan-at%20Transifex-brightgreen.svg "Freenode IRC")](https://transifex.com/wekan/wekan)
- [Quay](https://quay.io/repository/wekan/wekan)
- [Docker Hub](https://hub.docker.com/r/wekanteam/wekan)
docker-compose.yml at https://github.com/wekan/wekan/blob/main/docker-compose.yml [![Wekan Vanila Chat][vanila_badge]][vanila_chat]
[![IRC #wekan](https://img.shields.io/badge/IRC%20%23wekan-on%20Freenode-brightgreen.svg "Freenode IRC")](http://webchat.freenode.net?channels=%23wekan&uio=d4)
## Standards [![Contributors](https://img.shields.io/github/contributors/wekan/wekan.svg "Contributors")](https://github.com/wekan/wekan/graphs/contributors)
[![Docker Repository on Quay](https://quay.io/repository/wekan/wekan/status "Docker Repository on Quay")](https://quay.io/repository/wekan/wekan)
[![Docker Hub container status](https://img.shields.io/docker/build/wekanteam/wekan.svg "Docker Hub container status")](https://hub.docker.com/r/wekanteam/wekan)
[![Docker Hub pulls](https://img.shields.io/docker/pulls/wekanteam/wekan.svg "Docker Hub Pulls")](https://hub.docker.com/r/wekanteam/wekan)
[![Wekan Build Status][travis_badge]][travis_status]
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/02137ecec4e34c5aa303f57637196a93 "Codacy Badge")](https://www.codacy.com/app/xet7/wekan?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=wekan/wekan&amp;utm_campaign=Badge_Grade)
[![Code Climate](https://codeclimate.com/github/wekan/wekan/badges/gpa.svg "Code Climate")](https://codeclimate.com/github/wekan/wekan)
[![Project Dependencies](https://david-dm.org/wekan/wekan.svg "Project Dependencies")](https://david-dm.org/wekan/wekan)
[![Code analysis at Open Hub](https://img.shields.io/badge/code%20analysis-at%20Open%20Hub-brightgreen.svg "Code analysis at Open Hub")](https://www.openhub.net/p/wekan)
- [WeKan and Standard for Public Code](https://wekan.github.io/standard-for-public-code/) assessment was made at 2023-11. **NOTE**:
Currently Wekan meets 8 out of 16 criteria out of the box.
Some others could be met with small changes.
## Code stats
- [CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4619)
- [Code Climate](https://codeclimate.com/github/wekan/wekan)
- [Open Hub](https://www.openhub.net/p/wekan)
- [OSS Insight](https://ossinsight.io/analyze/wekan/wekan)
## [Translate WeKan ® at Transifex](https://app.transifex.com/wekan/)
Translations to non-English languages are accepted only at [Transifex](https://app.transifex.com/wekan/wekan) using webbrowser.
New English strings of new features can be added as PRs to master branch file wekan/imports/i18n/data/en.i18n.json .
## [WeKan ® feature requests and bugs](https://github.com/wekan/wekan/issues)
Please add most of your questions as GitHub issue: [WeKan ® Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
It's better than at chat where details get lost when chat scrolls up.
## Chat
[Discussions][discussions] - WeKan Community GitHub Discussions, that are not [Feature Requests and Bugs](https://github.com/wekan/wekan/issues).
[WeKan IRC FAQ](https://github.com/wekan/wekan/wiki/IRC-FAQ)
## Docker: Latest tag has newest release
You can use latest tag to get newest release tag.
See bottom of https://github.com/wekan/wekan/issues/3874
## FAQ
**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](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 :) - Please don't feed the trolls and spammers that are mentioned in the FAQ :)
## About WeKan ® Wekan is an completely [Open Source][open_source] and [Free software][free_software]
WeKan ® is an completely [Open Source][open_source] and [Free software][free_software]
collaborative kanban board application with MIT license. collaborative kanban board application with MIT license.
Whether youre maintaining a personal todo list, planning your holidays with some friends, Whether youre maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool
to keep your things organized. They give you a visual overview of the current state of your project,
and make you productive by allowing you to focus on the few items that matter the most.
Since WeKan ® is a free software, you dont have to trust us with your data and can Since Wekan is a free software, you dont have to trust us with your data and can
install Wekan on your own computer or server. In fact we encourage you to do install Wekan on your own computer or server. In fact we encourage you to do
that by providing one-click installation on various platforms. that by providing one-click installation on various platforms.
- WeKan ® is used in [most countries of the world](https://snapcraft.io/wekan). - [Features][features]: Wekan has real-time user interface. Not all features are implemented, yet.
- WeKan ® largest user has 30k users using WeKan ® in their company. - [Platforms][platforms]: Wekan supports many platforms and plan is to add more. This will be the first place to look if you want to **install** it, test out and learn more in depth.
- WeKan ® has been [translated](https://app.transifex.com/wekan/) to about 105 languages. - [Integrations][integrations]: Current possible integrations and future plans.
- [Features][features]: WeKan ® has real-time user interface. - [Team](https://github.com/wekan/wekan/wiki/Team): The people who spends their time and make wekan into what it is right now.
- [Platforms][platforms]: WeKan ® supports many platforms.
WeKan ® is critical part of new platforms Wekan is currently being integrated to.
## Requirements ## Roadmap
- 64bit: Linux [Snap](https://github.com/wekan/wekan-snap/wiki/Install) or [Sandstorm](https://sandstorm.io) / [Roadmap](https://github.com/wekan/wekan/wiki/Roadmap)
[Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows).
[More Platforms](https://github.com/wekan/wekan/wiki/Platforms), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
- 1 GB RAM minimum free for WeKan ®. Production server should have minimum total 4 GB RAM.
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/main/docker-compose.yml): 3 frontend servers,
each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
- Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
- SECURITY: Updating to newest WeKan ® version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
Old versions have security issues because of old versions Node.js etc. Only newest WeKan ® is supported.
WeKan ® on Sandstorm is not usually affected by any Standalone WeKan ® (Snap/Docker/Source) security issues.
- [Reporting all new bugs immediately](https://github.com/wekan/wekan/issues).
New features and fixes are added to WeKan ® [many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
- [Backups](https://github.com/wekan/wekan/wiki/Backup) of WeKan ® database once a day miminum.
Bugs, updates, users deleting list or card, harddrive full, harddrive crash etc can eat your data. There is no undo yet.
Some bug can cause WeKan ® board to not load at all, requiring manual fixing of database content.
## Roadmap and Demo Upcoming Wekan App Development Platform will make possible many use cases. If you don't find your feature or integration in
GitHub issues and [Features][features] or [Integrations][integrations] page at wiki, please add them.
[Roadmap][roadmap_wekan] - Public read-only board at WeKan ® demo. We are very welcoming to new developers and teams to submit new pull requests to devel branch to make this Wekan App Development Platform possible faster. Please see [Developer Documentation][dev_docs] to get started.
[Developer Documentation][dev_docs] We also welcome sponsors for features and bugfixes. By working directly with Wekan you get the benefit of active maintenance and new features added by growing Wekan developer community.
- There is many companies and individuals contributing code to WeKan ®, to add features and bugfixes Actual work happens at [Wekan GitHub issues][wekan_issues].
[many times a day](https://github.com/wekan/wekan/blob/main/CHANGELOG.md).
- [Please add Add new Feature Requests and Bug Reports immediately](https://github.com/wekan/wekan/issues).
- [Commercial Support](https://wekan.team/commercial-support/).
We also welcome sponsors for features and bugfixes. See [Development links on Wekan wiki](https://github.com/wekan/wekan/wiki#Development) bottom of the page for more info.
By working directly with WeKan ® you get the benefit of active maintenance and new features added by growing WeKan ® developer community.
## Getting Started with Development If you want to know what is going on exactly this moment, you can check out the [project page](https://github.com/wekan/wekan/projects/2).
The default branch uses [Meteor 2 with Node.js 14](https://wekan.github.io/install/). ## Demo
To contribute, [create a fork](https://github.com/wekan/wekan/wiki/Emoji#2-create-fork-of-httpsgithubcomwekanwekan-at-github-web-page) and run `./rebuild-wekan.sh` (or `./rebuild-wekan.bat` on Windows) as detailed [here](https://github.com/wekan/wekan/wiki/Emoji#3-select-option-1-to-install-dependencies-and-then-enter). Once you're ready, please test your code and [submit a pull request (PR)](https://github.com/wekan/wekan/wiki/Emoji#7-test). [Wekan demo][roadmap_wefork]
Please refer to the [developer documentation](https://github.com/wekan/wekan/wiki/Developer-Documentation) for more information.
## Screenshot ## Screenshot
[More screenshots at Features page](https://github.com/wekan/wekan/wiki/Features) [More screenshots at Features page](https://github.com/wekan/wekan/wiki/Features)
[![Screenshot of WeKan ®][screenshot_wekan]][roadmap_wekan] [![Screenshot of Wekan][screenshot_wefork]][roadmap_wefork]
## License ## License
WeKan ® is released under the very permissive [MIT license](LICENSE), and made Wekan is released under the very permissive [MIT license](LICENSE), and made
with [Meteor](https://www.meteor.com). with [Meteor](https://www.meteor.com).
[platforms]: https://github.com/wekan/wekan/wiki/Platforms [platforms]: https://github.com/wekan/wekan/wiki/Platforms
[dev_docs]: https://github.com/wekan/wekan/wiki/Developer-Documentation [dev_docs]: https://github.com/wekan/wekan/wiki/Developer-Documentation
[screenshot_wekan]: https://wekan.github.io/wekan-dark-mode.png [screenshot_wekan]: http://i.imgur.com/cI4jW2h.png
[screenshot_wefork]: https://wekan.github.io/wekan-markdown.png
[features]: https://github.com/wekan/wekan/wiki/Features [features]: https://github.com/wekan/wekan/wiki/Features
[roadmap_wekan]: https://boards.wekan.team/b/D2SzJKZDS4Z48yeQH/wekan-open-source-kanban-board-with-mit-license [integrations]: https://github.com/wekan/wekan/wiki/Integrations
[wekan_issues]: https://github.com/wekan/wekan/issues [roadmap_wekan]: http://try.wekan.io/b/MeSsFJaSqeuo9M6bs/wekan-roadmap
[roadmap_wefork]: https://wekan.indie.host/b/t2YaGmyXgNkppcFBq/wekan-fork-roadmap
[wekan_issues]: https://github.com/wekan/wekan/issues [wekan_issues]: https://github.com/wekan/wekan/issues
[wefork_issues]: https://github.com/wefork/wekan/issues
[docker_image]: https://hub.docker.com/r/wekanteam/wekan/ [docker_image]: https://hub.docker.com/r/wekanteam/wekan/
[travis_badge]: https://travis-ci.org/wekan/wekan.svg?branch=devel
[travis_status]: https://travis-ci.org/wekan/wekan
[wekan_wiki]: https://github.com/wekan/wekan/wiki [wekan_wiki]: https://github.com/wekan/wekan/wiki
[translate_wekan]: https://app.transifex.com/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
[discussions]: https://github.com/wekan/wekan/discussions [vanila_badge]: https://vanila.io/img/join-chat-button2.png
[vanila_chat]: https://chat.vanila.io/channel/wekan

View file

@ -1,113 +0,0 @@
# 🔐 WeKan — Login System Overview
This document provides a detailed overview of WeKans **login and authentication system**, covering client-side UI, server-side logic, external authentication methods, and potential upgrade paths.
---
## 🖥️ Login Web UI
WeKan's login interface is implemented using a combination of:
- `layouts.jade` Login HTML structure
- `layouts.js` Login logic and interactivity
- `layouts.css` Styling and layout
📁 Source: [`client/components/main`](https://github.com/wekan/wekan/tree/main/client/components/main)
---
## ⚙️ Server-Side Authentication
Server-side login functionality is handled in:
- [`server/authentication.js`](https://github.com/wekan/wekan/blob/main/server/authentication.js)
Other related configurations:
- 🔧 Account config: [`config/accounts.js`](https://github.com/wekan/wekan/blob/main/config/accounts.js)
- 📨 Sign-up invitations: [`models/settings.js#L275`](https://github.com/wekan/wekan/blob/main/models/settings.js#L275)
- 👤 User creation logic: [`models/users.js#L1339`](https://github.com/wekan/wekan/blob/main/models/users.js#L1339)
---
## 👥 Meteor User Accounts
WeKan utilizes Meteors `accounts` system. Relevant resources:
- 📚 Meteor 2.x Accounts Docs: [v2-docs.meteor.com/api/accounts](https://v2-docs.meteor.com/api/accounts)
- 🔍 Meteor Packages:
- [`packages`](https://github.com/wekan/wekan/blob/main/.meteor/packages)
- [`versions`](https://github.com/wekan/wekan/blob/main/.meteor/versions)
- 📦 Meteor 2.14 core packages: [Meteor 2.14 packages](https://github.com/meteor/meteor/tree/release/METEOR%402.14/packages)
---
## 🔐 External Authentication (OIDC, LDAP, etc.)
WeKan supports external authentication methods via internal packages.
📁 See [`packages/`](https://github.com/wekan/wekan/tree/main/packages) for:
- OpenID Connect (OIDC)
- LDAP
- OAuth and other integrations
---
## 📦 NPM & AtmosphereJS Dependencies
- 🔗 `package.json`: [Dependencies list](https://github.com/wekan/wekan/blob/main/package.json)
- 🧩 WekanTeam scoped NPM packages: [@wekanteam on npm](https://www.npmjs.com/search?q=%40wekanteam)
- ☁️ AtmosphereJS Meteor packages: [atmospherejs.com](https://atmospherejs.com)
---
## 🚧 Meteor Version & Upgrade Notes
- 📌 Current Version: **Meteor 2.14**
- [`.meteor/release`](https://github.com/wekan/wekan/blob/main/.meteor/release)
- 🔧 Maintained with only **critical fixes** until ~Summer 2025
- 🚀 Migration to **Meteor 3** or a new framework is under consideration
📘 Meteor 3 API: [docs.meteor.com/api/accounts](https://docs.meteor.com/api/accounts)
---
## 🧪 Prototypes & Examples
### 🐘 PHP Prototype Sign-Up
Used in experimental versions:
- Step 1: [`sign-up1.php`](https://github.com/wekan/php/blob/main/page/sign-up1.php)
- Step 2: [`sign-up2.php`](https://github.com/wekan/php/blob/main/page/sign-up2.php)
- Main entry: [`index.php#L72-L83`](https://github.com/wekan/php/blob/main/public/index.php#L72-L83)
---
### 🎨 WeKan Studio Prototype
Sign-up logic in the **WeKan Studio** version:
- [`signUp.fmt`](https://github.com/wekan/wekanstudio/blob/main/srv/templates/login/signUp.fmt)
---
## 📎 Future Considerations
- Upgrading to **Meteor 3.x**
- Refactoring frontend logic to fix translation rendering order
- Exploring **simplified authentication systems** in future prototypes
---
## 🔗 Project Links
- 🔧 Main Repo: [github.com/wekan/wekan](https://github.com/wekan/wekan)
- 🌐 Website: [wekan.github.io](https://wekan.github.io)
- 📚 Documentation: [Wekan Wiki](https://github.com/wekan/wekan/wiki)
---
---

View file

@ -1,11 +1,10 @@
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
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@wekan.team and not by the information responsibly by sending an email to security (at) wekan.team 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 at https://wekan.github.io/hall-of-fame . Others have just posted public GitHub issue,
so they are not at that hall-of-fame page.
## How should reports be formatted? ## How should reports be formatted?
@ -30,7 +29,7 @@ added to the Wekan Hall of Fame.
## Which domains are in scope? ## Which domains are in scope?
No public domains, because all those are donated to Wekan Open Source project, No public domains, because all those are donated to Wekan Open Source project,
and we don't have any permissions to do security scans on those donated servers. and we don't have any permissions to do security scans on those donated servers
Please don't perform research that could impact other users. Secondly, please keep Please don't perform research that could impact other users. Secondly, please keep
the reports short and succinct. If we fail to understand the logics of your bug, we will tell you. the reports short and succinct. If we fail to understand the logics of your bug, we will tell you.
@ -49,132 +48,31 @@ like Snap and Docker have their own specific sandboxing etc features.
Standalone Wekan by default does not load any files from Internet, like fonts, CSS, etc. Standalone Wekan by default does not load any files from Internet, like fonts, CSS, etc.
This also means all Standalone Wekan functionality works in offline local networks. This also means all Standalone Wekan functionality works in offline local networks.
WeKan is used at most countries of the world https://snapcraft.io/wekan Wekan is used by companies that have [thousands of users](https://github.com/wekan/wekan/wiki/AWS) and at healthcare.
and by by companies that have 30k users.
- Wekan private board attachments are not accessible without logging in. Wekan uses xss package for input fields like cards, as you can see from
- There is feature to set board public, so that board is visible without logging in in readonly mode, with realtime updates. [package.json](https://github.com/wekan/wekan/blob/devel/package.json). Other used versions can be seen from
- Admin Panel has feature to disable all public boards, so all boards are private. [Meteor versions file](https://github.com/wekan/wekan/blob/devel/.meteor/versions).
Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
It's possible to add attachments to cards, and markdown/html links to files.
## SSL/TLS Wekan attachments are not accessible without logging in. Import from Trello works by copying
Trello export JSON to Wekan Trello import page, and in Trello JSON file there is direct links to all publicly
accessible Trello attachment files, that Standalone Wekan downloads directly to Wekan MongoDB database in
[CollectionFS](https://github.com/wekan/wekan/pull/875) format. When Wekan board is exported in
Wekan JSON format, all board attachments are included in Wekan JSON file as base64 encoded text.
That Wekan JSON format file can be imported to Sandstorm Wekan with all the attachments, when we get
latest Wekan version working on Sandstorm, only couple of bugs are left before that. In Sandstorm it's not
possible yet to import from Trello with attachments, because Wekan does not implement Sandstorm-compatible
access to outside of Wekan grain.
- SSL/TLS encrypts traffic between webbrowser and webserver. Standalone Wekan only has password auth currently, there is work in progress to add
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530 [oauth2](https://github.com/wekan/wekan/pull/1578), [Openid](https://github.com/wekan/wekan/issues/538),
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check. [LDAP](https://github.com/wekan/wekan/issues/119) etc. If you need more login security for Standalone Wekan now,
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert. it's possible add additional [Google Auth proxybouncer](https://github.com/wekan/wekan/wiki/Let's-Encrypt-and-Google-Auth) in front of password auth, and then use Google Authenticator for Google Auth. Standalone Wekan does have [brute force protection with eluck:accounts-lockout and browser-policy clickjacking protection](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md#v080-2018-04-04-wekan-release). You can also optionally use some [WAF](https://en.wikipedia.org/wiki/Web_application_firewall)
- For On Premise: like for example [AWS WAF](https://aws.amazon.com/waf/).
- https://caddyserver.com/docs/automatic-https#local-https
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
- https://github.com/wekan/wekan/wiki/Azure
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
## XSS [All Wekan Platforms](https://github.com/wekan/wekan/wiki/Platforms)
- Dompurify https://www.npmjs.com/package/dompurify
- WeKan uses dompurify npm package to filter for XSS at fields like cards, as you can see from
[package.json](https://github.com/wekan/wekan/blob/main/package.json). Other used versions can be seen from
[Meteor versions file](https://github.com/wekan/wekan/blob/main/.meteor/versions).
- Forms can include markdown links, html, image tags etc like you see at https://wekan.github.io .
- It's possible to add attachments to cards, and markdown/html links to files.
- Dompurify cleans up viewed code, so Javascript in input fields does not execute
- https://wekan.github.io/hall-of-fame/fieldbleed/
- Reaction in comment is now checked, that it does not have extra added code
- https://wekan.github.io/hall-of-fame/reactionbleed/
- https://github.com/wekan/wekan/blob/main/packages/markdown/src/template-integration.js#L76
## QA about PubSub
Q:
Hello,
I have just seen the Meteor DevTools Evolved extension and was wondering if anyone had asked themselves the question of security.
Insofar as all data is shown in the minimongo tab in plain text.
How can data be hidden from this extension?
A:
## PubSub
- It is not security issue to show some text or image, that user has permission to see. It is a security issue, if browserside is some text or image that user should not see.
- Meteor has browserside minimongo database, made with Javascript, updated with Publish/Subscribe, PubSub.
- Publish/Subscribe means, that realtime web framework reads database changes stream, and then immediately updates webpage,
like like dashboards, chat, kanban. That is the point in any realtime web framework in any programming language.
- Yes, you should check with Meteor DevTools Evolved Chromium/Firefox extension that at minimongo is only text that user has permission to see.
- Do checking as logged in user, and logged out user.
- Check permissions and sanitize before allowing some change, because someone could modify content of input field,
PubSub/websocket data (for example with Burp Suite Community Edition), etc.
- If you have REST API, also check that only those that have login token, and have permission, can view or edit text
- You should not include any data user is not allowed to see. Not to webpage text, not to websockets/PubSub, etc.
- Minimongo should not have password hashes PubSub https://wekan.github.io/hall-of-fame/userbleed/
- PubSub uses Websockets, so you need those to be enabled at webserver like Caddy/Nginx/Apache etc, examples of settings
at right menu of https://github.com/wekan/wekan/wiki
- Clientside https://github.com/wekan/wekan/tree/main/client/components subscribes to
PubSub https://github.com/wekan/wekan/tree/main/server/publications or calls meteor methods at https://github.com/wekan/wekan/tree/main/models
- For Admin:
- You can have input field for password https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- You can save password to database https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- Check that only current user or Admin can change password https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- Note that currentUser uses code like Meteor.user() in .js file
- Do not have password hashes in PubSub https://github.com/wekan/wekan/blob/main/server/publications/users.js
- Only show Admin Panel to Admin https://github.com/wekan/wekan/blob/main/client/components/settings/settingBody.jade#L3
- If there is a lot of data, use pagination https://github.com/wekan/wekan/blob/main/client/components/settings/peopleBody.js
- Only have limited amount of data published in PubSub. Limit in MongoDB query in publications how much is published. Too much could make browser too slow.
- Use Environment variables for any email etc passwords.
- But what if you would like to remove minimongo? And only use Meteor methods for saving? In that case, you don't have realtime updates,
and you need to write much more code to load and save data yourself, handle any multi user data saving conflicts yourself,
and many Meteor Atmospherejs.com PubSub using packages would not work anymore https://github.com/wekan/we
## PubSub: Fix that user can not change to Admin
- With PubSub, there is checking, that someone modifying Websockets content, like permission isAdmin, can not change to Admin.
- https://github.com/wekan/wekan/commit/cbad4cf5943d47b916f64b4582f8ca76a9dfd743
- https://wekan.github.io/hall-of-fame/adminbleed/
## Permissions and Roles
- For any user permissions, it's best to use Meteor package package https://github.com/Meteor-Community-Packages/meteor-roles .
- Currently WeKan has custom hardcoded permissions, WeKan does not yet use that meteor-roles package.
- Using permissions at WeKan sidebar https://github.com/wekan/wekan/blob/main/client/components/sidebar/sidebar.js#L1854-L1875
- List of roles https://github.com/wekan/wekan/wiki/REST-API-Role . Change at board or Admin Panel. Also Organizations/Teams.
- Worker role: https://github.com/wekan/wekan/issues/2788
- Not implemented yet: Granular Roles https://github.com/wekan/wekan/issues/3022
- Check is user logged in, with `if (Meteor.user()) {`
- Check is code running at server `if (Meteor.isServer()) {` or client `if Meteor.isClient()) {` .
- Here is some authentication code https://github.com/wekan/wekan/blob/main/server/authentication.js
## Environment variables
- For any passwords, use environment variables, those are serverside
- Do not copy environment variable to public variable that is visible browserside https://github.com/wekan/wekan/blob/main/server/max-size.js
```
Meteor.startup(() => {
if (process.env.HEADER_LOGIN_ID) {
Meteor.settings.public.attachmentsUploadMaxSize = process.env.ATTACHMENTS_UPLOAD_MAX_SIZE;
Meteor.settings.public.attachmentsUploadMimeTypes = process.env.ATTACHMENTS_UPLOAD_MIME_TYPES;
Meteor.settings.public.avatarsUploadMaxSize = process.env.AVATARS_UPLOAD_MAX_SIZE;
```
- For serverside, you can set Meteor.settings.variablename, without text public
- For WeKan kanban, there is feature for setting board public, it can be viewed by anyone, there is realtime updates. But
- Some of those permissions are checked at users.js models at https://github.com/wekan/wekan/tree/main/models
- Environment variables are used for email server passwords, etc, at all platforms https://github.com/wekan/wekan/commit/a781c0e7dcfdbe34c1483ee83cec12455b7026f7
## Escape HTML comment tags so that HTML comments are visible
- Someone reported, that it is problem that content of HTML comments in edit mode, are not visible at at view mode, so this makes HTML comments visible.
- https://github.com/wekan/wekan/commit/167863d95711249e69bb3511175d73b34acbbdb3
- https://wekan.github.io/hall-of-fame/invisiblebleed/
## Attachments: XSS in filename is sanitized
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- https://wekan.github.io/hall-of-fame/filebleed/
## Brute force login protection
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
- https://github.com/wekan/wekan/tree/main/packages/wekan-accounts-lockout
### Sandstorm Wekan Security ### Sandstorm Wekan Security
@ -207,6 +105,12 @@ a security issue, we'd like to know about it, and also how to fix it:
Typical already known or "no impact" bugs such as: Typical already known or "no impact" bugs such as:
- Brute force password guessign. Currently there is
[brute force protection with eluck:accounts-lockout](https://github.com/wekan/wekan/blob/devel/CHANGELOG.md#v080-2018-04-04-wekan-release).
- Security issues related to that Wekan uses Meteor 1.6.0.1 related packages, and upgrading to newer
Meteor 1.6.1 is complicated process that requires lots of changes to many dependency packages.
Upgrading [has been tried many times, spending a lot of time](https://github.com/meteor/meteor/issues/9609)
but there still is issues. Helping with package upgrades is very welcome.
- [Wekan API old tokens not replaced correctly](https://github.com/wekan/wekan/issues/1437) - [Wekan API old tokens not replaced correctly](https://github.com/wekan/wekan/issues/1437)
- Missing Cookie flags on non-session cookies or 3rd party cookies - Missing Cookie flags on non-session cookies or 3rd party cookies
- Logout CSRF - Logout CSRF
@ -217,7 +121,7 @@ Typical already known or "no impact" bugs such as:
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server. - Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
Wekan is Open Source with MIT license, and free to use also for commercial use. Wekan is Open Source with MIT license, and free to use also for commercial use.
We welcome all fixes to improve security by email to security@wekan.team We welcome all fixes to improve security by email to security (at) wekan.team .
## Bonus Points ## Bonus Points

View file

@ -1,9 +0,0 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v7.85.0"
files:
userUploads:
- README.md
userScripts:
build: stacksmith/user-scripts/build.sh
boot: stacksmith/user-scripts/boot.sh
run: stacksmith/user-scripts/run.sh

752
api.py
View file

@ -1,752 +0,0 @@
#! /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
# TODO:
# addcustomfieldtoboard: There is error: Settings must be object. So adding does not work yet.
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
syntax = """=== Wekan API Python CLI: Shows IDs for addcard ===
# AUTHORID is USERID that writes card or custom field.
If *nix: chmod +x api.py => ./api.py users
Syntax:
User API:
python3 api.py user # Current user and list of current user boards
python3 api.py boards USERID # Boards of USERID
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 customfields BOARDID # Custom Fields of BOARDID
python3 api.py customfield BOARDID CUSTOMFIELDID # Info of CUSTOMFIELDID
python3 api.py addcustomfieldtoboard AUTHORID BOARDID NAME TYPE SETTINGS SHOWONCARD AUTOMATICALLYONCARD SHOWLABELONMINICARD SHOWSUMATTOPOFLIST # Add Custom Field to Board
python3 api.py editcustomfield BOARDID LISTID CARDID CUSTOMFIELDID NEWCUSTOMFIELDVALUE # Edit Custom Field
python3 api.py listattachments BOARDID # List attachments
python3 api.py cardsbyswimlane SWIMLANEID LISTID # Retrieve cards list on a swimlane
python3 api.py getcard BOARDID LISTID CARDID # Get card info
python3 api.py addlabel BOARDID LISTID CARDID LABELID # Add label to a card
python3 api.py addcardwithlabel AUTHORID BOARDID SWIMLANEID LISTID CARDTITLE CARDDESCRIPTION LABELIDS # Add a card and a label
python3 api.py editboardtitle BOARDID NEWBOARDTITLE # Edit board title
python3 api.py copyboard BOARDID NEWBOARDTITLE # Copy a board
python3 api.py createlabel BOARDID LABELCOLOR LABELNAME (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Create a new label
python3 api.py editcardcolor BOARDID LISTID CARDID COLOR (Color available: `white`, `green`, `yellow`, `orange`, `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`, `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`) # Edit card color
python3 api.py addchecklist BOARDID CARDID TITLE ITEM1 ITEM2 ITEM3 ITEM4 (You can add multiple items or just one, or also without any item, just TITLE works as well. * If items or Title contains spaces, you should add ' between them.) # Add checklist + item on a card
python3 api.py deleteallcards BOARDID SWIMLANEID ( * Be careful will delete ALL CARDS INSIDE the swimlanes automatically in every list * ) # Delete all cards on a swimlane
python3 api.py checklistid BOARDID CARDID # Retrieve Checklist ID attached to a card
python3 api.py checklistinfo BOARDID CARDID CHECKLISTID # Get checklist info
python3 api.py get_list_cards_count BOARDID LISTID # Retrieve how many cards in a list
python3 api.py get_board_cards_count BOARDID # Retrieve how many cards in a board
Admin API:
python3 api.py users # All users
python3 api.py boards # All Public Boards
python3 api.py newuser USERNAME EMAIL PASSWORD
"""
if arguments == 0:
print(syntax)
exit
# 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")
# ------- 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 -------------
"""
=== ADD CUSTOM FIELD TO BOARD ===
Type: text, number, date, dropdown, checkbox, currency, stringtemplate.
python3 api.py addcustomfieldtoboard cmx3gmHLKwAXLqjxz LcDW4QdooAx8hsZh8 "SomeField" "date" "" true true true true
=== USERS ===
python3 api.py users
=> 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'
apiuser = 'api/user'
apiallusers = 'api/allusers'
e = 'export'
s = '/'
l = 'lists'
sw = 'swimlane'
sws = 'swimlanes'
cs = 'cards'
cf = 'custom-fields'
bs = 'boards'
apbs = 'allpublicboards'
atl = 'attachmentslist'
at = 'attachment'
ats = 'attachments'
users = wekanurl + apiusers
user = wekanurl + apiuser
allusers = wekanurl + apiallusers
# ------- API URL GENERATION END -----------
# ------- LOGIN TOKEN START -----------
data = {"username": username, "password": password}
body = requests.post(wekanloginurl, json=data)
d = body.json()
apikey = d['token']
# ------- LOGIN TOKEN END -----------
if arguments == 10:
if sys.argv[1] == 'addcustomfieldtoboard':
# ------- ADD CUSTOM FIELD TO BOARD START -----------
authorid = sys.argv[2]
boardid = sys.argv[3]
name = sys.argv[4]
type1 = sys.argv[5]
settings = str(json.loads(sys.argv[6]))
# There is error: Settings must be object. So this does not work yet.
#settings = {'currencyCode': 'EUR'}
print(type(settings))
showoncard = sys.argv[7]
automaticallyoncard = sys.argv[8]
showlabelonminicard = sys.argv[9]
showsumattopoflist = sys.argv[10]
customfieldtoboard = wekanurl + apiboards + boardid + s + cf
# Add Custom Field to Board
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'authorId': '{}'.format(authorid), 'name': '{}'.format(name), 'type': '{}'.format(type1), 'settings': '{}'.format(settings), 'showoncard': '{}'.format(showoncard), 'automaticallyoncard': '{}'.format(automaticallyoncard), 'showlabelonminicard': '{}'.format(showlabelonminicard), 'showsumattopoflist': '{}'.format(showsumattopoflist)}
body = requests.post(customfieldtoboard, data=post_data, headers=headers)
print(body.text)
# ------- ADD CUSTOM FIELD TO BOARD END -----------
if arguments == 8:
if sys.argv[1] == 'addcardwithlabel':
# ------- ADD CARD WITH LABEL 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]
labelIds = sys.argv[8] # Aggiunto labelIds
cardtolist = wekanurl + apiboards + boardid + s + l + s + listid + s + cs
# Add card
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {
'authorId': '{}'.format(authorid),
'title': '{}'.format(cardtitle),
'description': '{}'.format(carddescription),
'swimlaneId': '{}'.format(swimlaneid),
'labelIds': labelIds
}
body = requests.post(cardtolist, data=post_data, headers=headers)
print(body.text)
# If ok id card
if body.status_code == 200:
card_data = body.json()
new_card_id = card_data.get('_id')
# Updating card
if new_card_id:
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + new_card_id
put_data = {'labelIds': labelIds}
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)
else:
print("Error obraining ID.")
else:
print("Error adding card.")
# ------- ADD CARD WITH LABEL END -----------
if arguments == 7:
if sys.argv[1] == 'addcard':
# ------- ADD 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
# Add card
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'authorId': '{}'.format(authorid), 'title': '{}'.format(cardtitle), 'description': '{}'.format(carddescription), 'swimlaneId': '{}'.format(swimlaneid)}
body = requests.post(cardtolist, data=post_data, headers=headers)
print(body.text)
# ------- ADD CARD END -----------
if arguments == 6:
if sys.argv[1] == 'editcard':
# ------- EDIT CARD 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)
# ------- EDIT CARD END -----------
if sys.argv[1] == 'editcustomfield':
# ------- EDIT CUSTOMFIELD START -----------
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
customfieldid = sys.argv[5]
newcustomfieldvalue = sys.argv[6]
edfield = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid + s + 'customFields' + s + customfieldid
#print(edfield)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'_id': '{}'.format(customfieldid), 'value': '{}'.format(newcustomfieldvalue)}
#print(post_data)
body = requests.post(edfield, data=post_data, headers=headers)
print("=== EDIT CUSTOMFIELD ===\n")
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- EDIT CUSTOMFIELD END -----------
if arguments == 5:
if sys.argv[1] == 'addlabel':
# ------- EDIT CARD ADD LABEL START -----------
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
labelIds = sys.argv[5]
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'labelIds': labelIds}
body = requests.put(edcard, data=put_data, headers=headers)
print("=== ADD LABEL ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- EDIT CARD ADD LABEL END -----------
if sys.argv[1] == 'editcardcolor':
# ------- EDIT CARD COLOR START -----------
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
newcolor = sys.argv[5]
valid_colors = ['white', 'green', 'yellow', 'orange', 'red', 'purple', 'blue', 'sky', 'lime', 'pink', 'black',
'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen', 'slateblue', 'magenta', 'gold', 'navy',
'gray', 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo']
if newcolor not in valid_colors:
print("Invalid color. Choose a color from the list.")
sys.exit(1)
edcard = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
print(edcard)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
put_data = {'color': '{}'.format(newcolor)}
body = requests.put(edcard, data=put_data, headers=headers)
print("=== EDIT CARD COLOR ===\n")
body = requests.get(edcard, headers=headers)
data2 = body.text.replace('}', "}\n")
print(data2)
# ------- EDIT CARD COLOR END -----------
if arguments >= 4:
if sys.argv[1] == 'newuser':
# ------- CREATE NEW USER START -----------
username = sys.argv[2]
email = sys.argv[3]
password = sys.argv[4]
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'username': '{}'.format(username),'email': '{}'.format(email),'password': '{}'.format(password)}
body = requests.post(users, data=post_data, headers=headers)
print("=== CREATE NEW USER ===\n")
print(body.text)
# ------- CREATE NEW USER END -----------
if sys.argv[1] == 'getcard':
# ------- LIST OF CARD START -----------
boardid = sys.argv[2]
listid = sys.argv[3]
cardid = sys.argv[4]
listone = wekanurl + apiboards + boardid + s + l + s + listid + s + cs + s + cardid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
print("=== INFO OF ONE LIST ===\n")
print("URL:", listone) # Stampa l'URL per debug
try:
response = requests.get(listone, headers=headers)
print("=== RESPONSE ===\n")
print("Status Code:", response.status_code) # Stampa il codice di stato per debug
if response.status_code == 200:
data2 = response.text.replace('}', "}\n")
print(data2)
else:
print(f"Error: {response.status_code}")
print(f"Response: {response.text}")
except Exception as e:
print(f"Error in the GET request: {e}")
# ------- LISTS OF CARD END -----------
if sys.argv[1] == 'createlabel':
# ------- CREATE LABEL START -----------
boardid = sys.argv[2]
labelcolor = sys.argv[3]
labelname = sys.argv[4]
label_url = wekanurl + apiboards + boardid + s + 'labels'
print(label_url)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
# Object to send
put_data = {'label': {'color': labelcolor, 'name': labelname}}
print("URL:", label_url)
print("Headers:", headers)
print("Data:", put_data)
try:
response = requests.put(label_url, json=put_data, headers=headers)
print("=== CREATE LABELS ===\n")
print("Response Status Code:", response.status_code)
print("Response Text:", response.text)
except Exception as e:
print("Error:", e)
# ------- CREATE LABEL END -----------
if sys.argv[1] == 'addchecklist':
# ------- ADD CHECKLIST START -----------
board_id = sys.argv[2]
card_id = sys.argv[3]
checklist_title = sys.argv[4]
# Aggiungi la checklist
checklist_url = wekanurl + apiboards + board_id + s + cs + s + card_id + '/checklists'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
data = {'title': checklist_title}
response = requests.post(checklist_url, data=data, headers=headers)
response.raise_for_status()
result = json.loads(response.text)
checklist_id = result.get('_id')
print(f"Checklist '{checklist_title}' created. ID: {checklist_id}")
# Aggiungi gli items alla checklist
items_to_add = sys.argv[5:]
for item_title in items_to_add:
checklist_item_url = wekanurl + apiboards + board_id + s + cs + s + card_id + s + 'checklists' + s + checklist_id + '/items'
item_data = {'title': item_title}
item_response = requests.post(checklist_item_url, data=item_data, headers=headers)
item_response.raise_for_status()
item_result = json.loads(item_response.text)
checklist_item_id = item_result.get('_id')
print(f"Item '{item_title}' added. ID: {checklist_item_id}")
if sys.argv[1] == 'checklistinfo':
# ------- ADD CHECKLIST START -----------
board_id = sys.argv[2]
card_id = sys.argv[3]
checklist_id = sys.argv[4]
checklist_url = wekanurl + apiboards + board_id + s + cs + s + card_id + '/checklists' + s + checklist_id
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
response = requests.get(checklist_url, headers=headers)
response.raise_for_status()
checklist_info = response.json()
print("Checklist Info:")
print(checklist_info)
if arguments == 3:
if sys.argv[1] == 'editboardtitle':
# ------- EDIT BOARD TITLE START -----------
boardid = sys.argv[2]
boardtitle = sys.argv[3]
edboardtitle = wekanurl + apiboards + boardid + s + 'title'
print(edboardtitle)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'title': boardtitle}
body = requests.put(edboardtitle, json=post_data, headers=headers)
print("=== EDIT BOARD TITLE ===\n")
#body = requests.get(edboardtitle, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
if body.status_code == 200:
print("Succesfull!")
else:
print(f"Error: {body.status_code}")
print(body.text)
# ------- EDIT BOARD TITLE END -----------
if sys.argv[1] == 'copyboard':
# ------- COPY BOARD START -----------
boardid = sys.argv[2]
boardtitle = sys.argv[3]
edboardcopy = wekanurl + apiboards + boardid + s + 'copy'
print(edboardcopy)
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
post_data = {'title': boardtitle}
body = requests.post(edboardcopy, json=post_data, headers=headers)
print("=== COPY BOARD ===\n")
#body = requests.get(edboardcopy, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
if body.status_code == 200:
print("Succesfull!")
else:
print(f"Error: {body.status_code}")
print(body.text)
# ------- COPY BOARD END -----------
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 sys.argv[1] == 'customfield':
# ------- INFO OF CUSTOM FIELD START -----------
boardid = sys.argv[2]
customfieldid = sys.argv[3]
customfieldone = wekanurl + apiboards + boardid + s + cf + s + customfieldid
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
print("=== INFO OF ONE CUSTOM FIELD ===\n")
body = requests.get(customfieldone, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- INFO OF CUSTOM FIELD END -----------
if sys.argv[1] == 'cardsbyswimlane':
# ------- RETRIEVE CARDS BY SWIMLANE ID START -----------
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
cardsbyswimlane = wekanurl + apiboards + boardid + s + sws + s + swimlaneid + s + cs
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
print("=== CARDS BY SWIMLANE ID ===\n")
print("URL:", cardsbyswimlane) # Debug
try:
body = requests.get(cardsbyswimlane, headers=headers)
print("Status Code:", body.status_code) # Debug
data = body.text.replace('}', "}\n")
print("Data:", data)
except Exception as e:
print("Error GET:", e)
# ------- RETRIEVE CARDS BY SWIMLANE ID END -----------
if sys.argv[1] == 'deleteallcards':
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
# ------- GET SWIMLANE CARDS START -----------
get_swimlane_cards_url = wekanurl + apiboards + boardid + s + "swimlanes" + s + swimlaneid + s + "cards"
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
try:
response = requests.get(get_swimlane_cards_url, headers=headers)
response.raise_for_status()
cards_data = response.json()
# Print the details of each card
for card in cards_data:
# ------- DELETE CARD START -----------
delete_card_url = wekanurl + apiboards + boardid + s + "lists" + s + card['listId'] + s + "cards" + s + card['_id']
try:
response = requests.delete(delete_card_url, headers=headers)
if response.status_code == 404:
print(f"Card not found: {card['_id']}")
else:
response.raise_for_status()
deleted_card_data = response.json()
print(f"Card Deleted Successfully. Card ID: {deleted_card_data['_id']}")
except requests.exceptions.RequestException as e:
print(f"Error deleting card: {e}")
# ------- DELETE CARD END -----------
except requests.exceptions.RequestException as e:
print(f"Error getting swimlane cards: {e}")
sys.exit(1)
# ------- GET SWIMLANE CARDS END -----------
if sys.argv[1] == 'get_list_cards_count':
# ------- GET LIST CARDS COUNT START -----------
boardid = sys.argv[2]
listid = sys.argv[3]
get_list_cards_count_url = wekanurl + apiboards + boardid + s + l + s + listid + s + "cards_count"
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
try:
response = requests.get(get_list_cards_count_url, headers=headers)
response.raise_for_status()
data = response.json()
print(f"List Cards Count: {data['list_cards_count']}")
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
# ------- GET LIST CARDS COUNT END -----------
if sys.argv[1] == 'checklistid':
# ------- ADD CHECKLIST START -----------
board_id = sys.argv[2]
card_id = sys.argv[3]
checklist_url = wekanurl + apiboards + board_id + s + cs + s + card_id + '/checklists'
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
response = requests.get(checklist_url, headers=headers)
response.raise_for_status()
checklists = response.json()
print("Checklists:")
for checklist in checklists:
print(checklist)
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] == 'customfields':
# ------- CUSTOM FIELDS OF BOARD START -----------
boardid = sys.argv[2]
boardcustomfields = wekanurl + apiboards + boardid + s + cf
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
body = requests.get(boardcustomfields, headers=headers)
print("=== CUSTOM FIELDS OF BOARD ===\n")
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- CUSTOM FIELDS OF BOARD END -----------
if sys.argv[1] == 'swimlanes':
boardid = sys.argv[2]
swimlanes = wekanurl + apiboards + boardid + s + sws
# ------- 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 sys.argv[1] == 'get_board_cards_count':
# ------- GET BOARD CARDS COUNT START -----------
boardid = sys.argv[2]
get_board_cards_count_url = wekanurl + apiboards + boardid + s + "cards_count"
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
try:
response = requests.get(get_board_cards_count_url, headers=headers)
response.raise_for_status()
data = response.json()
print(f"Board Cards Count: {data['board_cards_count']}")
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
# ------- GET BOARD CARDS COUNT 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 -----------
if sys.argv[1] == 'user':
# ------- LIST OF ALL USERS START -----------
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
print(user)
print("=== USER ===\n")
body = requests.get(user, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- LIST OF ALL USERS END -----------
if sys.argv[1] == 'boards':
# ------- LIST OF PUBLIC BOARDS START -----------
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
print("=== PUBLIC BOARDS ===\n")
listpublicboards = wekanurl + apiboards
body = requests.get(listpublicboards, headers=headers)
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- LIST OF PUBLIC BOARDS END -----------

View file

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

View file

@ -1,64 +0,0 @@
.activity-title {
margin: 0 0.5em 0.8em;
display: flex;
justify-content: space-between;
}
.reactions-popup .add-comment-reaction {
display: inline-block;
cursor: pointer;
border-radius: 5px;
font-size: 22px;
text-align: center;
line-height: 30px;
width: 40px;
}
.reactions-popup .add-comment-reaction:hover {
background-color: #b0c4de;
}
.activities {
clear: both;
}
.activities .activity {
margin: 0.5px 0;
padding: 6px 0;
display: flex;
}
.activities .activity .member {
width: 32px;
height: 32px;
}
.activities .activity .activity-member {
font-weight: 700;
}
.activities .activity .activity-desc {
word-wrap: break-word;
overflow: hidden;
flex: 1;
align-self: center;
margin: 0;
margin-left: 3px;
overflow: hidden;
word-break: break-word;
}
.activities .activity .activity-desc .activity-comment {
display: block;
border-radius: 3px;
background: #fff;
text-decoration: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;
}
.activities .activity .activity-desc .activity-checklist {
display: block;
border-radius: 3px;
background: #fff;
text-decoration: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;
}
.activities .activity .activity-desc .activity-meta {
font-size: 0.8em;
color: #999;
}

View file

@ -1,202 +1,210 @@
template(name="activities") template(name="activities")
if showActivities .activities.js-sidebar-activities
.activities.js-sidebar-activities //- We should use Template.dynamic here but there is a bug with
//- We should use Template.dynamic here but there is a bug with //- blaze-components: https://github.com/peerlibrary/meteor-blaze-components/issues/30
//- blaze-components: https://github.com/peerlibrary/meteor-blaze-components/issues/30 if $eq mode "board"
if $eq mode "board" +boardActivities
+boardActivities else
else +cardActivities
+cardActivities
template(name="boardActivities") template(name="boardActivities")
each activityData in currentBoard.activities each currentBoard.activities
+activity(activity=activityData card=card mode=mode) .activity
+userAvatar(userId=user._id)
p.activity-desc
+memberName(user=user)
if($eq activityType 'deleteAttachment')
| {{{_ 'activity-delete-attach' cardLink}}}.
if($eq activityType 'addAttachment')
| {{{_ 'activity-attached' attachmentLink cardLink}}}.
if($eq activityType 'addBoardMember')
| {{{_ 'activity-added' memberLink boardLabel}}}.
if($eq activityType 'addComment')
| {{{_ 'activity-on' cardLink}}}
a.activity-comment(href="{{ card.absoluteUrl }}")
+viewer
= comment.text
if($eq activityType 'addChecklist')
| {{{_ 'activity-checklist-added' cardLink}}}.
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer
= checklist.title
if($eq activityType 'removeChecklist')
| {{{_ 'activity-checklist-removed' cardLink}}}.
if($eq activityType 'checkedItem')
| {{{_ 'activity-checked-item' checkItem checklist.title cardLink}}}.
if($eq activityType 'uncheckedItem')
| {{{_ 'activity-unchecked-item' checkItem checklist.title cardLink}}}.
if($eq activityType 'checklistCompleted')
| {{{_ 'activity-checklist-completed' checklist.title cardLink}}}.
if($eq activityType 'checklistUncompleted')
| {{{_ 'activity-checklist-uncompleted' checklist.title cardLink}}}.
if($eq activityType 'addChecklistItem')
| {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer
= checklistItem.title
if($eq activityType 'removedChecklistItem')
| {{{_ 'activity-checklist-item-removed' checklist.title cardLink}}}.
if($eq activityType 'archivedCard')
| {{{_ 'activity-archived' cardLink}}}.
if($eq activityType 'archivedList')
| {{_ 'activity-archived' list.title}}.
if($eq activityType 'archivedSwimlane')
| {{_ 'activity-archived' swimlane.title}}.
if($eq activityType 'createBoard')
| {{_ 'activity-created' boardLabel}}.
if($eq activityType 'createCard')
| {{{_ 'activity-added' cardLink boardLabel}}}.
if($eq activityType 'createCustomField')
| {{_ 'activity-customfield-created' customField}}.
if($eq activityType 'createList')
| {{_ 'activity-added' list.title boardLabel}}.
if($eq activityType 'createSwimlane')
| {{_ 'activity-added' swimlane.title boardLabel}}.
if($eq activityType 'removeList')
| {{_ 'activity-removed' title boardLabel}}.
if($eq activityType 'importBoard')
| {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
if($eq activityType 'importCard')
| {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
if($eq activityType 'importList')
| {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
if($eq activityType 'joinMember')
if($eq user._id member._id)
| {{{_ 'activity-joined' cardLink}}}.
else
| {{{_ 'activity-added' memberLink cardLink}}}.
if($eq activityType 'moveCard')
| {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
if($eq activityType 'removeBoardMember')
| {{{_ 'activity-excluded' memberLink boardLabel}}}.
if($eq activityType 'restoredCard')
| {{{_ 'activity-sent' cardLink boardLabel}}}.
if($eq activityType 'addedLabel')
| {{{_ 'activity-added-label' lastLabel cardLink}}}.
if($eq activityType 'removedLabel')
| {{{_ 'activity-removed-label' lastLabel cardLink}}}.
if($eq activityType 'unjoinMember')
if($eq user._id member._id)
| {{{_ 'activity-unjoined' cardLink}}}.
else
| {{{_ 'activity-removed' memberLink cardLink}}}.
span(title=createdAt).activity-meta {{ moment createdAt }}
template(name="cardActivities") template(name="cardActivities")
each activityData in activities each currentCard.activities
+activity(activity=activityData card=card mode=mode) .activity
+userAvatar(userId=user._id)
p.activity-desc
+memberName(user=user)
if($eq activityType 'createCard')
| {{_ 'activity-added' cardLabel list.title}}.
if($eq activityType 'importCard')
| {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
if($eq activityType 'joinMember')
if($eq user._id member._id)
| {{_ 'activity-joined' cardLabel}}.
else
| {{{_ 'activity-added' memberLink cardLabel}}}.
if($eq activityType 'unjoinMember')
if($eq user._id member._id)
| {{_ 'activity-unjoined' cardLabel}}.
else
| {{{_ 'activity-removed' cardLabel memberLink}}}.
if($eq activityType 'archivedCard')
| {{_ 'activity-archived' cardLabel}}.
template(name="activity") if($eq activityType 'addedLabel')
.activity(data-id=activity._id) | {{{_ 'activity-added-label-card' lastLabel }}}.
+userAvatar(userId=activity.user._id)
p.activity-desc
span.activity-member
+memberName(user=activity.user)
//- attachment activity ------------------------------------------------- if($eq activityType 'removedLabel')
if($eq activity.activityType 'deleteAttachment') | {{{_ 'activity-removed-label-card' lastLabel }}}.
| {{{_ 'activity-delete-attach' cardLink}}}.
if($eq activity.activityType 'addAttachment') if($eq activityType 'removeChecklist')
| {{{_ 'activity-attached' attachmentLink cardLink}}}. | {{{_ 'activity-checklist-removed' cardLabel}}}.
if($neq mode 'board')
if activity.attachment.isImage
img.attachment-image-preview(src=activity.attachment.url)
//- board activity ------------------------------------------------------ if($eq activityType 'checkedItem')
if($eq activity.activityType 'createBoard') | {{{_ 'activity-checked-item-card' checkItem checklist.title }}}.
| {{{_ 'activity-created' boardLabelLink}}}.
if($eq activity.activityType 'importBoard') if($eq activityType 'uncheckedItem')
| {{{_ 'activity-imported-board' boardLabelLink sourceLink}}}. | {{{_ 'activity-unchecked-item-card' checkItem checklist.title }}}.
if($eq activity.activityType 'addBoardMember') if($eq activityType 'checklistCompleted')
| {{{_ 'activity-added' memberLink boardLabelLink}}}. | {{{_ 'activity-checklist-completed-card' checklist.title }}}.
if($eq activity.activityType 'removeBoardMember') if($eq activityType 'checklistUncompleted')
| {{{_ 'activity-excluded' memberLink boardLabelLink}}}. | {{{_ 'activity-checklist-uncompleted-card' checklist.title }}}.
//- card activity ------------------------------------------------------- if($eq activityType 'restoredCard')
if($eq activity.activityType 'createCard') | {{_ 'activity-sent' cardLabel boardLabel}}.
if($eq mode 'card') if($eq activityType 'moveCard')
| {{{_ 'activity-added' cardLabelLink (sanitize activity.listName)}}}. | {{_ 'activity-moved' cardLabel oldList.title list.title}}.
else if($eq activityType 'addAttachment')
| {{{_ 'activity-added' cardLabelLink boardLabelLink}}}. | {{{_ 'activity-attached' attachmentLink cardLabel}}}.
if attachment.isImage
if($eq activity.activityType 'importCard') img.attachment-image-preview(src=attachment.url)
| {{{_ 'activity-imported' cardLink boardLabelLink sourceLink}}}. if($eq activityType 'deleteAttachment')
| {{{_ 'activity-delete-attach' cardLabel}}}.
if($eq activity.activityType 'moveCard') if($eq activityType 'removedChecklist')
| {{{_ 'activity-moved' cardLabelLink (sanitize activity.oldList.title) (sanitize activity.list.title)}}}. | {{{_ 'activity-checklist-removed' cardLabel}}}.
if($eq activityType 'addChecklist')
if($eq activity.activityType 'moveCardBoard') | {{{_ 'activity-checklist-added' cardLabel}}}.
| {{{_ 'activity-moved' cardLink (sanitize activity.oldBoardName) (sanitize activity.boardName)}}}.
if($eq activity.activityType 'archivedCard')
| {{{_ 'activity-archived' cardLink}}}.
if($eq activity.activityType 'restoredCard')
| {{{_ 'activity-sent' cardLink boardLabelLink}}}.
//- checklist activity --------------------------------------------------
if($eq activity.activityType 'addChecklist')
| {{{_ 'activity-checklist-added' cardLink}}}.
if($eq mode 'card')
.activity-checklist .activity-checklist
+viewer +viewer
= activity.checklist.title = checklist.title
else if($eq activityType 'addChecklistItem')
a.activity-checklist(href="{{ activity.card.originRelativeUrl }}") | {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer +viewer
= activity.checklist.title = checklistItem.title
if($eq activity.activityType 'removedChecklist') if($eq activityType 'addComment')
| {{{_ 'activity-checklist-removed' cardLink}}}. +inlinedForm(classNames='js-edit-comment')
+editor(autofocus=true)
= comment.text
.edit-controls
button.primary(type="submit") {{_ 'edit'}}
else
.activity-comment
+viewer
= comment.text
span(title=createdAt).activity-meta {{ moment createdAt }}
if ($eq currentUser._id comment.userId)
= ' - '
a.js-open-inlined-form {{_ "edit"}}
= ' - '
a.js-delete-comment {{_ "delete"}}
if($eq activity.activityType 'completeChecklist')
| {{{_ 'activity-checklist-completed' (sanitize activity.checklist.title) cardLink}}}.
if($eq activity.activityType 'uncompleteChecklist')
| {{{_ 'activity-checklist-uncompleted' (sanitize activity.checklist.title) cardLink}}}.
if($eq activity.activityType 'checkedItem')
| {{{_ 'activity-checked-item' (sanitize checkItem) (sanitize activity.checklist.title) cardLink}}}.
if($eq activity.activityType 'uncheckedItem')
| {{{_ 'activity-unchecked-item' (sanitize checkItem) (sanitize activity.checklist.title) cardLink}}}.
if($eq activity.activityType 'addChecklistItem')
| {{{_ 'activity-checklist-item-added' (sanitize activity.checklist.title) cardLink}}}.
.activity-checklist(href="{{ activity.card.originRelativeUrl }}")
+viewer
= activity.checklistItem.title
if($eq activity.activityType 'removedChecklistItem')
| {{{_ 'activity-checklist-item-removed' (sanitize activity.checklist.title) cardLink}}}.
//- comment activity ----------------------------------------------------
if($eq activity.activityType 'deleteComment')
| {{{_ 'activity-deleteComment' activity.commentId}}}.
if($eq activity.activityType 'editComment')
| {{{_ 'activity-editComment' activity.commentId}}}.
if($eq activity.activityType 'addComment')
| {{{_ 'activity-on' cardLink}}}
a.activity-comment(href="{{ activity.card.originRelativeUrl }}")
+viewer
= activity.comment.text
//- date activity ------------------------------------------------
if($eq activity.activityType 'a-receivedAt')
| {{{_ 'activity-receivedDate' (sanitize receivedDate) cardLink}}}.
if($eq activity.activityType 'a-startAt')
| {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
if($eq activity.activityType 'a-dueAt')
| {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
if($eq activity.activityType 'a-endAt')
| {{{_ 'activity-endDate' (sanitize endDate) cardLink}}}.
//- customField activity ------------------------------------------------
if($eq activity.activityType 'createCustomField')
| {{_ 'activity-customfield-created' customField}}.
if($eq activity.activityType 'setCustomField')
| {{{_ 'activity-set-customfield' (sanitize lastCustomField) (sanitize lastCustomFieldValue) cardLink}}}.
if($eq activity.activityType 'unsetCustomField')
| {{{_ 'activity-unset-customfield' (sanitize lastCustomField) cardLink}}}.
//- label activity ------------------------------------------------------
if($eq activity.activityType 'addedLabel')
| {{{_ 'activity-added-label' (sanitize lastLabel) cardLink}}}.
if($eq activity.activityType 'removedLabel')
| {{{_ 'activity-removed-label' (sanitize lastLabel) cardLink}}}.
//- list activity -------------------------------------------------------
if($neq mode 'card')
if($eq activity.activityType 'createList')
| {{{_ 'activity-added' (sanitize listLabel) boardLabelLink}}}.
if($eq activity.activityType 'importList')
| {{{_ 'activity-imported' (sanitize listLabel) boardLabelLink sourceLink}}}.
if($eq activity.activityType 'removeList')
| {{{_ 'activity-removed' (sanitize activity.title) boardLabelLink}}}.
if($eq activity.activityType 'archivedList')
| {{_ 'activity-archived' (sanitize listLabel)}}.
if($eq activity.activityType 'changedListTitle')
| {{_ 'activity-changedListTitle' (sanitize listLabel) boardLabelLink}}
//- member activity ----------------------------------------------------
if($eq activity.activityType 'joinMember')
if($eq user._id activity.member._id)
| {{{_ 'activity-joined' cardLink}}}.
else else
| {{{_ 'activity-added' memberLink cardLink}}}. span(title=createdAt).activity-meta {{ moment createdAt }}
if($eq activity.activityType 'unjoinMember')
if($eq user._id activity.member._id)
| {{{_ 'activity-unjoined' cardLink}}}.
else
| {{{_ 'activity-removed' memberLink cardLink}}}.
//- swimlane activity --------------------------------------------------
if($eq activity.activityType 'createSwimlane')
| {{{_ 'activity-added' (sanitize activity.swimlane.title) boardLabelLink}}}.
if($eq activity.activityType 'archivedSwimlane')
| {{{_ 'activity-archived' (sanitize activity.swimlane.title)}}}.
//- I don't understand this part ----------------------------------------
if(currentData.timeKey)
| {{_ activity.activityType }}
= ' '
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
if (currentData.timeOldValue)
= ' '
| {{{_ "previous_as" }}}
= ' '
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
= ' @'
else if(currentData.timeValue)
| {{_ activity.activityType currentData.timeValue}}
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}

View file

@ -1,230 +1,100 @@
import { ReactiveCache } from '/imports/reactiveCache'; const activitiesPerPage = 20;
import DOMPurify from 'dompurify';
import { TAPi18n } from '/imports/i18n';
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;
// TODO is sidebar always available? E.g. on small screens/mobile devices const sidebar = this.parentComponent(); // XXX for some reason not working
const sidebar = Sidebar; sidebar.callFirstWith(null, 'resetNextPeak');
sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
this.autorun(() => { this.autorun(() => {
let mode = this.data()?.mode; let mode = this.data().mode;
if (mode) { const capitalizedMode = Utils.capitalize(mode);
const capitalizedMode = Utils.capitalize(mode); let thisId, searchId;
let searchId; if (mode === 'linkedcard' || mode === 'linkedboard') {
const showActivities = this.showActivities(); thisId = Session.get('currentCard');
if (mode === 'linkedcard' || mode === 'linkedboard') { searchId = Cards.findOne({_id: thisId}).linkedId;
const currentCard = Utils.getCurrentCard(); mode = mode.replace('linked', '');
searchId = currentCard.linkedId; } else {
mode = mode.replace('linked', ''); thisId = Session.get(`current${capitalizedMode}`);
} else if (mode === 'card') { searchId = thisId;
searchId = Utils.getCurrentCardId();
} else {
searchId = Session.get(`current${capitalizedMode}`);
}
const limit = this.page.get() * activitiesPerPage;
if (searchId === null) return;
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
this.loadNextPageLocked = false;
// TODO the guard can be removed as soon as the TODO above is resolved
if (!sidebar) return;
// If the sibear peak hasn't increased, that mean that there are no more
// activities, and we can stop calling new subscriptions.
// XXX This is hacky! We need to know excatly and reactively how many
// activities there are, we probably want to denormalize this number
// dirrectly into card and board documents.
const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
sidebar.calculateNextPeak();
const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
if (nextPeakBefore === nextPeakAfter) {
sidebar.callFirstWith(null, 'resetNextPeak');
}
});
} }
const limit = this.page.get() * activitiesPerPage;
const user = Meteor.user();
const hideSystem = user ? user.hasHiddenSystemMessages() : false;
if (searchId === null)
return;
this.subscribe('activities', mode, searchId, limit, hideSystem, () => {
this.loadNextPageLocked = false;
// If the sibear peak hasn't increased, that mean that there are no more
// activities, and we can stop calling new subscriptions.
// XXX This is hacky! We need to know excatly and reactively how many
// activities there are, we probably want to denormalize this number
// dirrectly into card and board documents.
const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
sidebar.calculateNextPeak();
const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
if (nextPeakBefore === nextPeakAfter) {
sidebar.callFirstWith(null, 'resetNextPeak');
}
});
}); });
}, },
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;
} }
}, },
showActivities() {
let ret = false;
let mode = this.data()?.mode;
if (mode) {
if (mode === 'linkedcard' || mode === 'linkedboard') {
const currentCard = Utils.getCurrentCard();
ret = currentCard.showActivities ?? false;
} else if (mode === 'card') {
ret = this.data()?.card?.showActivities ?? false;
} else {
ret = Utils.getCurrentBoard().showActivities ?? false;
}
}
return ret;
},
activities() {
const ret = this.data().card.activities();
return ret;
},
}).register('activities');
BlazeComponent.extendComponent({ checkItem(){
checkItem() { const checkItemId = this.currentData().checklistItemId;
const checkItemId = this.currentData().activity.checklistItemId; const checkItem = ChecklistItems.findOne({_id:checkItemId});
const checkItem = ReactiveCache.getChecklistItem(checkItemId); return checkItem.title;
return checkItem && checkItem.title;
}, },
boardLabelLink() { boardLabel() {
const data = this.currentData();
const currentBoardId = Session.get('currentBoard');
if (data.mode !== 'board') {
// data.mode: card, linkedcard, linkedboard
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
}
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
}
return TAPi18n.__('this-board'); return TAPi18n.__('this-board');
}, },
cardLabelLink() { cardLabel() {
const data = this.currentData(); return TAPi18n.__('this-card');
const currentBoardId = Session.get('currentBoard');
if (data.mode == 'card') {
// data.mode: card
return TAPi18n.__('this-card');
}
else if (data.mode !== 'board') {
// data.mode: linkedcard, linkedboard
return createCardLink(data.activity.card(), null);
}
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createCardLink(data.activity.card(), data.activity.board().title);
}
return createCardLink(this.currentData().activity.card(), null);
}, },
cardLink() { cardLink() {
const data = this.currentData(); const card = this.currentData().card();
const currentBoardId = Session.get('currentBoard'); return card && Blaze.toHTML(HTML.A({
if (data.mode !== 'board') { href: card.absoluteUrl(),
// data.mode: card, linkedcard, linkedboard 'class': 'action-card',
return createCardLink(data.activity.card(), null); }, card.title));
}
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createCardLink(data.activity.card(), data.activity.board().title);
}
return createCardLink(this.currentData().activity.card(), null);
}, },
receivedDate() { lastLabel(){
const receivedDate = this.currentData().activity.card(); const lastLabelId = this.currentData().labelId;
if (!receivedDate) return null; const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(lastLabelId);
return receivedDate.receivedAt; if(lastLabel.name === undefined || lastLabel.name === ''){
},
startDate() {
const startDate = this.currentData().activity.card();
if (!startDate) return null;
return startDate.startAt;
},
dueDate() {
const dueDate = this.currentData().activity.card();
if (!dueDate) return null;
return dueDate.dueAt;
},
endDate() {
const endDate = this.currentData().activity.card();
if (!endDate) return null;
return endDate.endAt;
},
lastLabel() {
const lastLabelId = this.currentData().activity.labelId;
if (!lastLabelId) return null;
const lastLabel = ReactiveCache.getBoard(
this.currentData().activity.boardId,
).getLabelById(lastLabelId);
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
return lastLabel.color; return lastLabel.color;
} else if (lastLabel.name !== undefined && lastLabel.name !== '') { }else{
return lastLabel.name; return lastLabel.name;
} else {
return null;
} }
}, },
lastCustomField() {
const lastCustomField = ReactiveCache.getCustomField(
this.currentData().activity.customFieldId,
);
if (!lastCustomField) return null;
return lastCustomField.name;
},
lastCustomFieldValue() {
const lastCustomField = ReactiveCache.getCustomField(
this.currentData().activity.customFieldId,
);
if (!lastCustomField) return null;
const value = this.currentData().activity.value;
if (
lastCustomField.settings.dropdownItems &&
lastCustomField.settings.dropdownItems.length > 0
) {
const dropDownValue = _.find(
lastCustomField.settings.dropdownItems,
item => {
return item._id === value;
},
);
if (dropDownValue) return dropDownValue.name;
}
return value;
},
listLabel() { listLabel() {
const activity = this.currentData().activity; return this.currentData().list().title;
const list = activity.list();
return (list && list.title) || activity.title;
}, },
sourceLink() { sourceLink() {
const source = this.currentData().activity.source; const source = this.currentData().source;
if (source) { if(source) {
if (source.url) { if(source.url) {
return Blaze.toHTML( return Blaze.toHTML(HTML.A({
HTML.A( href: source.url,
{ }, source.system));
href: source.url,
},
DOMPurify.sanitize(source.system, {
ALLOW_UNKNOWN_PROTOCOLS: true,
}),
),
);
} else { } else {
return DOMPurify.sanitize(source.system, { return source.system;
ALLOW_UNKNOWN_PROTOCOLS: true,
});
} }
} }
return null; return null;
@ -232,129 +102,43 @@ BlazeComponent.extendComponent({
memberLink() { memberLink() {
return Blaze.toHTMLWithData(Template.memberName, { return Blaze.toHTMLWithData(Template.memberName, {
user: this.currentData().activity.member(), user: this.currentData().member(),
}); });
}, },
attachmentLink() { attachmentLink() {
const attachment = this.currentData().activity.attachment(); const attachment = this.currentData().attachment();
// trying to display url before file is stored generates js errors // trying to display url before file is stored generates js errors
return ( return attachment && attachment.url({ download: true }) && Blaze.toHTML(HTML.A({
(attachment && href: attachment.url({ download: true }),
attachment.path && target: '_blank',
Blaze.toHTML( }, attachment.name()));
HTML.A(
{
href: `${attachment.link()}?download=true`,
target: '_blank',
},
DOMPurify.sanitize(attachment.name),
),
)) ||
DOMPurify.sanitize(this.currentData().activity.attachmentName)
);
}, },
customField() { customField() {
const customField = this.currentData().activity.customField(); const customField = this.currentData().customField();
if (!customField) return null;
return customField.name; return customField.name;
}, },
}).register('activity'); events() {
return [{
Template.activity.helpers({ // XXX We should use Popup.afterConfirmation here
sanitize(value) { 'click .js-delete-comment'() {
return DOMPurify.sanitize(value, { ALLOW_UNKNOWN_PROTOCOLS: true }); const commentId = this.currentData().commentId;
CardComments.remove(commentId);
},
'submit .js-edit-comment'(evt) {
evt.preventDefault();
const commentText = this.currentComponent().getValue().trim();
const commentId = Template.parentData().commentId;
if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
},
});
}
},
}];
}, },
}); }).register('activities');
Template.commentReactions.events({
'click .reaction'(event) {
if (ReactiveCache.getCurrentUser().isBoardMember()) {
const codepoint = event.currentTarget.dataset['codepoint'];
const commentId = Template.instance().data.commentId;
const cardComment = ReactiveCache.getCardComment(commentId);
cardComment.toggleReaction(codepoint);
}
},
'click .open-comment-reaction-popup': Popup.open('addReaction'),
})
Template.addReactionPopup.events({
'click .add-comment-reaction'(event) {
if (ReactiveCache.getCurrentUser().isBoardMember()) {
const codepoint = event.currentTarget.dataset['codepoint'];
const commentId = Template.instance().data.commentId;
const cardComment = ReactiveCache.getCardComment(commentId);
cardComment.toggleReaction(codepoint);
}
Popup.back();
},
})
Template.addReactionPopup.helpers({
codepoints() {
// Starting set of unicode codepoints as comment reactions
return [
'&#128077;',
'&#128078;',
'&#128064;',
'&#9989;',
'&#10060;',
'&#128591;',
'&#128079;',
'&#127881;',
'&#128640;',
'&#128522;',
'&#129300;',
'&#128532;'];
}
})
Template.commentReactions.helpers({
isSelected(userIds) {
return Meteor.userId() && userIds.includes(Meteor.userId());
},
userNames(userIds) {
const ret = ReactiveCache.getUsers({_id: {$in: userIds}})
.map(user => user.profile.fullname)
.join(', ');
return ret;
}
})
function createCardLink(card, board) {
if (!card) return '';
let text = card.title;
if (board) text = `${board} > ` + text;
return (
card &&
Blaze.toHTML(
HTML.A(
{
href: card.originRelativeUrl(),
class: 'action-card',
},
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
),
)
);
}
function createBoardLink(board, list) {
let text = board.title;
if (list) text += `: ${list}`;
return (
board &&
Blaze.toHTML(
HTML.A(
{
href: board.originRelativeUrl(),
class: 'action-board',
},
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
),
)
);
}

View file

@ -0,0 +1,48 @@
@import 'nib'
.activity-title
margin: 0 0.5em 0.8em
display: flex
justify-content:space-between
.activities
clear: both
.activity
margin: 10px 0
display: flex
.member
width: 24px
height: @width
.activity-desc
word-wrap: break-word
overflow: hidden
flex: 1
align-self: center
margin: 0
margin-left: 3px
overflow: hidden;
word-break: break-word;
.activity-comment
display: block
border-radius: 3px
background: white
text-decoration: none
box-shadow: 0 1px 2px rgba(0,0,0,.2)
margin-top: 5px
padding: 5px
.activity-checklist
display: block
border-radius: 3px
background: white
text-decoration: none
box-shadow: 0 1px 2px rgba(0,0,0,.2)
margin-top: 5px
padding: 5px
.activity-meta
font-size: 0.8em
color: darken(white, 40%)

View file

@ -1,140 +0,0 @@
.new-comment {
position: relative;
margin: 0 0 20px 38px;
}
.new-comment .member {
opacity: 0.7;
position: absolute;
top: 1px;
left: -38px;
}
.new-comment.is-open .member {
opacity: 1;
}
.new-comment.is-open .helper {
display: inline-block;
}
.new-comment.is-open textarea {
min-height: 100px;
color: #4d4d4d;
cursor: auto;
overflow: hidden;
word-wrap: break-word;
}
.new-comment .too-long {
margin-top: 8px;
}
.new-comment textarea {
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
height: 36px;
margin: 4px 4px 6px 0;
padding: 9px 11px;
width: 100%;
}
.new-comment textarea:hover,
.new-comment textarea:is-open {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.33);
border: 0;
cursor: pointer;
}
.new-comment textarea:is-open {
cursor: auto;
}
.comment-item {
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
color: #8c8c8c;
height: 36px;
margin: 4px 4px 6px 0;
width: 92%;
}
.comment-item:hover {
background: #e0e0e0;
}
.comment-item.add-comment {
display: flex;
margin: 5px;
}
.comment-item.add-comment a {
display: block;
margin: auto;
}
.comments {
clear: both;
}
.comments .comment {
margin: 0.5px 0;
padding: 6px 0;
display: flex;
}
.comments .comment .member {
width: 32px;
height: 32px;
}
.comments .comment .comment-member {
font-weight: 700;
}
.comments .comment .comment-desc {
word-wrap: break-word;
overflow: hidden;
flex: 1;
align-self: center;
margin: 0;
margin-left: 3px;
overflow: hidden;
word-break: break-word;
}
.comments .comment .comment-desc .comment-text {
display: block;
border-radius: 3px;
background: #fff;
text-decoration: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;
}
.comments .comment .comment-desc .reactions {
display: flex;
margin-top: 5px;
gap: 5px;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup {
display: flex;
align-items: center;
text-decoration: none;
height: 24px;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
font-size: 17px;
font-weight: 500;
margin-left: 2px;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
font-size: 8px;
margin-top: -7px;
margin-left: 1px;
}
.comments .comment .comment-desc .reactions .reaction {
cursor: pointer;
border: 1px solid #808080;
border-radius: 15px;
display: flex;
padding: 2px 5px;
}
.comments .comment .comment-desc .reactions .reaction.selected {
background-color: #b0c4de;
}
.comments .comment .comment-desc .reactions .reaction:hover {
background-color: #b0c4de;
}
.comments .comment .comment-desc .reactions .reaction .reaction-count {
font-size: 12px;
}
.comments .comment .comment-desc .comment-meta {
font-size: 0.8em;
color: #999;
}

View file

@ -1,65 +1,9 @@
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 noRemove=true) +userAvatar(userId=currentUser._id)
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}}
.add-controls .add-controls
button.primary.confirm.clear.js-add-comment(type="submit") {{_ 'comment'}} button.primary.confirm.clear.js-add-comment(type="submit") {{_ 'comment'}}
template(name="comments")
.comments
each commentData in getComments
+comment(commentData)
template(name="comment")
.comment
+userAvatar(userId=userId)
p.comment-desc
span.comment-member
+memberName(user=user)
+inlinedForm(classNames='js-edit-comment')
+editor(autofocus=true)
= text
.edit-controls
button.primary(type="submit") {{_ 'edit'}}
.fa.fa-times-thin.js-close-inlined-form
else
.comment-text
+viewer
= text
+commentReactions(reactions=reactions commentId=_id)
span(title=createdAt).comment-meta {{ moment createdAt }}
if($eq currentUser._id userId)
+editOrDeleteComment
else if currentUser.isBoardAdmin
+editOrDeleteComment
template(name="editOrDeleteComment")
= ' - '
a.js-open-inlined-form {{_ "edit"}}
= ' - '
a.js-delete-comment {{_ "delete"}}
template(name="deleteCommentPopup")
p {{_ "comment-delete"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="commentReactions")
.reactions
each reaction in reactions
span.reaction(class="{{#if isSelected reaction.userIds}}selected{{/if}}" data-codepoint="#{reaction.reactionCodepoint}" title="{{userNames reaction.userIds}}")
span.reaction-codepoint !{reaction.reactionCodepoint}
span.reaction-count #{reaction.userIds.length}
if (currentUser.isBoardMember)
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
i.fa.fa-smile-o
i.fa.fa-plus
template(name="addReactionPopup")
.reactions-popup
each codepoint in codepoints
span.add-comment-reaction(data-codepoint="#{codepoint}") !{codepoint}

View file

@ -1,11 +1,8 @@
import { ReactiveCache } from '/imports/reactiveCache';
const commentFormIsOpen = new ReactiveVar(false); const commentFormIsOpen = new ReactiveVar(false);
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onDestroyed() { onDestroyed() {
commentFormIsOpen.set(false); commentFormIsOpen.set(false);
$('.note-popover').hide();
}, },
commentFormIsOpen() { commentFormIsOpen() {
@ -17,82 +14,45 @@ BlazeComponent.extendComponent({
}, },
events() { events() {
return [ return [{
{ 'click .js-new-comment:not(.focus)'() {
'submit .js-new-comment-form'(evt) { commentFormIsOpen.set(true);
const input = this.getInput();
const text = input.val().trim();
const card = this.currentData();
let boardId = card.boardId;
let cardId = card._id;
if (card.isLinkedCard()) {
boardId = ReactiveCache.getCard(card.linkedId).boardId;
cardId = card.linkedId;
} else if (card.isLinkedBoard()) {
boardId = card.linkedId;
}
if (text) {
CardComments.insert({
text,
boardId,
cardId,
});
resetCommentInput(input);
Tracker.flush();
autosize.update(input);
input.trigger('submitted');
}
evt.preventDefault();
},
// 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();
}
},
}, },
]; 'submit .js-new-comment-form'(evt) {
const input = this.getInput();
const text = input.val().trim();
const card = this.currentData();
let boardId = card.boardId;
let cardId = card._id;
if (card.isLinkedCard()) {
boardId = Cards.findOne(card.linkedId).boardId;
cardId = card.linkedId;
}
if (text) {
CardComments.insert({
text,
boardId,
cardId,
});
resetCommentInput(input);
Tracker.flush();
autosize.update(input);
}
evt.preventDefault();
},
// 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('commentForm'); }).register('commentForm');
BlazeComponent.extendComponent({
getComments() {
const ret = this.data().comments();
return ret;
},
}).register("comments");
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-delete-comment': Popup.afterConfirm('deleteComment', () => {
const commentId = this.data()._id;
CardComments.remove(commentId);
Popup.back();
}),
'submit .js-edit-comment'(evt) {
evt.preventDefault();
const commentText = this.currentComponent()
.getValue()
.trim();
const commentId = this.data()._id;
if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
},
});
}
},
},
];
},
}).register("comment");
// XXX This should be a static method of the `commentForm` component // XXX This should be a static method of the `commentForm` component
function resetCommentInput(input) { function resetCommentInput(input) {
input.val(''); // without manually trigger, input event won't be fired input.val('');
input.blur(); input.blur();
commentFormIsOpen.set(false); commentFormIsOpen.set(false);
} }
@ -103,18 +63,17 @@ function resetCommentInput(input) {
// Tracker.autorun to register the component dependencies, and re-run when these // Tracker.autorun to register the component dependencies, and re-run when these
// dependencies are invalidated. A better component API would remove this hack. // dependencies are invalidated. A better component API would remove this hack.
Tracker.autorun(() => { Tracker.autorun(() => {
Utils.getCurrentCardId(); Session.get('currentCard');
Tracker.afterFlush(() => { Tracker.afterFlush(() => {
autosize.update($('.js-new-comment-input')); autosize.update($('.js-new-comment-input'));
}); });
}); });
EscapeActions.register( EscapeActions.register('inlinedForm',
'inlinedForm',
() => { () => {
const draftKey = { const draftKey = {
fieldName: 'cardComment', fieldName: 'cardComment',
docId: Utils.getCurrentCardId(), docId: Session.get('currentCard'),
}; };
const commentInput = $('.js-new-comment-input'); const commentInput = $('.js-new-comment-input');
const draft = commentInput.val().trim(); const draft = commentInput.val().trim();
@ -125,10 +84,7 @@ EscapeActions.register(
} }
resetCommentInput(commentInput); resetCommentInput(commentInput);
}, },
() => { () => { return commentFormIsOpen.get(); }, {
return commentFormIsOpen.get();
},
{
noClickEscapeOn: '.js-new-comment', noClickEscapeOn: '.js-new-comment',
}, }
); );

View file

@ -0,0 +1,48 @@
@import 'nib'
.new-comment
position: relative
margin: 0 0 20px 38px
.member
opacity: .7
position: absolute
top: 1px
left: -38px
&.is-open
.member
opacity: 1
.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)
color: #8c8c8c
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

View file

@ -14,7 +14,6 @@ template(name="archivedBoards")
i.fa.fa-undo i.fa.fa-undo
| {{_ 'restore-board'}} | {{_ 'restore-board'}}
= title = title
span {{ moment archivedAt 'LLL' }}
else else
li.no-items-message {{_ 'no-archived-boards'}} li.no-items-message {{_ 'no-archived-boards'}}

View file

@ -1,55 +1,45 @@
import { ReactiveCache } from '/imports/reactiveCache'; Template.boardListHeaderBar.events({
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
});
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { onCreated() {
this.subscribe('archivedBoards'); this.subscribe('archivedBoards');
}, },
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
archivedBoards() { archivedBoards() {
const ret = ReactiveCache.getBoards( return Boards.find({ archived: true }, {
{ archived: true }, sort: ['title'],
{ });
sort: { archivedAt: -1, modifiedAt: -1 },
},
);
return ret;
}, },
events() { events() {
return [ return [{
{ 'click .js-restore-board'() {
'click .js-restore-board'() { // TODO : Make isSandstorm variable global
// TODO : Make isSandstorm variable global const isSandstorm = Meteor.settings && Meteor.settings.public &&
const isSandstorm = Meteor.settings.public.sandstorm;
Meteor.settings && if (isSandstorm && Session.get('currentBoard')) {
Meteor.settings.public && const currentBoard = Boards.findOne(Session.get('currentBoard'));
Meteor.settings.public.sandstorm; currentBoard.archive();
if (isSandstorm && Utils.getCurrentBoardId()) { }
const currentBoard = Utils.getCurrentBoard(); const board = this.currentData();
currentBoard.archive(); board.restore();
} Utils.goBoardId(board._id);
const board = this.currentData();
board.restore();
Utils.goBoardId(board._id);
},
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
Popup.back();
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
Boards.remove(currentBoard._id);
}
Boards.remove(this._id);
FlowRouter.go('home');
}),
}, },
]; 'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
Popup.close();
const isSandstorm = Meteor.settings && Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Session.get('currentBoard')) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
Boards.remove(currentBoard._id);
}
Boards.remove(this._id);
FlowRouter.go('home');
}),
}];
}, },
}).register('archivedBoards'); }).register('archivedBoards');

View file

@ -1,221 +0,0 @@
.board-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: hidden;
}
.board-wrapper .board-canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
transition: margin 0.1s;
overflow-y: auto;
}
.board-wrapper .board-canvas .board-overlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
top: -100px;
right: -400px;
background: #000;
opacity: 0.33;
animation: fadeIn 0.2s;
z-index: 16;
}
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
display: none;
}
@media screen and (max-width: 800px) {
.board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc;
display: flex;
flex-direction: column;
margin: 0;
padding: 0 0px 0px 0;
overflow-x: hidden;
overflow-y: auto;
}
}
.calendar-event-green {
background: #3cb500 !important;
border-color: #2a8000;
color: #fff !important;
}
.calendar-event-yellow {
background: #fad900 !important;
border-color: #c7ac00;
color: #000 !important;
}
.calendar-event-orange {
background: #ff9f19 !important;
border-color: #cc7c14;
color: #000 !important;
}
.calendar-event-red {
background: #eb4646 !important;
border-color: #b83737;
color: #fff !important;
}
.calendar-event-purple {
background: #a632db !important;
border-color: #7d26a6;
color: #fff !important;
}
.calendar-event-blue {
background: #0079bf !important;
border-color: #005a8a;
color: #fff !important;
}
.calendar-event-pink {
background: #ff78cb !important;
border-color: #cc62a3;
color: #000 !important;
}
.calendar-event-sky {
background: #00c2e0 !important;
border-color: #0094ab;
color: #fff !important;
}
.calendar-event-black {
background: #4d4d4d !important;
border-color: #1a1a1a;
color: #fff !important;
}
.calendar-event-lime {
background: #51e898 !important;
border-color: #3eb375;
color: #000 !important;
}
.calendar-event-silver {
background: #c0c0c0 !important;
border-color: #8c8c8c;
color: #000 !important;
}
.calendar-event-peachpuff {
background: #ffdab9 !important;
border-color: #ccaf95;
color: #000 !important;
}
.calendar-event-crimson {
background: #dc143c !important;
border-color: #a8112f;
color: #fff !important;
}
.calendar-event-plum {
background: #dda0dd !important;
border-color: #a87ba8;
color: #000 !important;
}
.calendar-event-darkgreen {
background: #006400 !important;
border-color: #003000;
color: #fff !important;
}
.calendar-event-slateblue {
background: #6a5acd !important;
border-color: #4f4399;
color: #fff !important;
}
.calendar-event-magenta {
background: #f0f !important;
border-color: #c0c;
color: #fff !important;
}
.calendar-event-gold {
background: #ffd700 !important;
border-color: #ca0;
color: #000 !important;
}
.calendar-event-navy {
background: #000080 !important;
border-color: #003;
color: #fff !important;
}
.calendar-event-gray {
background: #808080 !important;
border-color: #333;
color: #fff !important;
}
.calendar-event-saddlebrown {
background: #8b4513 !important;
border-color: #572b0c;
color: #fff !important;
}
.calendar-event-paleturquoise {
background: #afeeee !important;
border-color: #8ababa;
color: #000 !important;
}
.calendar-event-mistyrose {
background: #ffe4e1 !important;
border-color: #ccb8b6;
color: #000 !important;
}
.calendar-event-indigo {
background: #4b0082 !important;
border-color: #2b004d;
color: #fff !important;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
background-color: rgba(0, 0, 0, 0.5);
width: 100%;
height: 100%;
}
.modal-dialog {
display: flex;
justify-content: center;
align-items: center;
height: 25%; /* Adjust the height to make it smaller */
position: relative;
margin: 10% auto; /* This margin will help center the modal vertically */
max-width: 400px; /* Adjust the max-width to make it smaller */
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 1px;
border-bottom: 1px solid #ccc;
}
.modal-title {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-size: 18px;
}
.modal-footer {
display: flex;
justify-content: center;
align-items: center;
padding-top: 4px;
border-top: 1px solid #ccc;
}
.close {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 5px;
right: 5px;
font-size: 25px;
cursor: pointer;
}

View file

@ -6,47 +6,30 @@ template(name="board")
else else
+boardBody +boardBody
else else
//-- XXX We need a better error message in case the board has been archived //- XXX We need a better error message in case the board has been archived
+message(label="board-not-found") +message(label="board-not-found")
//-- | {{goHome}}
else else
+spinner +spinner
template(name="boardBody") template(name="boardBody")
if notDisplayThisBoard .board-wrapper(class=currentBoard.colorClass)
| {{_ 'tableVisibilityMode-allowPrivateOnly'}} +sidebar
else .board-canvas.js-swimlanes.js-perfect-scrollbar(
.board-wrapper(class=currentBoard.colorClass) class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
.board-canvas.js-swimlanes( class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
class="{{#if hasSwimlanes}}dragscroll{{/if}}" class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}" if showOverlay.get
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}" .board-overlay
class="{{#if draggingActive.get}}is-dragging-active{{/if}}" if isViewSwimlanes
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}") each currentBoard.swimlanes
if showOverlay.get +swimlane(this)
.board-overlay if isViewLists
if currentBoard.isTemplatesBoard +listsGroup
each currentBoard.swimlanes if isViewCalendar
+swimlane(this) +calendarView
else if isViewSwimlanes
if hasSwimlanes
each currentBoard.swimlanes
+swimlane(this)
else
a.js-empty-board-add-swimlane(title="{{_ 'add-swimlane'}}")
h1.big-message.quiet
| {{_ 'add-swimlane'}} +
else if isViewLists
+listsGroup(currentBoard)
else if isViewCalendar
+calendarView
else
+listsGroup(currentBoard)
+sidebar
template(name="calendarView") template(name="calendarView")
if isViewCalendar .calendar-view.swimlane
.calendar-view.swimlane if currentCard
if currentCard +cardDetails(currentCard)
+cardDetails(currentCard) +fullcalendar(calendarOptions)
+fullcalendar(calendarOptions)

View file

@ -1,10 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
const subManager = new SubsManager(); const subManager = new SubsManager();
const { calculateIndex } = Utils; const { calculateIndex, enableClickOnTouch } = Utils;
const swimlaneWhileSortingHeight = 150;
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { onCreated() {
@ -16,8 +11,9 @@ BlazeComponent.extendComponent({
// unfortunatly, Blaze doesn't have this notion. // unfortunatly, Blaze doesn't have this notion.
this.autorun(() => { this.autorun(() => {
const currentBoardId = Session.get('currentBoard'); const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return; if (!currentBoardId)
const handle = subManager.subscribe('board', currentBoardId, false); return;
const handle = subManager.subscribe('board', currentBoardId);
Tracker.nonreactive(() => { Tracker.nonreactive(() => {
Tracker.autorun(() => { Tracker.autorun(() => {
this.isBoardReady.set(handle.ready()); this.isBoardReady.set(handle.ready());
@ -27,53 +23,18 @@ BlazeComponent.extendComponent({
}, },
onlyShowCurrentCard() { onlyShowCurrentCard() {
return Utils.isMiniScreen() && Utils.getCurrentCardId(true); return Utils.isMiniScreen() && Session.get('currentCard');
}, },
goHome() {
FlowRouter.go('home');
},
}).register('board'); }).register('board');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { onCreated() {
Meteor.subscribe('tableVisibilityModeSettings');
this.showOverlay = new ReactiveVar(false); this.showOverlay = new ReactiveVar(false);
this.draggingActive = new ReactiveVar(false); this.draggingActive = new ReactiveVar(false);
this._isDragging = false; this._isDragging = false;
// Used to set the overlay // Used to set the overlay
this.mouseHasEnterCardDetails = false; this.mouseHasEnterCardDetails = false;
// fix swimlanes sort field if there are null values
const currentBoardData = Utils.getCurrentBoard();
const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
if (nullSortSwimlanes.length > 0) {
const swimlanes = currentBoardData.swimlanes();
let count = 0;
swimlanes.forEach(s => {
Swimlanes.update(s._id, {
$set: {
sort: count,
},
});
count += 1;
});
}
// fix lists sort field if there are null values
const nullSortLists = currentBoardData.nullSortLists();
if (nullSortLists.length > 0) {
const lists = currentBoardData.lists();
let count = 0;
lists.forEach(l => {
Lists.update(l._id, {
$set: {
sort: count,
},
});
count += 1;
});
}
}, },
onRendered() { onRendered() {
const boardComponent = this; const boardComponent = this;
@ -82,67 +43,21 @@ BlazeComponent.extendComponent({
$swimlanesDom.sortable({ $swimlanesDom.sortable({
tolerance: 'pointer', tolerance: 'pointer',
appendTo: '.board-canvas', appendTo: '.board-canvas',
helper(evt, item) { helper: 'clone',
const helper = $(`<div class="swimlane" handle: '.js-swimlane-header',
style="flex-direction: column; items: '.js-swimlane:not(.placeholder)',
height: ${swimlaneWhileSortingHeight}px;
width: $(boardComponent.width)px;
overflow: hidden;"/>`);
helper.append(item.clone());
// Also grab the list of lists of cards
const list = item.next();
helper.append(list.clone());
return helper;
},
items: '.swimlane:not(.placeholder)',
placeholder: 'swimlane placeholder', placeholder: 'swimlane placeholder',
distance: 7, distance: 7,
start(evt, ui) { start(evt, ui) {
const listDom = ui.placeholder.next('.js-swimlane');
const parentOffset = ui.item.parent().offset();
ui.placeholder.height(ui.helper.height()); ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close'); EscapeActions.executeUpTo('popup-close');
listDom.addClass('moving-swimlane');
boardComponent.setIsDragging(true); boardComponent.setIsDragging(true);
ui.placeholder.insertAfter(ui.placeholder.next());
boardComponent.origPlaceholderIndex = ui.placeholder.index();
// resize all swimlanes + headers to be a total of 150 px per row
// this could be achieved by setIsDragging(true) but we want immediate
// result
ui.item
.siblings('.js-swimlane')
.css('height', `${swimlaneWhileSortingHeight - 26}px`);
// set the new scroll height after the resize and insertion of
// the placeholder. We want the element under the cursor to stay
// at the same place on the screen
ui.item.parent().get(0).scrollTop =
ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
},
beforeStop(evt, ui) {
const parentOffset = ui.item.parent().offset();
const siblings = ui.item.siblings('.js-swimlane');
siblings.css('height', '');
// compute the new scroll height after the resize and removal of
// the placeholder
const scrollTop =
ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
// then reset the original view of the swimlane
siblings.removeClass('moving-swimlane');
// and apply the computed scrollheight
ui.item.parent().get(0).scrollTop = scrollTop;
}, },
stop(evt, ui) { stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element // To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any. // of the previous and the following card -- if any.
const prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0); const prevSwimlaneDom = ui.item.prev('.js-swimlane').get(0);
const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0); const nextSwimlaneDom = ui.item.next('.js-swimlane').get(0);
const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1); const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
$swimlanesDom.sortable('cancel'); $swimlanesDom.sortable('cancel');
@ -157,149 +72,65 @@ BlazeComponent.extendComponent({
boardComponent.setIsDragging(false); boardComponent.setIsDragging(false);
}, },
sort(evt, ui) {
// get the mouse position in the sortable
const parentOffset = ui.item.parent().offset();
const cursorY =
evt.pageY - parentOffset.top + ui.item.parent().scrollTop();
// compute the intended index of the placeholder (we need to skip the
// slots between the headers and the list of cards)
const newplaceholderIndex = Math.floor(
cursorY / swimlaneWhileSortingHeight,
);
let destPlaceholderIndex = (newplaceholderIndex + 1) * 2;
// if we are scrolling far away from the bottom of the list
if (destPlaceholderIndex >= ui.item.parent().get(0).childElementCount) {
destPlaceholderIndex = ui.item.parent().get(0).childElementCount - 1;
}
// update the placeholder position in the DOM tree
if (destPlaceholderIndex !== ui.placeholder.index()) {
if (destPlaceholderIndex < boardComponent.origPlaceholderIndex) {
ui.placeholder.insertBefore(
ui.placeholder
.siblings()
.slice(destPlaceholderIndex - 2, destPlaceholderIndex - 1),
);
} else {
ui.placeholder.insertAfter(
ui.placeholder
.siblings()
.slice(destPlaceholderIndex - 1, destPlaceholderIndex),
);
}
}
},
}); });
this.autorun(() => { // ugly touch event hotfix
// Always reset dragscroll on view switch enableClickOnTouch('.js-swimlane:not(.placeholder)');
dragscroll.reset();
if (Utils.isTouchScreenOrShowDesktopDragHandles()) { function userIsMember() {
$swimlanesDom.sortable({ return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
handle: '.js-swimlane-header-handle', }
});
} else {
$swimlanesDom.sortable({
handle: '.swimlane-header',
});
}
// Disable drag-dropping if the current user is not a board member
$swimlanesDom.sortable(
'option',
'disabled',
!ReactiveCache.getCurrentUser()?.isBoardAdmin(),
);
});
// If there is no data in the board (ie, no lists) we autofocus the list // If there is no data in the board (ie, no lists) we autofocus the list
// creation form by clicking on the corresponding element. // creation form by clicking on the corresponding element.
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
if (Utils.canModifyBoard() && currentBoard.lists().length === 0) { if (userIsMember() && currentBoard.lists().count() === 0) {
boardComponent.openNewListForm(); boardComponent.openNewListForm();
} }
dragscroll.reset();
Utils.setBackgroundImage();
},
notDisplayThisBoard() {
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
let currentBoard = Utils.getCurrentBoard();
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue && currentBoard.permission == 'public') {
return true;
}
return false;
}, },
isViewSwimlanes() { isViewSwimlanes() {
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = Meteor.user();
if (currentUser) { if (!currentUser) return false;
return (currentUser.profile || {}).boardView === 'board-view-swimlanes'; return (currentUser.profile.boardView === 'board-view-swimlanes');
} else {
return (
window.localStorage.getItem('boardView') === 'board-view-swimlanes'
);
}
},
hasSwimlanes() {
return Utils.getCurrentBoard().swimlanes().length > 0;
}, },
isViewLists() { isViewLists() {
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = Meteor.user();
if (currentUser) { if (!currentUser) return true;
return (currentUser.profile || {}).boardView === 'board-view-lists'; return (currentUser.profile.boardView === 'board-view-lists');
} else {
return window.localStorage.getItem('boardView') === 'board-view-lists';
}
}, },
isViewCalendar() { isViewCalendar() {
const currentUser = ReactiveCache.getCurrentUser(); const currentUser = Meteor.user();
if (currentUser) { if (!currentUser) return true;
return (currentUser.profile || {}).boardView === 'board-view-cal'; return (currentUser.profile.boardView === 'board-view-cal');
} else {
return window.localStorage.getItem('boardView') === 'board-view-cal';
}
},
isVerticalScrollbars() {
const user = ReactiveCache.getCurrentUser();
return user && user.isVerticalScrollbars();
}, },
openNewListForm() { openNewListForm() {
if (this.isViewSwimlanes()) { if (this.isViewSwimlanes()) {
// The form had been removed in 416b17062e57f215206e93a85b02ef9eb1ab4902 this.childComponents('swimlane')[0]
// this.childComponents('swimlane')[0] .childComponents('addListAndSwimlaneForm')[0].open();
// .childComponents('addListAndSwimlaneForm')[0]
// .open();
} else if (this.isViewLists()) { } else if (this.isViewLists()) {
this.childComponents('listsGroup')[0] this.childComponents('listsGroup')[0]
.childComponents('addListForm')[0] .childComponents('addListForm')[0].open();
.open();
} }
}, },
events() { events() {
return [ return [{
{ // XXX The board-overlay div should probably be moved to the parent
// XXX The board-overlay div should probably be moved to the parent // component.
// component. 'mouseenter .board-overlay'() {
mouseup() { if (this.mouseHasEnterCardDetails) {
if (this._isDragging) { this.showOverlay.set(false);
this._isDragging = false; }
}
},
'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'),
}, },
]; 'mouseup'() {
if (this._isDragging) {
this._isDragging = false;
}
},
}];
}, },
// XXX Flow components allow us to avoid creating these two setter methods by // XXX Flow components allow us to avoid creating these two setter methods by
@ -311,39 +142,35 @@ BlazeComponent.extendComponent({
scrollLeft(position = 0) { scrollLeft(position = 0) {
const swimlanes = this.$('.js-swimlanes'); const swimlanes = this.$('.js-swimlanes');
swimlanes && swimlanes && swimlanes.animate({
swimlanes.animate({ scrollLeft: position,
scrollLeft: position, });
});
}, },
scrollTop(position = 0) { scrollTop(position = 0) {
const swimlanes = this.$('.js-swimlanes'); const swimlanes = this.$('.js-swimlanes');
swimlanes && swimlanes && swimlanes.animate({
swimlanes.animate({ scrollTop: position,
scrollTop: position, });
});
}, },
}).register('boardBody'); }).register('boardBody');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onRendered() { onRendered() {
this.autorun(function () { this.autorun(function(){
$('#calendar-view').fullCalendar('refetchEvents'); $('#calendar-view').fullCalendar('refetchEvents');
}); });
}, },
calendarOptions() { calendarOptions() {
return { return {
id: 'calendar-view', id: 'calendar-view',
defaultView: 'month', defaultView: 'agendaDay',
editable: true, editable: true,
selectable: true,
timezone: 'local', timezone: 'local',
weekNumbers: true,
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
@ -353,59 +180,33 @@ BlazeComponent.extendComponent({
nowIndicator: true, nowIndicator: true,
businessHours: { businessHours: {
// days of week. an array of zero-based day of week integers (0=Sunday) // days of week. an array of zero-based day of week integers (0=Sunday)
dow: [1, 2, 3, 4, 5], // Monday - Friday dow: [ 1, 2, 3, 4, 5 ], // Monday - Friday
start: '8:00', start: '8:00',
end: '18:00', end: '18:00',
}, },
locale: TAPi18n.getLanguage(), locale: TAPi18n.getLanguage(),
events(start, end, timezone, callback) { events(start, end, timezone, callback) {
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
const events = []; const events = [];
const pushEvent = function (card, title, start, end, extraCls) { currentBoard.cardsInInterval(start.toDate(), end.toDate()).forEach(function(card){
start = start || card.startAt;
end = end || card.endAt;
title = title || card.title;
const className =
(extraCls ? `${extraCls} ` : '') +
(card.color ? `calendar-event-${card.color}` : '');
events.push({ events.push({
id: card._id, id: card._id,
title, title: card.title,
start, start: card.startAt,
end: end || card.endAt, end: card.endAt,
allDay: allDay: Math.abs(card.endAt.getTime() - card.startAt.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,
}), }),
className,
}); });
};
currentBoard
.cardsInInterval(start.toDate(), end.toDate())
.forEach(function (card) {
pushEvent(card);
});
currentBoard
.cardsDueInBetween(start.toDate(), end.toDate())
.forEach(function (card) {
pushEvent(
card,
`${card.title} ${TAPi18n.__('card-due')}`,
card.dueAt,
new Date(card.dueAt.getTime() + 36e5),
);
});
events.sort(function (first, second) {
return first.id > second.id ? 1 : -1;
}); });
callback(events); callback(events);
}, },
eventResize(event, delta, revertFunc) { eventResize(event, delta, revertFunc) {
let isOk = false; let isOk = false;
const card = ReactiveCache.getCard(event.id); const card = Cards.findOne(event.id);
if (card) { if (card) {
card.setEnd(event.end.toDate()); card.setEnd(event.end.toDate());
@ -417,14 +218,12 @@ BlazeComponent.extendComponent({
}, },
eventDrop(event, delta, revertFunc) { eventDrop(event, delta, revertFunc) {
let isOk = false; let isOk = false;
const card = ReactiveCache.getCard(event.id); const card = Cards.findOne(event.id);
if (card) { if (card) {
// TODO: add a flag for allDay events // TODO: add a flag for allDay events
if (!event.allDay) { if (!event.allDay) {
// https://github.com/wekan/wekan/issues/2917#issuecomment-1236753962 card.setStart(event.start.toDate());
//card.setStart(event.start.toDate()); card.setEnd(event.end.toDate());
//card.setEnd(event.end.toDate());
card.setDue(event.start.toDate());
isOk = true; isOk = true;
} }
} }
@ -432,66 +231,6 @@ BlazeComponent.extendComponent({
revertFunc(); revertFunc();
} }
}, },
select: function (startDate) {
const currentBoard = Utils.getCurrentBoard();
const currentUser = ReactiveCache.getCurrentUser();
const modalElement = document.createElement('div');
modalElement.classList.add('modal', 'fade');
modalElement.setAttribute('tabindex', '-1');
modalElement.setAttribute('role', 'dialog');
modalElement.innerHTML = `
<div class="modal-dialog justify-content-center align-items-center" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${TAPi18n.__('r-create-card')}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body text-center">
<input type="text" class="form-control" id="card-title-input" placeholder="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="create-card-button">${TAPi18n.__('add-card')}</button>
</div>
</div>
</div>
`;
const createCardButton = modalElement.querySelector('#create-card-button');
createCardButton.addEventListener('click', function () {
const myTitle = modalElement.querySelector('#card-title-input').value;
if (myTitle) {
const firstList = currentBoard.draggableLists()[0];
const firstSwimlane = currentBoard.swimlanes()[0];
Meteor.call('createCardWithDueDate', currentBoard._id, firstList._id, myTitle, startDate.toDate(), firstSwimlane._id, function(error, result) {
if (error) {
console.log(error);
} else {
console.log("Card Created", result);
}
});
closeModal();
}
});
document.body.appendChild(modalElement);
const openModal = function() {
modalElement.style.display = 'flex';
};
const closeModal = function() {
modalElement.style.display = 'none';
};
const closeButton = modalElement.querySelector('[data-dismiss="modal"]');
closeButton.addEventListener('click', closeModal);
openModal();
}
}; };
}, },
isViewCalendar() {
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
return (currentUser.profile || {}).boardView === 'board-view-cal';
} else {
return window.localStorage.getItem('boardView') === 'board-view-cal';
}
},
}).register('calendarView'); }).register('calendarView');

View file

@ -0,0 +1,55 @@
@import 'nib'
position()
if arguments[0] == cover || arguments[0] == fixed-cover
if arguments[0] == cover
position: absolute
else
position: fixed
left: 0
right: 0
top: 0
bottom: 0
else
position: arguments
.board-wrapper
position: cover
overflow-x: hidden
overflow-y: hidden
.board-canvas
position: cover
transition: margin .1s
overflow-y: auto
&.is-sibling-sidebar-open
margin-right: 248px
.board-overlay
position: fixed-cover
top: -100px
right: -400px
background: black
opacity: 0.33
animation: fadeIn 0.2s
z-index: 16
&.is-dragging-active
.open-minicard-composer,
.minicard-wrapper.is-checked
display: none
@media screen and (max-width: 800px)
.board-wrapper
.board-canvas
.swimlane
border-bottom: 1px solid #CCC
display: flex
flex-direction: column
margin: 0
padding: 0 40px 0px 0
overflow-x: hidden
overflow-y: auto

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
// We define a set of six board colors that we took from the FlatUI palette.
// http://flatuicolors.com
//
// XXX Centralizing all these properties in a single file just because their
// value is derived from the same color, doesn't make any sense. We should
// create a mixin/macro that would generate 6 versions of a given property and
// dispatch this list in the other stylus files.
setBoardColor(color)
&#header,
&.sk-spinner div,
.board-backgrounds-list &.background-box,
.board-list & a
background-color: color
.is-selected .minicard
border-left: 3px solid color
button[type=submit].primary, input[type=submit].primary
background-color: darken(color, 20%)
&.pop-over .pop-over-list li a:not(.disabled):hover,
.sidebar .sidebar-content .sidebar-btn:hover,
.sidebar-list li a:hover
background-color: lighten(color, 10%)
&#header ul li.current, &#header-quick-access ul li.current
border-bottom: 2px solid lighten(color, 10%)
&#header-quick-access
background: darken(color, 10%)
color: white
&#header #header-main-bar .board-header-btn.emphasis
background: complement(color)
&:hover,
.board-header-btn-close
background: darken(complement(color), 10%)
&:hover .board-header-btn-close
background: darken(complement(color), 20%)
.materialCheckBox.is-checked
border-bottom: 2px solid color
border-right: 2px solid color
.is-multiselection-active .multi-selection-checkbox
&.is-checked + .minicard
background: lighten(color, 90%)
&:not(.is-checked) + .minicard:hover:not(.minicard-composer)
background: lighten(color, 97%)
.toggle-label
&:after
background-color: darken(color, 20%)
.toggle-switch:checked ~ .toggle-label
background-color: lighten(color, 20%)
&:after
background-color: darken(color, 20%)
@media screen and (max-width: 800px)
&.pop-over .header
background: color
color: white
&#header ul li.current, &#header-quick-access ul li.current
border-bottom: 4px solid lighten(color, 20%)
.board-color-nephritis
setBoardColor(#27AE60)
.board-color-pomegranate
setBoardColor(#C0392B)
.board-color-belize
setBoardColor(#2980B9)
.board-color-wisteria
setBoardColor(#8E44AD)
.board-color-midnight
setBoardColor(#2C3E50)
.board-color-pumpkin
setBoardColor(#E67E22)

View file

@ -1,23 +0,0 @@
.integration-form {
padding: 5px;
border-bottom: 1px solid #ccc;
}
.flex,
.option {
display: -webkit-box;
display: -moz-box;
display: -webkit-flex;
display: -moz-flex;
display: -ms-flexbox;
display: flex;
}
.option {
-webkit-border-radius: 3px;
border-radius: 3px;
background: #fff;
text-decoration: none;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;
}

View file

@ -1,98 +1,77 @@
template(name="boardHeaderBar") template(name="boardHeaderBar")
h1.header-board-menu h1.header-board-menu
with currentBoard with currentBoard
if $eq title 'Templates' a(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}")
| {{_ 'templates'}}
else
+viewer +viewer
= title = title
.board-header-btns.left .board-header-btns.left
unless isMiniScreen unless isMiniScreen
if currentBoard unless isSandstorm
if currentUser if currentBoard
with currentBoard if currentUser
if currentUser.isBoardAdmin a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title) title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa.fa-pencil-square-o i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span
= currentBoard.stars
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" a.board-header-btn(
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") title="{{_ currentBoard.permission}}")
if showStarCounter i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span span {{_ currentBoard.permission}}
= currentBoard.stars
a.board-header-btn( a.board-header-btn.js-watch-board(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" title="{{_ watchLevel }}")
title="{{_ currentBoard.permission}}") if $eq watchLevel "watching"
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") i.fa.fa-eye
span {{_ currentBoard.permission}} if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn.js-watch-board( else
title="{{_ watchLevel }}") a.board-header-btn.js-log-in(
if $eq watchLevel "watching" title="{{_ 'log-in'}}")
i.fa.fa-eye i.fa.fa-sign-in
if $eq watchLevel "tracking" span {{_ 'log-in'}}
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}
.board-header-btns.right .board-header-btns.right
if currentBoard if currentBoard
if isMiniScreen if isMiniScreen
if currentUser unless isSandstorm
with currentBoard if currentUser
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title) a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
i.fa.fa-pencil-square-o title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span
= currentBoard.stars
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" a.board-header-btn(
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}") class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") title="{{_ currentBoard.permission}}")
if showStarCounter i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span span {{_ currentBoard.permission}}
= currentBoard.stars
a.board-header-btn( a.board-header-btn.js-watch-board(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}" title="{{_ watchLevel }}")
title="{{_ currentBoard.permission}}") if $eq watchLevel "watching"
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}") i.fa.fa-eye
span {{_ currentBoard.permission}} if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn.js-watch-board( else
title="{{_ watchLevel }}") a.board-header-btn.js-log-in(
if $eq watchLevel "watching" title="{{_ 'log-in'}}")
i.fa.fa-eye i.fa.fa-sign-in
if $eq watchLevel "tracking" span {{_ 'log-in'}}
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}
if isSandstorm if isSandstorm
if currentUser if currentUser
@ -100,11 +79,6 @@ template(name="boardHeaderBar")
i.fa.fa-archive i.fa.fa-archive
span {{_ 'archives'}} span {{_ 'archives'}}
//if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
// i.fa(class="{{directionClass}}")
// span {{_ 'sort'}}{{_ listSortShortDesc}}
a.board-header-btn.js-open-filter-view( a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}" title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}emphasis{{/if}}") class="{{#if Filter.isActive}}emphasis{{/if}}")
@ -113,22 +87,19 @@ template(name="boardHeaderBar")
if Filter.isActive if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}") a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin i.fa.fa-times-thin
if currentUser.isAdmin
a.board-header-btn.js-open-rules-view(title="{{_ 'rules'}}")
i.fa.fa-magic
span {{_ 'rules'}}
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}") a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search i.fa.fa-search
span {{_ 'search'}} span {{_ 'search'}}
unless currentBoard.isTemplatesBoard 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-th-large
i.fa.fa-caret-down span {{_ currentUser.profile.boardView}}
if $eq boardView 'board-view-swimlanes'
i.fa.fa-th-large
if $eq boardView 'board-view-lists'
i.fa.fa-trello
if $eq boardView 'board-view-cal'
i.fa.fa-calendar
span {{#if boardView}}{{_ boardView}}{{else}}{{_ 'board-view-swimlanes'}}{{/if}}
if canModifyBoard if canModifyBoard
a.board-header-btn.js-multiselection-activate( a.board-header-btn.js-multiselection-activate(
@ -141,8 +112,40 @@ template(name="boardHeaderBar")
i.fa.fa-times-thin i.fa.fa-times-thin
.separator .separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}") a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
i.fa.fa-navicon i.board-header-btn-icon.fa.fa-navicon
template(name="boardMenuPopup")
ul.pop-over-list
li: a.js-custom-fields {{_ 'custom-fields'}}
li: a.js-open-archives {{_ 'archived-items'}}
if currentUser.isBoardAdmin
li: a.js-change-board-color {{_ 'board-change-color'}}
//-
XXX Language should be handled by sandstorm, but for now display a
language selection link in the board menu. This link is normally present
in the header bar that is not displayed on sandstorm.
if isSandstorm
li: a.js-change-language {{_ 'language'}}
unless isSandstorm
if currentUser.isBoardAdmin
hr
ul.pop-over-list
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
li: a.js-archive-board {{_ 'archive-board'}}
li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
hr
ul.pop-over-list
li: a.js-subtask-settings {{_ 'subtask-settings'}}
if isSandstorm
hr
ul.pop-over-list
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
li: a.js-import-board {{_ 'import-board-c'}}
hr
ul.pop-over-list
li: a.js-subtask-settings {{_ 'subtask-settings'}}
template(name="boardVisibilityList") template(name="boardVisibilityList")
ul.pop-over-list ul.pop-over-list
@ -154,15 +157,14 @@ template(name="boardVisibilityList")
if visibilityCheck if visibilityCheck
i.fa.fa-check i.fa.fa-check
span.sub-name {{_ 'private-desc'}} span.sub-name {{_ 'private-desc'}}
if notAllowPrivateVisibilityOnly li
li with "public"
with "public" a.js-select-visibility
a.js-select-visibility i.fa.fa-globe.colorful
i.fa.fa-globe.colorful | {{_ 'public'}}
| {{_ 'public'}} if visibilityCheck
if visibilityCheck i.fa.fa-check
i.fa.fa-check span.sub-name {{_ 'public-desc'}}
span.sub-name {{_ 'public-desc'}}
template(name="boardChangeVisibilityPopup") template(name="boardChangeVisibilityPopup")
+boardVisibilityList +boardVisibilityList
@ -194,30 +196,64 @@ template(name="boardChangeWatchPopup")
i.fa.fa-check i.fa.fa-check
span.sub-name {{_ 'muted-info'}} span.sub-name {{_ 'muted-info'}}
template(name="boardChangeViewPopup") template(name="boardChangeColorPopup")
ul.pop-over-list .board-backgrounds-list.clearfix
li each backgroundColors
with "board-view-swimlanes" .board-background-select.js-select-background
a.js-open-swimlanes-view span.background-box(class="board-color-{{this}}")
i.fa.fa-th-large.colorful if isSelected
| {{_ 'board-view-swimlanes'}}
if $eq Utils.boardView "board-view-swimlanes"
i.fa.fa-check
li
with "board-view-lists"
a.js-open-lists-view
i.fa.fa-trello.colorful
| {{_ 'board-view-lists'}}
if $eq Utils.boardView "board-view-lists"
i.fa.fa-check
li
with "board-view-cal"
a.js-open-cal-view
i.fa.fa-calendar.colorful
| {{_ 'board-view-cal'}}
if $eq Utils.boardView "board-view-cal"
i.fa.fa-check i.fa.fa-check
template(name="boardSubtaskSettingsPopup")
form.board-subtask-settings
h3 {{_ 'show-parent-in-minicard'}}
a#prefix-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-full-path'}}is-checked{{/if}}")
span {{_ 'prefix-with-full-path'}}
a#prefix-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'prefix-with-parent'}}is-checked{{/if}}")
span {{_ 'prefix-with-parent'}}
a#subtext-with-full-path.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-full-path'}}is-checked{{/if}}")
span {{_ 'subtext-with-full-path'}}
a#subtext-with-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'subtext-with-parent'}}is-checked{{/if}}")
span {{_ 'subtext-with-parent'}}
a#no-parent.flex.js-field-show-parent-in-minicard(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
.materialCheckBox(class="{{#if $eq presentParentTask 'no-parent'}}is-checked{{/if}}")
span {{_ 'no-parent'}}
div
hr
div.check-div
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
span {{_ 'show-subtasks-field'}}
label
| {{_ 'deposit-subtasks-board'}}
select.js-field-deposit-board(disabled="{{#unless allowsSubtasks}}disabled{{/unless}}")
each boards
if isBoardSelected
option(value=_id selected="selected") {{title}}
else
option(value=_id) {{title}}
if isNullBoardSelected
option(value='null' selected="selected") {{_ 'custom-field-dropdown-none'}}
else
option(value='null') {{_ 'custom-field-dropdown-none'}}
div
hr
label
| {{_ 'deposit-subtasks-list'}}
select.js-field-deposit-list(disabled="{{#unless hasLists}}disabled{{/unless}}")
each lists
if isListSelected
option(value=_id selected="selected") {{title}}
else
option(value=_id) {{title}}
template(name="createBoard") template(name="createBoard")
form form
label label
@ -236,39 +272,26 @@ 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'}}
a.js-import-board {{_ 'import'}} a.js-import-board {{_ 'import-board'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
//template(name="listsortPopup") template(name="chooseBoardSource")
// h2 ul.pop-over-list
// | {{_ 'list-sort-by'}} li
// hr a(href="{{pathFor '/import/trello'}}") {{_ 'from-trello'}}
// ul.pop-over-list li
// each value in allowedSortValues a(href="{{pathFor '/import/wekan'}}") {{_ 'from-wekan'}}
// li
// a.js-sort-by(name="{{value.name}}")
// if $eq sortby value.name
// i(class="fa {{Direction}}")
// | {{_ value.label }}{{_ value.shortLabel}}
// if $eq sortby value.name
// i(class="fa fa-check")
template(name="boardChangeTitlePopup") template(name="boardChangeTitlePopup")
form form
label label
| {{_ 'title'}} | {{_ 'title'}}
input.js-board-name(type="text" value=title autofocus dir="auto") input.js-board-name(type="text" value=title autofocus)
label label
| {{_ 'description'}} | {{_ 'description'}}
textarea.js-board-desc(dir="auto")= description textarea.js-board-desc= description
input.primary.wide(type="submit" value="{{_ 'rename'}}") input.primary.wide(type="submit" value="{{_ 'rename'}}")
template(name="boardCreateRulePopup") template(name="boardCreateRulePopup")
@ -276,17 +299,26 @@ template(name="boardCreateRulePopup")
button.js-confirm.negate.full(type="submit") {{_ 'archive'}} button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
template(name="cardsSortPopup") template(name="archiveBoardPopup")
ul.pop-over-list p {{_ 'close-board-pop'}}
li button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
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'}}
template(name="outgoingWebhooksPopup")
each integrations
form.integration-form
if title
h4 {{title}}
else
h4 {{_ 'no-name'}}
label
| URL
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
input(type="hidden" value=_id name="id")
input.primary.wide(type="submit" value="{{_ 'save'}}")
form.integration-form
h4
| {{_ 'new-outgoing-webhook'}}
label
| URL
input.js-outgoing-webhooks-url(type="text" name="url" autofocus)
input.primary.wide(type="submit" value="{{_ 'save'}}")

View file

@ -1,156 +1,257 @@
import { ReactiveCache } from '/imports/reactiveCache'; Template.boardMenuPopup.events({
import { TAPi18n } from '/imports/i18n'; 'click .js-rename-board': Popup.open('boardChangeTitle'),
import dragscroll from '@wekanteam/dragscroll'; 'click .js-custom-fields'() {
Sidebar.setView('customFields');
Popup.close();
},
'click .js-open-archives'() {
Sidebar.setView('archives');
Popup.close();
},
'click .js-change-board-color': Popup.open('boardChangeColor'),
'click .js-change-language': Popup.open('changeLanguage'),
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
currentBoard.archive();
// XXX We should have some kind of notification on top of the page to
// confirm that the board was successfully archived.
FlowRouter.go('home');
}),
'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
Popup.close();
Boards.remove(currentBoard._id);
FlowRouter.go('home');
}),
'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
});
/* Template.boardMenuPopup.helpers({
const DOWNCLS = 'fa-sort-down'; exportUrl() {
const UPCLS = 'fa-sort-up'; const params = {
*/ boardId: Session.get('currentBoard'),
const sortCardsBy = new ReactiveVar(''); };
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(evt, tpl) {
const newTitle = templateInstance const newTitle = tpl.$('.js-board-name').val().trim();
.$('.js-board-name') const newDesc = tpl.$('.js-board-desc').val().trim();
.val()
.trim();
const newDesc = templateInstance
.$('.js-board-desc')
.val()
.trim();
if (newTitle) { if (newTitle) {
this.rename(newTitle); this.rename(newTitle);
this.setDescription(newDesc); this.setDescription(newDesc);
Popup.back(); Popup.close();
} }
event.preventDefault(); evt.preventDefault();
}, },
}); });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
watchLevel() { watchLevel() {
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
return currentBoard && currentBoard.getWatchLevel(Meteor.userId()); return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
}, },
isStarred() { isStarred() {
const boardId = Session.get('currentBoard'); const boardId = Session.get('currentBoard');
const user = ReactiveCache.getCurrentUser(); const user = Meteor.user();
return user && user.hasStarred(boardId); return user && user.hasStarred(boardId);
}, },
// Only show the star counter if the number of star is greater than 2 // Only show the star counter if the number of star is greater than 2
showStarCounter() { showStarCounter() {
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
return currentBoard && currentBoard.stars >= 2; return currentBoard && currentBoard.stars >= 2;
}, },
/*
showSort() {
return ReactiveCache.getCurrentUser().hasSortBy();
},
directionClass() {
return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
},
changeDirection() {
const direction = 0 - this.currentDirection() === -1 ? '-' : '';
Meteor.call('setListSortBy', direction + this.currentListSortBy());
},
currentDirection() {
return ReactiveCache.getCurrentUser().getListSortByDirection();
},
currentListSortBy() {
return ReactiveCache.getCurrentUser().getListSortBy();
},
listSortShortDesc() {
return `list-label-short-${this.currentListSortBy()}`;
},
*/
events() { events() {
return [ return [{
{ 'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-edit-board-title': Popup.open('boardChangeTitle'), 'click .js-star-board'() {
'click .js-star-board'() { Meteor.user().toggleBoardStar(Session.get('currentBoard'));
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
Sidebar.toggle();
},
'click .js-open-filter-view'() {
Sidebar.setView('filter');
},
'click .js-sort-cards': Popup.open('cardsSort'),
/*
'click .js-open-sort-view'(evt) {
const target = evt.target;
if (target.tagName === 'I') {
// click on the text, popup choices
this.changeDirection();
} else {
// change the sort order
Popup.open('listsort')(evt);
}
},
*/
'click .js-filter-reset'(event) {
event.stopPropagation();
Sidebar.setView();
Filter.reset();
},
'click .js-sort-reset'() {
Session.set('sortBy', '');
},
'click .js-open-search-view'() {
Sidebar.setView('search');
},
'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId();
MultiSelection.activate();
if (currentCard) {
MultiSelection.add(currentCard);
}
},
'click .js-multiselection-reset'(event) {
event.stopPropagation();
MultiSelection.disable();
},
'click .js-log-in'() {
FlowRouter.go('atSignIn');
},
}, },
]; 'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
'click .js-toggle-board-view'() {
const currentUser = Meteor.user();
if (currentUser.profile.boardView === 'board-view-swimlanes') {
currentUser.setBoardView('board-view-cal');
} else if (currentUser.profile.boardView === 'board-view-lists') {
currentUser.setBoardView('board-view-swimlanes');
} else if (currentUser.profile.boardView === 'board-view-cal') {
currentUser.setBoardView('board-view-lists');
}
},
'click .js-open-filter-view'() {
Sidebar.setView('filter');
},
'click .js-filter-reset'(evt) {
evt.stopPropagation();
Sidebar.setView();
Filter.reset();
},
'click .js-open-search-view'() {
Sidebar.setView('search');
},
'click .js-open-rules-view'() {
Modal.openWide('rulesMain');
},
'click .js-multiselection-activate'() {
const currentCard = Session.get('currentCard');
MultiSelection.activate();
if (currentCard) {
MultiSelection.add(currentCard);
}
},
'click .js-multiselection-reset'(evt) {
evt.stopPropagation();
MultiSelection.disable();
},
'click .js-log-in'() {
FlowRouter.go('atSignIn');
},
}];
}, },
}).register('boardHeaderBar'); }).register('boardHeaderBar');
Template.boardHeaderBar.helpers({ Template.boardHeaderBar.helpers({
boardView() { canModifyBoard() {
return Utils.boardView(); return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
isSortActive() {
return Session.get('sortBy') ? true : false;
}, },
}); });
Template.boardChangeViewPopup.events({ BlazeComponent.extendComponent({
'click .js-open-lists-view'() { backgroundColors() {
Utils.setBoardView('board-view-lists'); return Boards.simpleSchema()._schema.color.allowedValues;
Popup.back();
}, },
'click .js-open-swimlanes-view'() {
Utils.setBoardView('board-view-swimlanes'); isSelected() {
Popup.back(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
return currentBoard.color === this.currentData().toString();
}, },
'click .js-open-cal-view'() {
Utils.setBoardView('board-view-cal'); events() {
Popup.back(); return [{
'click .js-select-background'(evt) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
const newColor = this.currentData().toString();
currentBoard.setColor(newColor);
evt.preventDefault();
},
}];
}, },
}); }).register('boardChangeColorPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Boards.findOne(Session.get('currentBoard'));
},
allowsSubtasks() {
return this.currentBoard.allowsSubtasks;
},
isBoardSelected() {
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
},
isNullBoardSelected() {
return (this.currentBoard.subtasksDefaultBoardId === null) || (this.currentBoard.subtasksDefaultBoardId === undefined);
},
boards() {
return Boards.find({
archived: false,
'members.userId': Meteor.userId(),
}, {
sort: ['title'],
});
},
lists() {
return Lists.find({
boardId: this.currentBoard._id,
archived: false,
}, {
sort: ['title'],
});
},
hasLists() {
return this.lists().count() > 0;
},
isListSelected() {
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
},
presentParentTask() {
let result = this.currentBoard.presentParentTask;
if ((result === null) || (result === undefined)) {
result = 'no-parent';
}
return result;
},
events() {
return [{
'click .js-field-has-subtasks'(evt) {
evt.preventDefault();
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
$('.js-field-has-subtasks .materialCheckBox').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
$('.js-field-has-subtasks').toggleClass('is-checked', this.currentBoard.allowsSubtasks);
$('.js-field-deposit-board').prop('disabled', !this.currentBoard.allowsSubtasks);
},
'change .js-field-deposit-board'(evt) {
let value = evt.target.value;
if (value === 'null') {
value = null;
}
this.currentBoard.setSubtasksDefaultBoardId(value);
evt.preventDefault();
},
'change .js-field-deposit-list'(evt) {
this.currentBoard.setSubtasksDefaultListId(evt.target.value);
evt.preventDefault();
},
'click .js-field-show-parent-in-minicard'(evt) {
const value = evt.target.id || $(evt.target).parent()[0].id || $(evt.target).parent()[0].parent()[0].id;
const options = [
'prefix-with-full-path',
'prefix-with-parent',
'subtext-with-full-path',
'subtext-with-parent',
'no-parent'];
options.forEach(function(element) {
if (element !== value) {
$(`#${element} .materialCheckBox`).toggleClass('is-checked', false);
$(`#${element}`).toggleClass('is-checked', false);
}
});
$(`#${value} .materialCheckBox`).toggleClass('is-checked', true);
$(`#${value}`).toggleClass('is-checked', true);
this.currentBoard.setPresentParentTask(value);
evt.preventDefault();
},
}];
},
}).register('boardSubtaskSettingsPopup');
const CreateBoard = BlazeComponent.extendComponent({ const CreateBoard = BlazeComponent.extendComponent({
template() { template() {
@ -161,11 +262,6 @@ const CreateBoard = BlazeComponent.extendComponent({
this.visibilityMenuIsOpen = new ReactiveVar(false); this.visibilityMenuIsOpen = new ReactiveVar(false);
this.visibility = new ReactiveVar('private'); this.visibility = new ReactiveVar('private');
this.boardId = new ReactiveVar(''); this.boardId = new ReactiveVar('');
Meteor.subscribe('tableVisibilityModeSettings');
},
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
}, },
visibilityCheck() { visibilityCheck() {
@ -181,134 +277,74 @@ const CreateBoard = BlazeComponent.extendComponent({
this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get()); this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
}, },
toggleAddTemplateContainer() { onSubmit(evt) {
$('#add-template-container').toggleClass('is-checked'); evt.preventDefault();
},
onSubmit(event) {
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();
const addTemplateContainer = $('#add-template-container.is-checked').length > 0; this.boardId.set(Boards.insert({
if (addTemplateContainer) { title,
//const templateContainerId = Meteor.call('setCreateTemplateContainer'); permission: visibility,
//Utils.goBoardId(templateContainerId); }));
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
this.boardId.set( Swimlanes.insert({
Boards.insert({ title: 'Default',
// title: TAPi18n.__('templates'), boardId: this.boardId.get(),
title: title, });
permission: 'private',
type: 'template-container',
}),
);
// Insert the card templates swimlane Utils.goBoardId(this.boardId.get());
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() {
return [ return [{
{ 'click .js-select-visibility'() {
'click .js-select-visibility'() { this.setVisibility(this.currentData());
this.setVisibility(this.currentData());
},
'click .js-change-visibility': this.toggleVisibilityMenu,
'click .js-import': Popup.open('boardImportBoard'),
submit: this.onSubmit,
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-board-template': Popup.open('searchElement'),
'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
}, },
]; 'click .js-change-visibility': this.toggleVisibilityMenu,
'click .js-import': Popup.open('boardImportBoard'),
submit: this.onSubmit,
'click .js-import-board': Popup.open('chooseBoardSource'),
}];
}, },
}).register('createBoardPopup'); }).register('createBoardPopup');
BlazeComponent.extendComponent({
template() {
return 'chooseBoardSource';
},
}).register('chooseBoardSourcePopup');
(class HeaderBarCreateBoard extends CreateBoard { (class HeaderBarCreateBoard extends CreateBoard {
onSubmit(event) { onSubmit(evt) {
super.onSubmit(event); super.onSubmit(evt);
// Immediately star boards crated with the headerbar popup. // Immediately star boards crated with the headerbar popup.
ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get()); Meteor.user().toggleBoardStar(this.boardId.get());
} }
}.register('headerBarCreateBoardPopup')); }).register('headerBarCreateBoardPopup');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
},
visibilityCheck() { visibilityCheck() {
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
return this.currentData() === currentBoard.permission; return this.currentData() === currentBoard.permission;
}, },
selectBoardVisibility() { selectBoardVisibility() {
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
const visibility = this.currentData(); const visibility = this.currentData();
currentBoard.setVisibility(visibility); currentBoard.setVisibility(visibility);
Popup.back(); Popup.close();
}, },
events() { events() {
return [ return [{
{ 'click .js-select-visibility': this.selectBoardVisibility,
'click .js-select-visibility': this.selectBoardVisibility, }];
},
];
}, },
}).register('boardChangeVisibilityPopup'); }).register('boardChangeVisibilityPopup');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
watchLevel() { watchLevel() {
const currentBoard = Utils.getCurrentBoard(); const currentBoard = Boards.findOne(Session.get('currentBoard'));
return currentBoard.getWatchLevel(Meteor.userId()); return currentBoard.getWatchLevel(Meteor.userId());
}, },
@ -317,134 +353,60 @@ BlazeComponent.extendComponent({
}, },
events() { events() {
return [ return [{
{ 'click .js-select-watch'() {
'click .js-select-watch'() { const level = this.currentData();
const level = this.currentData(); Meteor.call('watch', 'board', Session.get('currentBoard'), level, (err, ret) => {
Meteor.call( if (!err && ret) Popup.close();
'watch', });
'board',
Session.get('currentBoard'),
level,
(err, ret) => {
if (!err && ret) Popup.back();
},
);
},
}, },
]; }];
}, },
}).register('boardChangeWatchPopup'); }).register('boardChangeWatchPopup');
/*
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { integrations() {
//this.sortBy = new ReactiveVar(); const boardId = Session.get('currentBoard');
////this.sortDirection = new ReactiveVar(); return Integrations.find({ boardId: `${boardId}` }).fetch();
//this.setSortBy();
this.downClass = DOWNCLS;
this.upClass = UPCLS;
},
allowedSortValues() {
const types = [];
const pushed = {};
ReactiveCache.getCurrentUser()
.getListSortTypes()
.forEach(type => {
const key = type.replace(/^-/, '');
if (pushed[key] === undefined) {
types.push({
name: key,
label: `list-label-${key}`,
shortLabel: `list-label-short-${key}`,
});
pushed[key] = 1;
}
});
return types;
},
Direction() {
return ReactiveCache.getCurrentUser().getListSortByDirection() === -1
? this.downClass
: this.upClass;
},
sortby() {
return ReactiveCache.getCurrentUser().getListSortBy();
}, },
setSortBy(type = null) { integration(id) {
const user = ReactiveCache.getCurrentUser(); const boardId = Session.get('currentBoard');
if (type === null) { return Integrations.findOne({ _id: id, boardId: `${boardId}` });
type = user._getListSortBy();
} else {
let value = '';
if (type.map) {
// is an array
value = (type[1] === -1 ? '-' : '') + type[0];
}
Meteor.call('setListSortBy', value);
}
//this.sortBy.set(type[0]);
//this.sortDirection.set(type[1]);
}, },
events() { events() {
return [ return [{
{ 'submit'(evt) {
'click .js-sort-by'(evt) { evt.preventDefault();
evt.preventDefault(); const url = evt.target.url.value;
const target = evt.target; const boardId = Session.get('currentBoard');
const sortby = target.getAttribute('name'); let id = null;
const down = !!target.querySelector(`.${this.upClass}`); let integration = null;
const direction = down ? -1 : 1; if (evt.target.id) {
this.setSortBy([sortby, direction]); id = evt.target.id.value;
if (Utils.isMiniScreen) { integration = this.integration(id);
Popup.back(); if (url) {
Integrations.update(integration._id, {
$set: {
url: `${url}`,
},
});
} else {
Integrations.remove(integration._id);
} }
}, } else if (url) {
Integrations.insert({
userId: Meteor.userId(),
enabled: true,
type: 'outgoing-webhooks',
url: `${url}`,
boardId: `${boardId}`,
activities: ['all'],
});
}
Popup.close();
}, },
]; }];
}, },
}).register('listsortPopup'); }).register('outgoingWebhooksPopup');
*/
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-sort-due'() {
const sortBy = {
dueAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('due-date'));
Popup.back();
},
'click .js-sort-title'() {
const sortBy = {
title: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('title'));
Popup.back();
},
'click .js-sort-created-asc'() {
const sortBy = {
createdAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
Popup.back();
},
'click .js-sort-created-desc'() {
const sortBy = {
createdAt: -1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
Popup.back();
},
},
];
},
}).register('cardsSortPopup');

View file

@ -0,0 +1,22 @@
.integration-form
padding: 5px
border-bottom: 1px solid #ccc
.flex
display: -webkit-box
display: -moz-box
display: -webkit-flex
display: -moz-flex
display: -ms-flexbox
display: flex
.option
@extends .flex
-webkit-border-radius: 3px;
border-radius: 3px;
background: #fff;
text-decoration: none;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;

View file

@ -1,281 +0,0 @@
@import url("../../../css/reset.css") print, screen;
.board-list {
margin: 0 8px;
}
.board-list li {
float: left;
width: 20%;
box-sizing: border-box;
position: relative;
}
.board-list li.placeholder:after {
content: '';
display: block;
background: #ccc;
border-radius: 3px;
height: 106px;
margin: 8px;
}
.board-list li.ui-sortable-helper {
cursor: grabbing;
transform: rotate(4deg);
display: block !important;
}
.board-list li.starred .fa-star,
.board-list li.starred .fa-star-o {
opacity: 1;
}
.board-list .board-list-item {
overflow: hidden;
background-color: #999;
color: #f6f6f6;
min-height: 100px;
font-size: 16px;
line-height: 22px;
border-radius: 3px;
display: block;
font-weight: 700;
padding: 8px;
margin: 8px;
position: relative;
text-decoration: none;
word-wrap: break-word;
}
.board-list .board-list-item.template-container {
border: 4px solid #fff;
}
.board-list .board-list-item.tile {
background-size: auto;
background-repeat: repeat;
}
.board-list .board-list-item-sub-name {
color: rgba(255,255,255,0.5);
display: block;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
.board-list .board-list-item-desc {
color: #fff;
display: block;
font-size: 14px;
font-weight: 400;
line-height: 18px;
}
.board-list .js-add-board {
text-align: center;
}
.board-list .js-add-board .label {
font-weight: normal;
line-height: 56px;
}
.board-list .js-add-board :hover {
background-color: #939393;
}
.board-list .fa-star,
.board-list .fa-star-o {
bottom: 0;
font-size: 14px;
height: 18px;
line-height: 18px;
opacity: 0;
padding: 9px 9px;
position: absolute;
right: 0;
top: 0;
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
.board-list .fa-circle {
bottom: 0;
font-size: 10px;
height: 10px;
line-height: 10px;
padding: 9px 9px;
position: absolute;
right: 0;
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
.board-list .has-overtime-card-active {
color: #eb4646 !important;
}
.board-list .no-overtime-card-active {
color: #3cb500 !important;
}
.board-list .is-star-active {
color: #fff;
}
.board-list .fa-clone {
position: absolute;
bottom: 0;
font-size: 14px;
height: 18px;
line-height: 18px;
opacity: 0;
right: 0;
padding: 9px 9px;
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
.board-list .fa-archive {
position: absolute;
bottom: 0;
font-size: 14px;
height: 18px;
line-height: 18px;
opacity: 0;
left: 0;
padding: 9px 9px;
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
.board-list li:hover a:hover .fa-star,
.board-list li:hover a:hover .fa-clone,
.board-list li:hover a:hover .fa-archive,
.board-list li:hover a:hover .fa-star-o {
color: #fff;
}
.board-list li:hover a .fa-star,
.board-list li:hover a .fa-clone,
.board-list li:hover a .fa-archive,
.board-list li:hover a .fa-star-o {
color: #fff;
opacity: 0.75;
}
.board-list li:hover a .fa-star:hover,
.board-list li:hover a .fa-clone:hover,
.board-list li:hover a .fa-archive:hover,
.board-list li:hover a .fa-star-o:hover {
font-size: 18px;
opacity: 1;
}
.board-list li:hover a .fa-star.is-star-active,
.board-list li:hover a .fa-clone.is-star-active,
.board-list li:hover a .fa-archive.is-star-active,
.board-list li:hover a .fa-star-o.is-star-active {
opacity: 1;
}
.board-backgrounds-list .board-background-select {
box-sizing: border-box;
display: block;
float: left;
width: 50%;
padding-top: 12px;
position: relative;
z-index: 1;
}
.board-backgrounds-list .board-background-select:nth-child(-n + 2) {
padding-top: 0;
}
.board-backgrounds-list .board-background-select:nth-child(2n) {
padding-left: 6px;
}
.board-backgrounds-list .board-background-select:nth-child(2n+1) {
padding-right: 6px;
}
.board-backgrounds-list .board-background-select .background-box {
color: #fff;
border-radius: 3px;
background-size: cover;
display: block;
height: 74px;
position: relative;
width: 100%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.board-backgrounds-list .board-background-select .background-box i.fa-check {
font-size: 25px;
color: #fff;
}
@media screen and (max-width: 800px) {
.board-list {
height: 100%;
overflow: scroll;
}
.board-list li {
width: 50%;
}
.board-list .board-list-item {
overflow: hidden;
height: 8rem;
}
.board-list .board-list-item-sub-name {
position: relative;
top: -100px;
left: -100px;
}
.board-list .board-handle {
position: absolute;
padding: 7px;
top: 50%;
transform: translateY(-50%);
right: 10px;
font-size: 24px;
}
}
@media screen and (max-width: 360px) {
li {
width: 100%;
}
.board-handle {
position: absolute;
padding: 7px;
top: 50%;
transform: translateY(-50%);
right: 10px;
font-size: 24px;
}
}
.AllBoardTeamsOrgs {
list-style-type: none;
overflow: hidden;
}
.AllBoardTeams,
.AllBoardOrgs,
.AllBoardBtns {
float: left;
}
.js-AllBoardOrgs {
margin-left: 16px;
}
.AllBoardTeams {
margin-left: 16px;
}
.AllBoardButtonsContainer {
margin: 16px;
}
#filterBtn,
#resetBtn {
display: inline;
}
.js-board {
display: block;
}
.minicard-members {
padding: 6px 0 6px 8px;
width: 100%;
margin-bottom: 2px;
margin-left: -4px;
display: inline-block;
}
.minicard-lists {
margin: 0 auto;
max-width: 95%;
height: 100%;
}
.flex {
display: flex;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-wrap .item {
margin: 2px;
padding-right: 6px;
text-align: center;
}

View file

@ -1,40 +1,10 @@
template(name="boardList") template(name="boardList")
.wrapper .wrapper
ul.AllBoardTeamsOrgs ul.board-list.clearfix
li.AllBoardTeams
if userHasTeams
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
option(value="-1") {{_ 'teams'}} :
each teamsDatas
option(value="{{teamId}}") {{_ teamDisplayName}}
li.AllBoardOrgs
if userHasOrgs
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
option(value="-1") {{_ 'organizations'}} :
each orgsDatas
option(value="{{orgId}}") {{orgDisplayName}}
//li.AllBoardTemplates
// if userHasTemplates
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
// option(value="-1") {{_ 'templates'}} :
// each templatesDatas
// option(value="{{templateId}}") {{_ templateDisplayName}}
li.AllBoardBtns
div.AllBoardButtonsContainer
if userHasOrgsOrTeams
i.fa.fa-filter
input#filterBtn(type="button" value="{{_ 'filter'}}")
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
ul.board-list.clearfix.js-boards
li.js-add-board li.js-add-board
a.board-list-item.label(title="{{_ 'add-board'}}") a.board-list-item.label {{_ 'add-board'}}
| {{_ 'add-board'}}
each boards each boards
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
if isInvited if isInvited
.board-list-item .board-list-item
span.details span.details
@ -46,109 +16,23 @@ 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
if $eq type "template-container" a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}") span.details
span.details span.board-list-item-name= title
span.board-list-item-name(title="{{_ 'template-container'}}") i.fa.js-star-board(
+viewer class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
= title title="{{_ 'star-board-title'}}")
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}" if hasSpentTimeCards
title="{{_ 'star-board-title'}}") i.fa.js-has-spenttime-cards(
p.board-list-item-desc class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
+viewer title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
= description
if hasSpentTimeCards p.board-list-item-desc= description
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 isTouchScreenOrShowDesktopDragHandles
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
else
if isSandstorm
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if isAdministrable
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if currentUser.isAdmin
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+viewer
= title
unless currentSetting.hideBoardMemberList
if allowsBoardMemberList
.minicard-members
each member in boardMembers _id
a.name
+userAvatar(userId=member noRemove=true)
unless currentSetting.hideCardCounterList
if allowsCardCounterList
.minicard-lists.flex.flex-wrap
each list in boardLists _id
.item
| {{ list }}
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 isTouchScreenOrShowDesktopDragHandles
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
else
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 {{_ title }} h1 {{_ 'my-boards'}}
//.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}}")
// i.fa.fa-clone
// span {{_ 'templates'}}

View file

@ -1,353 +1,60 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
const subManager = new SubsManager(); const subManager = new SubsManager();
Template.boardList.helpers({
hideCardCounterList() {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
return Utils.isMiniScreen() && Session.get('currentBoard'); */
return true;
},
hideBoardMemberList() {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
return Utils.isMiniScreen() && Session.get('currentBoard'); */
return true;
},
})
Template.boardListHeaderBar.events({
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
});
Template.boardListHeaderBar.helpers({
title() {
//if (FlowRouter.getRouteName() === 'template-container') {
// return 'template-container';
//} else {
return FlowRouter.getRouteName() === 'home' ? 'my-boards' : 'public';
//}
},
templatesBoardId() {
return ReactiveCache.getCurrentUser()?.getTemplatesBoardId();
},
templatesBoardSlug() {
return ReactiveCache.getCurrentUser()?.getTemplatesBoardSlug();
},
});
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
onCreated() { onCreated() {
Meteor.subscribe('setting'); Meteor.subscribe('setting');
Meteor.subscribe('tableVisibilityModeSettings');
let currUser = ReactiveCache.getCurrentUser();
let userLanguage;
if (currUser && currUser.profile) {
userLanguage = currUser.profile.language
}
if (userLanguage) {
TAPi18n.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 = Utils.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);
},
});
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$boards.sortable({
handle: '.board-handle',
});
}
});
},
userHasTeams() {
if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
return true;
else
return false;
},
teamsDatas() {
const teams = ReactiveCache.getCurrentUser()?.teams
if (teams)
return teams.sort((a, b) => a.teamDisplayName.localeCompare(b.teamDisplayName));
else
return [];
},
userHasOrgs() {
if (ReactiveCache.getCurrentUser()?.orgs?.length > 0)
return true;
else
return false;
},
orgsDatas() {
const orgs = ReactiveCache.getCurrentUser()?.orgs;
if (orgs)
return orgs.sort((a, b) => a.orgDisplayName.localeCompare(b.orgDisplayName));
else
return [];
},
userHasOrgsOrTeams() {
const ret = this.userHasOrgs() || this.userHasTeams();
return ret;
},
boards() { boards() {
let query = { return Boards.find({
// { type: 'board' }, archived: false,
// { type: { $in: ['board','template-container'] } }, 'members.userId': Meteor.userId(),
$and: [ }, {
{ archived: false }, sort: ['title'],
{ type: { $in: ['board', 'template-container'] } },
{ $or: [] },
{ title: { $not: { $regex: /^\^.*\^$/ } } }
]
};
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
if (FlowRouter.getRouteName() === 'home') {
query.$and[2].$or.push({ 'members.userId': Meteor.userId() });
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) {
query.$and.push({ 'permission': 'private' });
}
const currUser = ReactiveCache.getCurrentUser();
let orgIdsUserBelongs = currUser?.orgIdsUserBelongs() || '';
if (orgIdsUserBelongs) {
let orgsIds = orgIdsUserBelongs.split(',');
// for(let i = 0; i < orgsIds.length; i++){
// query.$and[2].$or.push({'orgs.orgId': orgsIds[i]});
// }
//query.$and[2].$or.push({'orgs': {$elemMatch : {orgId: orgsIds[0]}}});
query.$and[2].$or.push({ 'orgs.orgId': { $in: orgsIds } });
}
let teamIdsUserBelongs = currUser?.teamIdsUserBelongs() || '';
if (teamIdsUserBelongs) {
let teamsIds = teamIdsUserBelongs.split(',');
// for(let i = 0; i < teamsIds.length; i++){
// query.$or[2].$or.push({'teams.teamId': teamsIds[i]});
// }
//query.$and[2].$or.push({'teams': { $elemMatch : {teamId: teamsIds[0]}}});
query.$and[2].$or.push({ 'teams.teamId': { $in: teamsIds } });
}
}
else if (allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue) {
query = {
archived: false,
//type: { $in: ['board','template-container'] },
type: 'board',
permission: 'public',
};
}
const ret = ReactiveCache.getBoards(query, {
sort: { sort: 1 /* boards default sorting */ },
}); });
return ret;
},
boardLists(boardId) {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
const lists = ReactiveCache.getLists({ 'boardId': boardId, 'archived': false },{sort: ['sort','asc']});
const ret = lists.map(list => {
let cardCount = ReactiveCache.getCards({ 'boardId': boardId, 'listId': list._id }).length;
return `${list.title}: ${cardCount}`;
});
return ret;
*/
return [];
},
boardMembers(boardId) {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
const lists = ReactiveCache.getBoard(boardId)
const boardMembers = lists?.members.map(member => member.userId);
return boardMembers;
*/
return [];
}, },
isStarred() { isStarred() {
const user = ReactiveCache.getCurrentUser(); const user = Meteor.user();
return user && user.hasStarred(this.currentData()._id); return user && user.hasStarred(this.currentData()._id);
}, },
isAdministrable() {
const user = ReactiveCache.getCurrentUser();
return user && user.isBoardAdmin(this.currentData()._id);
},
hasOvertimeCards() { hasOvertimeCards() {
subManager.subscribe('board', this.currentData()._id);
return this.currentData().hasOvertimeCards(); return this.currentData().hasOvertimeCards();
}, },
hasSpentTimeCards() { hasSpentTimeCards() {
subManager.subscribe('board', this.currentData()._id);
return this.currentData().hasSpentTimeCards(); return this.currentData().hasSpentTimeCards();
}, },
isInvited() { isInvited() {
const user = ReactiveCache.getCurrentUser(); const user = Meteor.user();
return user && user.isInvitedTo(this.currentData()._id); return user && user.isInvitedTo(this.currentData()._id);
}, },
events() { events() {
return [ return [{
{ 'click .js-add-board': Popup.open('createBoard'),
'click .js-add-board': Popup.open('createBoard'), 'click .js-star-board'(evt) {
'click .js-star-board'(evt) { const boardId = this.currentData()._id;
const boardId = this.currentData()._id; Meteor.user().toggleBoardStar(boardId);
ReactiveCache.getCurrentUser().toggleBoardStar(boardId); evt.preventDefault();
evt.preventDefault();
},
'click .js-clone-board'(evt) {
let title = getSlug(ReactiveCache.getBoard(this.currentData()._id).title) || 'cloned-board';
Meteor.call(
'copyBoard',
this.currentData()._id,
{
sort: ReactiveCache.getBoards({ archived: false }).length,
type: 'board',
title: ReactiveCache.getBoard(this.currentData()._id).title,
},
(err, res) => {
if (err) {
console.error(err);
} else {
Session.set('fromBoard', null);
subManager.subscribe('board', res, false);
FlowRouter.go('board', {
id: res,
slug: title,
});
}
},
);
evt.preventDefault();
},
'click .js-archive-board'(evt) {
const boardId = this.currentData()._id;
Meteor.call('archiveBoard', boardId);
evt.preventDefault();
},
'click .js-accept-invite'() {
const boardId = this.currentData()._id;
Meteor.call('acceptInvite', boardId);
},
'click .js-decline-invite'() {
const boardId = this.currentData()._id;
Meteor.call('quitBoard', boardId, (err, ret) => {
if (!err && ret) {
Meteor.call('acceptInvite', boardId);
FlowRouter.go('home');
}
});
},
'click #resetBtn'(event) {
let allBoards = document.getElementsByClassName("js-board");
let currBoard;
for (let i = 0; i < allBoards.length; i++) {
currBoard = allBoards[i];
currBoard.style.display = "block";
}
},
'click #filterBtn'(event) {
event.preventDefault();
let selectedTeams = document.querySelectorAll('#jsAllBoardTeams option:checked');
let selectedTeamsValues = Array.from(selectedTeams).map(function (elt) { return elt.value });
let index = selectedTeamsValues.indexOf("-1");
if (index > -1) {
selectedTeamsValues.splice(index, 1);
}
let selectedOrgs = document.querySelectorAll('#jsAllBoardOrgs option:checked');
let selectedOrgsValues = Array.from(selectedOrgs).map(function (elt) { return elt.value });
index = selectedOrgsValues.indexOf("-1");
if (index > -1) {
selectedOrgsValues.splice(index, 1);
}
if (selectedTeamsValues.length > 0 || selectedOrgsValues.length > 0) {
const query = {
$and: [
{ archived: false },
{ type: 'board' },
{ $or: [] }
]
};
if (selectedTeamsValues.length > 0) {
query.$and[2].$or.push({ 'teams.teamId': { $in: selectedTeamsValues } });
}
if (selectedOrgsValues.length > 0) {
query.$and[2].$or.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
}
let filteredBoards = ReactiveCache.getBoards(query, {});
let allBoards = document.getElementsByClassName("js-board");
let currBoard;
if (filteredBoards.length > 0) {
let currBoardId;
let found;
for (let i = 0; i < allBoards.length; i++) {
currBoard = allBoards[i];
currBoardId = currBoard.classList[0];
found = filteredBoards.find(function (board) {
return board._id == currBoardId;
});
if (found !== undefined)
currBoard.style.display = "block";
else
currBoard.style.display = "none";
}
}
else {
for (let i = 0; i < allBoards.length; i++) {
currBoard = allBoards[i];
currBoard.style.display = "none";
}
}
}
},
}, },
]; 'click .js-accept-invite'() {
const boardId = this.currentData()._id;
Meteor.user().removeInvite(boardId);
},
'click .js-decline-invite'() {
const boardId = this.currentData()._id;
Meteor.call('quitBoard', boardId, (err, ret) => {
if (!err && ret) {
Meteor.user().removeInvite(boardId);
FlowRouter.go('home');
}
});
},
}];
}, },
}).register('boardList'); }).register('boardList');

View file

@ -0,0 +1,169 @@
@import 'nib'
$spaceBetweenTiles = 16px
.board-list
margin: 0 ($spaceBetweenTiles/2)
li
float: left
width: 25%
box-sizing: border-box
position: relative
&.starred
.fa-star,
.fa-star-o
opacity: 1
.board-list-item
overflow: hidden;
background-color: #999
color: #f6f6f6
height: 90px
font-size: 16px
line-height: 22px
border-radius: 3px
display: block
font-weight: 700
min-height: 18px
padding: 8px
margin: ($spaceBetweenTiles/2)
position: relative
text-decoration: none
&.tile
background-size: auto
background-repeat: repeat
.board-list-item-sub-name
color: rgba(255, 255, 255, .5)
display: block
font-size: 14px
font-weight: 400
line-height: 22px
.board-list-item-desc
color: #fff
display: block
font-size: 14px
font-weight: 400
line-height: 18px
.js-add-board
text-align:center
.label
font-weight: normal
line-height:90px
:hover
background-color:#939393
.fa-star,
.fa-star-o
bottom: 0
font-size: 14px
height: 18px
line-height: 18px
opacity: 0
padding: 9px 9px
position: absolute
right: 0
top: 0
transition-duration: .15s
transition-property: color, font-size, background
.fa-circle
bottom: 0;
font-size: 10px;
height: 10px;
line-height: 10px;
padding: 9px 9px;
position: absolute;
right: 0;
transition-duration: .15s
transition-property: color, font-size, background
.has-overtime-card-active
color: #eb4646 !important
.no-overtime-card-active
color: #3cb500 !important
.is-star-active
color: white
li:hover a
&:hover
.fa-star,
.fa-star-o
color: white
.fa-star,
.fa-star-o
color: white
opacity: .75
&:hover
font-size: 18px
opacity: 1
&.is-star-active
opacity: 1
.board-backgrounds-list
.board-background-select
box-sizing: border-box
display: block
float: left
width: 50%
padding-top: 12px
position: relative
z-index: 1
&:nth-child(-n + 2)
padding-top: 0
&:nth-child(2n)
padding-left: 6px
&:nth-child(2n+1)
padding-right: 6px
.background-box
border-radius: 3px
background-size: cover
display: block
height: 74px
position: relative
width: 100%
cursor: pointer
display: flex
align-items: center
justify-content: center
i.fa-check
font-size: 25px
color: white
@media screen and (max-width: 800px)
.board-list
height: 100%
overflow: scroll
li
width: 33.3%
.board-list-item
overflow: hidden
.board-list-item-sub-name
position: relative
top: -100px
left: -100px
@media screen and (max-width: 360px)
li
width: 50%

View file

@ -1,8 +0,0 @@
template(name="miniboard")
.minicard(
class="minicard-{{colorClass}}")
.minicard-title
.handle
.fa.fa-arrows
+viewer
= title

View file

@ -1,202 +0,0 @@
.hidden {
display: none;
}
.attachment-upload {
text-align: center;
font-weight: bold;
}
.attachment-gallery {
display: flex;
flex-direction: column;
}
.attachment-item {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 16px;
}
.attachment-item:hover {
background: #e0e0e0;
}
.attachment-thumbnail-container {
display: block;
width: 150px;
min-width: 150px;
max-height: 150px;
padding-right: 16px;
}
.attachment-thumbnail {
max-width: 150px;
max-height: 150px;
min-height: 2em;
cursor: pointer;
}
.attachment-thumbnail-text {
min-height: 2em;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 5px;
}
.attachment-details-container {
display: block;
flex-grow: 1;
}
.attachment-details {
display: flex;
justify-content: space-between;
margin-right: 25px; /* Make sure the icons are not to far to the right */
}
.attachment-actions {
display: flex;
flex-direction: row;
align-items: center;
}
.add-attachment {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #555;
border-radius: 5px;
padding: 10px;
cursor: pointer;
margin-top: 16px;
}
.icon {
font-size: 1.5em;
cursor: pointer;
margin-left: 10px;
}
.icon:hover {
color: #666;
}
#viewer-overlay {
width: 100%;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 9999 !important;
background: rgba(13, 13, 13, 0.95);
}
#viewer-container {
display: flex;
flex-direction: row;
justify-content: space-between;
height: 100%;
}
#viewer-top-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
padding: 16px;
}
#attachment-name {
color: white;
font-size: 1.5em;
max-width: calc(
100% - 50px
); /* Make sure the name does not overlap the close button */
}
#viewer-close {
color: white;
cursor: pointer;
font-size: 4em;
top: 0;
right: 8px;
position: absolute;
}
.attachment-arrow {
font-size: 4em;
color: white;
cursor: pointer;
align-self: center;
margin: 0 20px;
}
#viewer-content {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: calc(100% - 50px);
}
#image-viewer {
background: repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%) 50% /
20px 20px; /* Checkerboard background for transparent images */
max-width: 100%;
max-height: 100%;
}
#pdf-viewer {
width: 40vw;
height: 100%;
}
#txt-viewer {
background-color: white;
width: 40vw;
height: 100%;
}
.pdf-preview-error {
margin-top: 20vh;
display: block;
font-size: 2em;
color: white;
}
@media screen and (max-width: 1600px) {
#pdf-viewer {
width: 60vw;
}
}
@media screen and (max-width: 800px) {
#viewer-container {
display: block;
}
.attachment-arrow {
position: absolute;
bottom: 2.2em;
font-size: 1.6em;
padding: 16px;
}
#prev-attachment {
left: 0;
}
#next-attachment {
right: 0;
}
#pdf-viewer {
width: 100%;
height: calc(
100vh - 155px
); /* Full height - height of top and bottom bars */
}
#txt-viewer {
width: 100%;
height: calc(
100vh - 155px
); /* Full height - height of top and bottom bars */
}
#audio-viewer {
margin-top: 20%;
width: 100%;
}
.attachment-thumbnail-container {
width: 100px;
min-width: 100px;
}
.attachment-thumbnail {
max-width: 100px;
}
.attachment-details {
flex-direction: column;
margin-right: 0px;
}
.attachment-actions {
flex-direction: row;
margin-top: 10px;
}
}

View file

@ -1,141 +1,53 @@
template(name="cardAttachmentsPopup") template(name="cardAttachmentsPopup")
if $gt uploads.length 0 ul.pop-over-list
.attachment-upload {{_ 'uploading'}} li
table input.js-attach-file.hide(type="file" name="file" multiple)
tr a.js-computer-upload {{_ 'computer'}}
th.upload-file-name-descr {{_ 'name'}} li
th.upload-progress-descr {{_ 'progress'}} a.js-upload-clipboard-image {{_ 'clipboard'}}
th.upload-remaining-descr {{_ 'remaining_time'}}
th.upload-speed-descr {{_ 'speed'}}
each upload in uploads
tr
td.upload-file-name-value {{upload.file.name}}
td.upload-progress-value {{upload.progress.get}}%
td.upload-remaining-value {{getEstimateTime upload}}
td.upload-speed-value {{getEstimateSpeed upload}}
else
ul.pop-over-list
li
input.js-attach-file.hide(type="file" name="file" multiple)
a.js-computer-upload {{_ 'computer'}}
li
a.js-upload-clipboard-image {{_ 'clipboard'}}
template(name="previewClipboardImagePopup") template(name="previewClipboardImagePopup")
p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}} p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}}
img.preview-clipboard-image() img.preview-clipboard-image()
button.primary.js-upload-pasted-image {{_ 'upload'}} button.primary.js-upload-pasted-image {{_ 'upload'}}
template(name="previewAttachedImagePopup")
img.preview-large-image.js-large-image-clicked(src="{{url}}")
template(name="attachmentDeletePopup") template(name="attachmentDeletePopup")
p {{_ "attachment-delete-pop"}} p {{_ "attachment-delete-pop"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}} button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="attachmentViewer") template(name="attachmentsGalery")
#viewer-overlay.hidden .attachments-galery
#viewer-top-bar
span#attachment-name
a#viewer-close.fa.fa-times-thin
#viewer-container
i.fa.fa-chevron-left.attachment-arrow#prev-attachment
#viewer-content
img#image-viewer.hidden
video#video-viewer.hidden(controls="true")
audio#audio-viewer.hidden(controls="true")
object#pdf-viewer.hidden(type="application/pdf")
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
object#txt-viewer.hidden(type="text/plain")
i.fa.fa-chevron-right.attachment-arrow#next-attachment
template(name="attachmentGallery")
.attachment-gallery
if canModifyCard
a.attachment-item.add-attachment.js-add-attachment
i.fa.fa-plus.icon
each attachments each attachments
.attachment-item .attachment-item
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}") a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}")
if link if isUploaded
if(isImage) if isImage
img.attachment-thumbnail(src="{{link}}" title="{{sanitize name}}") img.attachment-thumbnail-img(src="{{url}}")
else if($eq extension 'svg')
img.attachment-thumbnail(src="{{link}}" title="{{sanitize name}}" type="image/svg+xml")
else if($eq extension 'mp3')
video.attachment-thumbnail(title="{{sanitize name}}")
source(src="{{link}}" type="audio/mpeg")
else if($eq extension 'ogg')
video.attachment-thumbnail(title="{{sanitize name}}")
source(src="{{link}}" type="video/ogg")
else if($eq extension 'webm')
video.attachment-thumbnail(title="{{sanitize name}}")
source(src="{{link}}" type="video/webm")
else if($eq extension 'mp4')
video.attachment-thumbnail(title="{{sanitize name}}")
source(src="{{link}}" type="video/mp4")
else else
span.attachment-thumbnail-text= extension span.attachment-thumbnail-ext= extension
.attachment-details-container
.attachment-details
div
b
= name
span.file-size ({{fileSize size}})
.attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}")
i.fa.fa-download.icon(title="{{_ 'download'}}")
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.js-rename
i.fa.fa-pencil-square-o.icon(title="{{_ 'rename'}}")
a.js-confirm-delete
i.fa.fa-trash.icon(title="{{_ 'delete'}}")
a.fa.fa-navicon.icon.js-open-attachment-menu(data-attachment-link="{{link}}" title="{{_ 'attachmentActionsPopup-title'}}")
template(name="attachmentActionsPopup")
ul.pop-over-list
li
if isImage
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-book
i.fa.fa-picture-o
if isCover
| {{_ 'remove-cover'}}
else else
| {{_ 'add-cover'}} +spinner
if currentUser.isBoardAdmin p.attachment-details
if isImage = name
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}") span.attachment-details-actions
i.fa.fa-picture-o a.js-download(href="{{url download=true}}")
if isBackgroundImage i.fa.fa-download
| {{_ 'remove-background-image'}} | {{_ 'download'}}
else if currentUser.isBoardMember
| {{_ 'add-background-image'}} if isImage
a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-thumb-tack
if($eq ../coverId _id)
| {{_ 'remove-cover'}}
else
| {{_ 'add-cover'}}
a.js-confirm-delete
i.fa.fa-close
| {{_ 'delete'}}
if $neq versions.original.storage "fs" if currentUser.isBoardMember
a.js-move-storage-fs li.attachment-item.add-attachment
i.fa.fa-arrow-right a.js-add-attachment {{_ 'add-attachment' }}
| {{_ 'attachment-move-storage-fs'}}
if $neq versions.original.storage "gridfs"
if versions.original.storage
a.js-move-storage-gridfs
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-gridfs'}}
if $neq versions.original.storage "s3"
if versions.original.storage
a.js-move-storage-s3
i.fa.fa-arrow-right
| {{_ 'attachment-move-storage-s3'}}
template(name="attachmentRenamePopup")
input.js-edit-attachment-name(type='text' autofocus value="{{getNameWithoutExtension}}" dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-attachment-name(type="submit") {{_ 'save'}}

View file

@ -1,509 +1,132 @@
import { ReactiveCache } from '/imports/reactiveCache'; Template.attachmentsGalery.events({
import { ObjectID } from 'bson';
import DOMPurify from 'dompurify';
const filesize = require('filesize');
const prettyMilliseconds = require('pretty-ms');
// We store current card ID and the ID of currently opened attachment in a
// global var. This is used so that we know what's the next attachment to open
// when the user clicks on the prev/next button in the attachment viewer.
let cardId = null;
let openAttachmentId = null;
// Used to store the start and end coordinates of a touch event for attachment swiping
let touchStartCoords = null;
let touchEndCoords = null;
// Stores link to the attachment for which attachment actions popup was opened
attachmentActionsLink = null;
Template.attachmentGallery.events({
'click .open-preview'(event) {
openAttachmentId = $(event.currentTarget).attr("data-attachment-id");
cardId = $(event.currentTarget).attr("data-card-id");
openAttachmentViewer(openAttachmentId);
},
'click .js-add-attachment': Popup.open('cardAttachments'), 'click .js-add-attachment': Popup.open('cardAttachments'),
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete',
function() {
Attachments.remove(this._id);
Popup.close();
}
),
// If we let this event bubble, FlowRouter will handle it and empty the page // If we let this event bubble, FlowRouter will handle it and empty the page
// content, see #101. // content, see #101.
'click .js-download'(event) { 'click .js-download'(event) {
event.stopPropagation(); event.stopPropagation();
}, },
'click .js-open-attachment-menu': Popup.open('attachmentActions'), 'click .js-add-cover'() {
'mouseover .js-open-attachment-menu'(event) { // For some reason I cannot combine handlers for "click .js-open-attachment-menu" and "mouseover .js-open-attachment-menu" events so this is a quick workaround. Cards.findOne(this.cardId).setCover(this._id);
attachmentActionsLink = event.currentTarget.getAttribute("data-attachment-link");
}, },
'click .js-rename': Popup.open('attachmentRename'), 'click .js-remove-cover'() {
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', function() { Cards.findOne(this.cardId).unsetCover();
Attachments.remove(this._id); },
Popup.back(); 'click .js-preview-image'(evt) {
}), Popup.open('previewAttachedImage').call(this, evt);
}); // when multiple thumbnails, if click one then another very fast,
// we might get a wrong width from previous img.
function getNextAttachmentId(currentAttachmentId, offset = 0) { // when popup reused, onRendered() won't be called, so we cannot get there.
const attachments = ReactiveCache.getAttachments({'meta.cardId': cardId}); // here make sure to get correct size when this img fully loaded.
const img = $('img.preview-large-image')[0];
let i = 0; if (!img) return;
for (; i < attachments.length; i++) { const rePosPopup = () => {
if (attachments[i]._id === currentAttachmentId) { const w = img.width;
break; const h = img.height;
} // if the image is too large, we resize & center the popup.
} if (w > 300) {
return attachments[(i + offset + 1 + attachments.length) % attachments.length]._id; $('div.pop-over').css({
} width: (w + 20),
position: 'absolute',
function getPrevAttachmentId(currentAttachmentId, offset = 0) { left: (window.innerWidth - w)/2,
const attachments = ReactiveCache.getAttachments({'meta.cardId': cardId}); top: (window.innerHeight - h)/2,
});
let i = 0;
for (; i < attachments.length; i++) {
if (attachments[i]._id === currentAttachmentId) {
break;
}
}
return attachments[(i + offset - 1 + attachments.length) % attachments.length]._id;
}
function attachmentCanBeOpened(attachment) {
return (
attachment.isImage ||
attachment.isPDF ||
attachment.isText ||
attachment.isJSON ||
attachment.isVideo ||
attachment.isAudio
);
}
function openAttachmentViewer(attachmentId) {
const attachment = ReactiveCache.getAttachment(attachmentId);
// Check if we can open the attachment (if we have a viewer for it) and exit if not
if (!attachmentCanBeOpened(attachment)) {
return;
}
/*
Instructions for adding a new viewer:
- add a new case to the switch statement below
- implement cleanup in the closeAttachmentViewer() function, if necessary
- mark attachment type as openable by adding a new condition to the attachmentCanBeOpened function
*/
switch(true){
case (attachment.isImage):
$("#image-viewer").attr("src", attachment.link());
$("#image-viewer").removeClass("hidden");
break;
case (attachment.isPDF):
$("#pdf-viewer").attr("data", attachment.link());
$("#pdf-viewer").removeClass("hidden");
break;
case (attachment.isVideo):
// We have to create a new <source> DOM element and append it to the video
// element, otherwise the video won't load
let videoSource = document.createElement('source');
videoSource.setAttribute('src', attachment.link());
$("#video-viewer").append(videoSource);
$("#video-viewer").removeClass("hidden");
break;
case (attachment.isAudio):
// We have to create a new <source> DOM element and append it to the audio
// element, otherwise the audio won't load
let audioSource = document.createElement('source');
audioSource.setAttribute('src', attachment.link());
$("#audio-viewer").append(audioSource);
$("#audio-viewer").removeClass("hidden");
break;
case (attachment.isText):
case (attachment.isJSON):
$("#txt-viewer").attr("data", attachment.link());
$("#txt-viewer").removeClass("hidden");
break;
}
$('#attachment-name').text(attachment.name);
$('#viewer-overlay').removeClass('hidden');
}
function closeAttachmentViewer() {
$("#viewer-overlay").addClass("hidden");
// We need to reset the viewers to avoid showing previous attachments
$("#image-viewer").attr("src", "");
$("#image-viewer").addClass("hidden");
$("#pdf-viewer").attr("data", "");
$("#pdf-viewer").addClass("hidden");
$("#txt-viewer").attr("data", "");
$("#txt-viewer").addClass("hidden");
$("#video-viewer").get(0).pause(); // Stop playback
$("#video-viewer").get(0).currentTime = 0;
$("#video-viewer").empty();
$("#video-viewer").addClass("hidden");
$("#audio-viewer").get(0).pause(); // Stop playback
$("#audio-viewer").get(0).currentTime = 0;
$("#audio-viewer").empty();
$("#audio-viewer").addClass("hidden");
}
function openNextAttachment() {
closeAttachmentViewer();
let i = 0;
// Find an attachment that can be opened
while (true) {
const id = getNextAttachmentId(openAttachmentId, i);
const attachment = ReactiveCache.getAttachment(id);
if (attachmentCanBeOpened(attachment)) {
openAttachmentId = id;
openAttachmentViewer(id);
break;
} }
i++; };
} const url = $(evt.currentTarget).attr('src');
} if (img.src === url && img.complete)
rePosPopup();
function openPrevAttachment() { else
closeAttachmentViewer(); img.onload = rePosPopup;
let i = 0;
// Find an attachment that can be opened
while (true) {
const id = getPrevAttachmentId(openAttachmentId, i);
const attachment = ReactiveCache.getAttachment(id);
if (attachmentCanBeOpened(attachment)) {
openAttachmentId = id;
openAttachmentViewer(id);
break;
}
i--;
}
}
function processTouch(){
xDist = touchEndCoords.x - touchStartCoords.x;
yDist = touchEndCoords.y - touchStartCoords.y;
console.log("xDist: " + xDist);
// Left swipe
if (Math.abs(xDist) > Math.abs(yDist) && xDist < 0) {
openNextAttachment();
}
// Right swipe
if (Math.abs(xDist) > Math.abs(yDist) && xDist > 0) {
openPrevAttachment();
}
// Up swipe
if (Math.abs(yDist) > Math.abs(xDist) && yDist < 0) {
closeAttachmentViewer();
}
}
Template.attachmentViewer.events({
'touchstart #viewer-container'(event) {
console.log("touchstart")
touchStartCoords = {
x: event.changedTouches[0].screenX,
y: event.changedTouches[0].screenY
}
},
'touchend #viewer-container'(event) {
console.log("touchend")
touchEndCoords = {
x: event.changedTouches[0].screenX,
y: event.changedTouches[0].screenY
}
processTouch();
},
'click #viewer-container'(event) {
// Make sure the click was on #viewer-container and not on any of its children
if(event.target !== event.currentTarget) {
event.stopPropagation();
return;
}
closeAttachmentViewer();
},
'click #viewer-content'(event) {
// Make sure the click was on #viewer-content and not on any of its children
if(event.target !== event.currentTarget) {
event.stopPropagation();
return;
}
closeAttachmentViewer();
},
'click #viewer-close'() {
closeAttachmentViewer();
},
'click #next-attachment'() {
openNextAttachment();
},
'click #prev-attachment'() {
openPrevAttachment();
}, },
}); });
Template.attachmentGallery.helpers({ Template.previewAttachedImagePopup.events({
isBoardAdmin() { 'click .js-large-image-clicked'(){
return ReactiveCache.getCurrentUser().isBoardAdmin(); Popup.close();
}, },
fileSize(size) {
const ret = filesize(size);
return ret;
},
sanitize(value) {
return DOMPurify.sanitize(value);
},
});
Template.cardAttachmentsPopup.onCreated(function() {
this.uploads = new ReactiveVar([]);
});
Template.cardAttachmentsPopup.helpers({
getEstimateTime(upload) {
const ret = prettyMilliseconds(upload.estimateTime.get());
return ret;
},
getEstimateSpeed(upload) {
const ret = filesize(upload.estimateSpeed.get(), {round: 0}) + "/s";
return ret;
},
uploads() {
return Template.instance().uploads.get();
}
}); });
Template.cardAttachmentsPopup.events({ Template.cardAttachmentsPopup.events({
'change .js-attach-file'(event, templateInstance) { 'change .js-attach-file'(evt) {
const card = this; const card = this;
const files = event.currentTarget.files; FS.Utility.eachFile(evt, (f) => {
if (files) { const file = new FS.File(f);
let uploads = []; if (card.isLinkedCard()) {
for (const file of files) { file.boardId = Cards.findOne(card.linkedId).boardId;
const fileId = new ObjectID().toString(); file.cardId = card.linkedId;
let fileName = DOMPurify.sanitize(file.name); } else {
file.boardId = card.boardId;
// If sanitized filename is not same as original filename, file.cardId = card._id;
// it could be XSS that is already fixed with sanitize,
// or just normal mistake, so it is not a problem.
// That is why here is no warning.
if (fileName !== file.name) {
// If filename is empty, only in that case add some filename
if (fileName.length === 0) {
fileName = 'Empty-filename-after-sanitize.txt';
}
}
const config = {
file: file,
fileId: fileId,
fileName: fileName,
meta: Utils.getCommonAttachmentMetaFrom(card),
chunkSize: 'dynamic',
};
config.meta.fileId = fileId;
const uploader = Attachments.insert(
config,
false,
);
uploader.on('start', function() {
uploads.push(this);
templateInstance.uploads.set(uploads);
});
uploader.on('uploaded', (error, fileRef) => {
if (!error) {
if (fileRef.isImage) {
card.setCover(fileRef._id);
}
}
});
uploader.on('end', (error, fileRef) => {
uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
templateInstance.uploads.set(uploads);
if (uploads.length == 0 ) {
Popup.back();
}
});
uploader.start();
} }
} file.userId = Meteor.userId();
const attachment = Attachments.insert(file);
if (attachment && attachment._id && attachment.isImage()) {
card.setCover(attachment._id);
}
Popup.close();
});
}, },
'click .js-computer-upload'(event, templateInstance) { 'click .js-computer-upload'(evt, tpl) {
templateInstance.find('.js-attach-file').click(); tpl.find('.js-attach-file').click();
event.preventDefault(); evt.preventDefault();
}, },
'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'), 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
}); });
const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
let pastedResults = null; let pastedResults = null;
Template.previewClipboardImagePopup.onRendered(() => { Template.previewClipboardImagePopup.onRendered(() => {
// we can paste image from clipboard // we can paste image from clipboard
const handle = results => { $(document.body).pasteImageReader((results) => {
if (results.dataURL.startsWith('data:image/')) { if (results.dataURL.startsWith('data:image/')) {
const direct = results => { $('img.preview-clipboard-image').attr('src', results.dataURL);
$('img.preview-clipboard-image').attr('src', results.dataURL); pastedResults = results;
pastedResults = results;
};
if (MAX_IMAGE_PIXEL) {
// if has size limitation on image we shrink it before uploading
Utils.shrinkImage({
dataurl: results.dataURL,
maxSize: MAX_IMAGE_PIXEL,
ratio: COMPRESS_RATIO,
callback(changed) {
if (changed !== false && !!changed) {
results.dataURL = changed;
}
direct(results);
},
});
} else {
direct(results);
}
} }
}; });
$(document.body).pasteImageReader(handle);
// we can also drag & drop image file to it // we can also drag & drop image file to it
$(document.body).dropImageReader(handle); $(document.body).dropImageReader((results) => {
if (results.dataURL.startsWith('data:image/')) {
$('img.preview-clipboard-image').attr('src', results.dataURL);
pastedResults = results;
}
});
}); });
Template.previewClipboardImagePopup.events({ Template.previewClipboardImagePopup.events({
'click .js-upload-pasted-image'() { 'click .js-upload-pasted-image'() {
const card = this; const results = pastedResults;
if (pastedResults && pastedResults.file) { if (results && results.file) {
const file = pastedResults.file; const card = this;
window.oPasted = pastedResults; const file = new FS.File(results.file);
const fileId = new ObjectID().toString(); if (!results.name) {
const config = { // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type
file, if (typeof results.file.type === 'string') {
fileId: fileId, file.name(results.file.type.replace('image/', 'clipboard.'));
meta: Utils.getCommonAttachmentMetaFrom(card),
fileName: file.name || file.type.replace('image/', 'clipboard.'),
chunkSize: 'dynamic',
};
config.meta.fileId = fileId;
const uploader = Attachments.insert(
config,
false,
);
uploader.on('uploaded', (error, fileRef) => {
if (!error) {
if (fileRef.isImage) {
card.setCover(fileRef._id);
}
} }
}); }
uploader.on('end', (error, fileRef) => { file.updatedAt(new Date());
pastedResults = null; file.boardId = card.boardId;
$(document.body).pasteImageReader(() => {}); file.cardId = card._id;
Popup.back(); file.userId = Meteor.userId();
}); const attachment = Attachments.insert(file);
uploader.start();
if (attachment && attachment._id && attachment.isImage()) {
card.setCover(attachment._id);
}
pastedResults = null;
$(document.body).pasteImageReader(() => {});
Popup.close();
} }
}, },
}); });
BlazeComponent.extendComponent({
isCover() {
const ret = ReactiveCache.getCard(this.data().meta.cardId).coverId == this.data()._id;
return ret;
},
isBackgroundImage() {
//const currentBoard = Utils.getCurrentBoard();
//return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src");
return false;
},
events() {
return [
{
'click .js-add-cover'() {
ReactiveCache.getCard(this.data().meta.cardId).setCover(this.data()._id);
Popup.back();
},
'click .js-remove-cover'() {
ReactiveCache.getCard(this.data().meta.cardId).unsetCover();
Popup.back();
},
'click .js-add-background-image'() {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL(attachmentActionsLink);
Utils.setBackgroundImage(attachmentActionsLink);
Popup.back();
event.preventDefault();
},
'click .js-remove-background-image'() {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL("");
Utils.setBackgroundImage("");
Popup.back();
Utils.reload();
event.preventDefault();
},
'click .js-move-storage-fs'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
Popup.back();
},
'click .js-move-storage-gridfs'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
Popup.back();
},
'click .js-move-storage-s3'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "s3");
Popup.back();
},
}
]
}
}).register('attachmentActionsPopup');
BlazeComponent.extendComponent({
getNameWithoutExtension() {
const ret = this.data().name.replace(new RegExp("\." + this.data().extension + "$"), "");
return ret;
},
events() {
return [
{
'keydown input.js-edit-attachment-name'(evt) {
// enter = save
if (evt.keyCode === 13) {
this.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-attachment-name'(event) {
// save button pressed
event.preventDefault();
const name = this.$('.js-edit-attachment-name')[0]
.value
.trim() + this.data().extensionWithDot;
if (name === DOMPurify.sanitize(name)) {
Meteor.call('renameAttachment', this.data()._id, name);
}
Popup.back();
},
}
]
}
}).register('attachmentRenamePopup');

View file

@ -0,0 +1,85 @@
@import 'nib'
.attachments-galery
display: flex
flex-wrap: wrap
.attachment-item
width: 33.33% - 2%
margin: 10px 1% 0
text-align: center
border-radius: 3px
overflow: hidden
background: darken(white, 7%)
min-height: 120px
&:hover
background: darken(white, 12%)
&.add-attachment
display: flex
align-items: center
a
display: block
margin: auto
.attachment-thumbnail
height: 80px
display: flex
align-items: center
justify-content: center
position: relative
.attachment-thumbnail-img
max-height: 100%
max-width: 100%
.attachment-thumbnail-ext
text-transform: uppercase
font-size: 1.6em
.attachment-details
font-size: 0.75em
margin: 3px
.attachment-details-actions a
display: block
.attachment-image-preview
max-width: 100px
display: block
box-shadow: 0 1px 2px rgba(0,0,0,.2)
.preview-large-image
max-width: 1000px
display: block
box-shadow: 0 1px 2px rgba(0,0,0,.2)
.preview-clipboard-image
width: 280px
max-width: 100%;
height: 200px
display: block
border: 1px solid black
box-shadow: 0 1px 2px rgba(0,0,0,.2)
@media screen and (max-width: 800px)
.attachments-galery
flex-direction
row
.attachment-item
width: 50% - 2%
.attachment-thumbnail
height: 130px
.attachment-details
font-size: 1.1em
@media screen and (max-width: 360px)
.attachments-galery
.attachment-item
width: 100%
.attachment-thumbnail
height: 200px

View file

@ -4,9 +4,9 @@ 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
= name = name
if hasCustomField if hasCustomField
i.fa.fa-check i.fa.fa-check
hr hr
a.quiet-button.full.js-settings a.quiet-button.full.js-settings
i.fa.fa-cog i.fa.fa-cog
@ -30,10 +30,6 @@ template(name="cardCustomField-text")
= value = value
else else
| {{_ 'edit'}} | {{_ 'edit'}}
else
+viewer
= value
template(name="cardCustomField-number") template(name="cardCustomField-number")
if canModifyCard if canModifyCard
@ -48,55 +44,16 @@ template(name="cardCustomField-number")
= value = value
else else
| {{_ 'edit'}} | {{_ 'edit'}}
else
if value
= value
template(name="cardCustomField-checkbox")
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
if canModifyCard
.check-box-container
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
else
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
template(name="cardCustomField-currency")
if canModifyCard
+inlinedForm(classNames="js-card-customfield-currency")
input(type="text" value=data.value 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
= 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}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}") a.js-edit-date(title="{{showTitle}}" class="{{classes}}")
if value if value
div.card-date div.card-date
time(datetime="{{showISODate}}") time(datetime="{{showISODate}}")
| {{showDate}} | {{showDate}}
if showWeekOfYear else
b | {{_ 'edit'}}
| {{showWeek}}
else
| {{_ 'edit'}}
else
if value
div.card-date
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="cardCustomField-dropdown") template(name="cardCustomField-dropdown")
if canModifyCard if canModifyCard
@ -104,13 +61,9 @@ template(name="cardCustomField-dropdown")
select.inline select.inline
each items each items
if($eq data.value this._id) if($eq data.value this._id)
option(value=_id selected="selected") option(value=_id selected="selected") {{name}}
+viewer
= name
else else
option(value=_id) option(value=_id) {{name}}
+viewer
= name
.edit-controls.clearfix .edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}} button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form a.fa.fa-times-thin.js-close-inlined-form
@ -120,29 +73,4 @@ template(name="cardCustomField-dropdown")
+viewer +viewer
= selectedItem = selectedItem
else else
| {{_ 'edit'}} | {{_ 'edit'}}
else
if value
+viewer
= selectedItem
template(name="cardCustomField-stringtemplate")
if canModifyCard
+inlinedForm(classNames="js-card-customfield-stringtemplate")
each item in stringtemplateItems.get
input.js-card-customfield-stringtemplate-item(type="text" value=item placeholder="")
input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}" autofocus)
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
else
a.js-open-inlined-form
if value
+viewer
= formattedValue
else
| {{_ 'edit'}}
else
if value
+viewer
= formattedValue

View file

@ -1,135 +1,85 @@
import moment from 'moment/min/moment-with-locales';
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import Cards from '/models/cards';
import { CustomFieldStringTemplate } from '/client/lib/customFields'
Template.cardCustomFieldsPopup.helpers({ Template.cardCustomFieldsPopup.helpers({
hasCustomField() { hasCustomField() {
const card = Utils.getCurrentCard(); const card = Cards.findOne(Session.get('currentCard'));
const customFieldId = this._id; const customFieldId = this._id;
return card.customFieldIndex(customFieldId) > -1; return card.customFieldIndex(customFieldId) > -1;
}, },
}); });
Template.cardCustomFieldsPopup.events({ Template.cardCustomFieldsPopup.events({
'click .js-select-field'(event) { 'click .js-select-field'(evt) {
const card = Utils.getCurrentCard(); const card = Cards.findOne(Session.get('currentCard'));
const customFieldId = this._id; const customFieldId = this._id;
card.toggleCustomField(customFieldId); card.toggleCustomField(customFieldId);
event.preventDefault(); evt.preventDefault();
}, },
'click .js-settings'(event) { 'click .js-settings'(evt) {
EscapeActions.executeUpTo('detailsPane'); EscapeActions.executeUpTo('detailsPane');
Sidebar.setView('customFields'); Sidebar.setView('customFields');
event.preventDefault(); evt.preventDefault();
}, },
}); });
// cardCustomField // cardCustomField
const CardCustomField = BlazeComponent.extendComponent({ const CardCustomField = BlazeComponent.extendComponent({
getTemplate() { getTemplate() {
return `cardCustomField-${this.data().definition.type}`; return `cardCustomField-${this.data().definition.type}`;
}, },
onCreated() { onCreated() {
const self = this; const self = this;
self.card = Utils.getCurrentCard(); self.card = Cards.findOne(Session.get('currentCard'));
self.customFieldId = this.data()._id; self.customFieldId = this.data()._id;
}, },
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
}); });
CardCustomField.register('cardCustomField'); CardCustomField.register('cardCustomField');
// cardCustomField-text // cardCustomField-text
(class extends CardCustomField { (class extends CardCustomField {
onCreated() { onCreated() {
super.onCreated(); super.onCreated();
} }
events() { events() {
return [ return [{
{ 'submit .js-card-customfield-text'(evt) {
'submit .js-card-customfield-text'(event) { evt.preventDefault();
event.preventDefault(); const value = this.currentComponent().getValue();
const value = this.currentComponent().getValue(); this.card.setCustomField(this.customFieldId, value);
this.card.setCustomField(this.customFieldId, value);
},
}, },
]; }];
} }
}.register('cardCustomField-text'));
}).register('cardCustomField-text');
// cardCustomField-number // cardCustomField-number
(class extends CardCustomField { (class extends CardCustomField {
onCreated() { onCreated() {
super.onCreated(); super.onCreated();
} }
events() { events() {
return [ return [{
{ 'submit .js-card-customfield-number'(evt) {
'submit .js-card-customfield-number'(event) { evt.preventDefault();
event.preventDefault(); const value = parseInt(this.find('input').value, 10);
const value = parseInt(this.find('input').value, 10); this.card.setCustomField(this.customFieldId, value);
this.card.setCustomField(this.customFieldId, value);
},
}, },
]; }];
}
}.register('cardCustomField-number'));
// cardCustomField-checkbox
(class extends CardCustomField {
onCreated() {
super.onCreated();
} }
toggleItem() { }).register('cardCustomField-number');
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() {
super.onCreated(); super.onCreated();
const self = this; const self = this;
@ -144,14 +94,6 @@ CardCustomField.register('cardCustomField');
}); });
} }
showWeek() {
return this.date.get().week().toString();
}
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
}
showDate() { showDate() {
// this will start working once mquandalle:moment // this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5 // is updated to at least moment.js 2.10.5
@ -166,10 +108,8 @@ CardCustomField.register('cardCustomField');
} }
classes() { classes() {
if ( if (this.date.get().isBefore(this.now.get(), 'minute') &&
this.date.get().isBefore(this.now.get(), 'minute') && this.now.get().isBefore(this.data().value)) {
this.now.get().isBefore(this.data().value)
) {
return 'current'; return 'current';
} }
return ''; return '';
@ -180,20 +120,19 @@ CardCustomField.register('cardCustomField');
} }
events() { events() {
return [ return [{
{ 'click .js-edit-date': Popup.open('cardCustomField-date'),
'click .js-edit-date': Popup.open('cardCustomField-date'), }];
},
];
} }
}.register('cardCustomField-date'));
}).register('cardCustomField-date');
// cardCustomField-datePopup // cardCustomField-datePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(); super.onCreated();
const self = this; const self = this;
self.card = Utils.getCurrentCard(); self.card = Cards.findOne(Session.get('currentCard'));
self.customFieldId = this.data()._id; self.customFieldId = this.data()._id;
this.data().value && this.date.set(moment(this.data().value)); this.data().value && this.date.set(moment(this.data().value));
} }
@ -205,10 +144,11 @@ CardCustomField.register('cardCustomField');
_deleteDate() { _deleteDate() {
this.card.setCustomField(this.customFieldId, ''); this.card.setCustomField(this.customFieldId, '');
} }
}.register('cardCustomField-datePopup')); }).register('cardCustomField-datePopup');
// cardCustomField-dropdown // cardCustomField-dropdown
(class extends CardCustomField { (class extends CardCustomField {
onCreated() { onCreated() {
super.onCreated(); super.onCreated();
this._items = this.data().definition.settings.dropdownItems; this._items = this.data().definition.settings.dropdownItems;
@ -220,109 +160,20 @@ CardCustomField.register('cardCustomField');
} }
selectedItem() { selectedItem() {
const selected = this._items.find(item => { const selected = this._items.find((item) => {
return item._id === this.data().value; return item._id === this.data().value;
}); });
return selected return (selected) ? selected.name : TAPi18n.__('custom-field-dropdown-unknown');
? selected.name
: TAPi18n.__('custom-field-dropdown-unknown');
} }
events() { events() {
return [ return [{
{ 'submit .js-card-customfield-dropdown'(evt) {
'submit .js-card-customfield-dropdown'(event) { evt.preventDefault();
event.preventDefault(); const value = this.find('select').value;
const value = this.find('select').value; this.card.setCustomField(this.customFieldId, value);
this.card.setCustomField(this.customFieldId, value);
},
}, },
]; }];
}
}.register('cardCustomField-dropdown'));
// cardCustomField-stringtemplate
class CardCustomFieldStringTemplate extends CardCustomField {
onCreated() {
super.onCreated();
this.customField = new CustomFieldStringTemplate(this.data().definition);
this.stringtemplateItems = new ReactiveVar(this.data().value ?? []);
} }
formattedValue() { }).register('cardCustomField-dropdown');
const ret = this.customField.getFormattedValue(this.data().value);
return ret;
}
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.stringtemplateItems.get();
this.card.setCustomField(this.customFieldId, items);
},
'keydown .js-card-customfield-stringtemplate-item'(event) {
if (event.keyCode === 13) {
event.preventDefault();
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
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);
}
if (event.metaKey || event.ctrlKey) {
this.find('button[type=submit]').click();
}
}
},
'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 ?? []);
},
},
];
}
}
CardCustomFieldStringTemplate.register('cardCustomField-stringtemplate');

View file

@ -1,67 +0,0 @@
.card-date {
display: block;
border-radius: 4px;
padding: 1px 3px;
background-color: #dbdbdb;
}
.card-date:hover,
.card-date.is-active {
background-color: #b3b3b3;
}
.card-date.current,
.card-date.almost-due,
.card-date.due,
.card-date.long-overdue {
color: #fff;
}
.card-date.current {
background-color: #5ba639;
}
.card-date.current:hover,
.card-date.current.is-active {
background-color: #46802c;
}
.card-date.almost-due {
background-color: #edc909;
}
.card-date.almost-due:hover,
.card-date.almost-due.is-active {
background-color: #bc9f07;
}
.card-date.due {
background-color: #fa3f00;
}
.card-date.due:hover,
.card-date.due.is-active {
background-color: #c73200;
}
.card-date.long-overdue {
background-color: #fd5d47;
}
.card-date.long-overdue:hover,
.card-date.long-overdue.is-active {
background-color: #fd3e24;
}
.card-date.end-date time::before {
content: "\f253";
}
.card-date.due-date time::before {
content: "\f090";
}
.card-date.start-date time::before {
content: "\f251";
}
.card-date.received-date time::before {
content: "\f08b";
}
.card-date time::before {
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
-webkit-font-smoothing: antialiased;
margin-right: 0.3em;
}
.customfield-date {
display: block;
border-radius: 4px;
padding: 1px 3px;
}

View file

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

View file

@ -1,54 +1,141 @@
import moment from 'moment/min/moment-with-locales'; // Edit received, start, due & end dates
import { TAPi18n } from '/imports/i18n'; BlazeComponent.extendComponent({
import { DatePicker } from '/client/lib/datepicker'; 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({
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
});
// editCardReceivedDatePopup // editCardReceivedDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated();
this.data().getReceived() && this.data().getReceived() && this.date.set(moment(this.data().getReceived()));
this.date.set(moment(this.data().getReceived()));
} }
_storeDate(date) { _storeDate(date) {
this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setReceived(date);
} }
_deleteDate() { _deleteDate() {
this.card.unsetReceived(); this.card.setReceived(null);
} }
}.register('editCardReceivedDatePopup')); }).register('editCardReceivedDatePopup');
// editCardStartDatePopup // editCardStartDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated();
this.data().getStart() && this.date.set(moment(this.data().getStart())); this.data().getStart() && this.date.set(moment(this.data().getStart()));
} }
onRendered() { onRendered() {
super.onRendered(); super.onRendered();
if (moment.isDate(this.card.getReceived())) { if (moment.isDate(this.card.getReceived())) {
this.$('.js-datepicker').datepicker( this.$('.js-datepicker').datepicker('setStartDate', this.card.getReceived());
'setStartDate',
this.card.getReceived(),
);
} }
} }
_storeDate(date) { _storeDate(date) {
this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setStart(date);
} }
_deleteDate() { _deleteDate() {
this.card.unsetStart(); this.card.setStart(null);
} }
}.register('editCardStartDatePopup')); }).register('editCardStartDatePopup');
// editCardDueDatePopup // editCardDueDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated('1970-01-01 17:00:00'); super.onCreated();
this.data().getDue() && this.date.set(moment(this.data().getDue())); this.data().getDue() && this.date.set(moment(this.data().getDue()));
} }
@ -60,18 +147,18 @@ import { DatePicker } from '/client/lib/datepicker';
} }
_storeDate(date) { _storeDate(date) {
this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setDue(date);
} }
_deleteDate() { _deleteDate() {
this.card.unsetDue(); this.card.setDue(null);
} }
}.register('editCardDueDatePopup')); }).register('editCardDueDatePopup');
// editCardEndDatePopup // editCardEndDatePopup
(class extends DatePicker { (class extends DatePicker {
onCreated() { onCreated() {
super.onCreated(moment().format('YYYY-MM-DD HH:mm')); super.onCreated();
this.data().getEnd() && this.date.set(moment(this.data().getEnd())); this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
} }
@ -83,13 +170,14 @@ import { DatePicker } from '/client/lib/datepicker';
} }
_storeDate(date) { _storeDate(date) {
this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm')); this.card.setEnd(date);
} }
_deleteDate() { _deleteDate() {
this.card.unsetEnd(); this.card.setEnd(null);
} }
}.register('editCardEndDatePopup')); }).register('editCardEndDatePopup');
// Display received, start, due & end dates // Display received, start, due & end dates
const CardDate = BlazeComponent.extendComponent({ const CardDate = BlazeComponent.extendComponent({
@ -106,14 +194,6 @@ const CardDate = BlazeComponent.extendComponent({
}, 60000); }, 60000);
}, },
showWeek() {
return this.date.get().week().toString();
},
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
},
showDate() { showDate() {
// this will start working once mquandalle:moment // this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5 // is updated to at least moment.js 2.10.5
@ -144,20 +224,17 @@ class CardReceivedDate extends CardDate {
const startAt = this.data().getStart(); const startAt = this.data().getStart();
const theDate = this.date.get(); const theDate = this.date.get();
// if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged // if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
if ( if (((startAt) && (theDate.isAfter(dueAt))) ||
(startAt && theDate.isAfter(startAt)) || ((endAt) && (theDate.isAfter(endAt))) ||
(endAt && theDate.isAfter(endAt)) || ((dueAt) && (theDate.isAfter(dueAt))))
(dueAt && theDate.isAfter(dueAt))
)
classes += 'long-overdue'; classes += 'long-overdue';
else classes += 'current'; else
classes += 'current';
return classes; return classes;
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-received-on')} ${this.date return `${TAPi18n.__('card-received-on')} ${this.date.get().format('LLLL')}`;
.get()
.format('LLLL')}`;
} }
events() { events() {
@ -184,10 +261,13 @@ class CardStartDate extends CardDate {
const theDate = this.date.get(); const theDate = this.date.get();
const now = this.now.get(); const now = this.now.get();
// 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.isAfter(now)) classes += ''; else if (theDate.isBefore(now, 'minute'))
else classes += 'current'; classes += 'almost-due';
else
classes += 'current';
return classes; return classes;
} }
@ -218,12 +298,17 @@ class CardDueDate extends CardDate {
const theDate = this.date.get(); const theDate = this.date.get();
const now = this.now.get(); const now = this.now.get();
// if the due date is after the end date, green - done early // if the due date is after the end date, green - done early
if (endAt && theDate.isAfter(endAt)) classes += 'current'; if ((endAt) && (theDate.isAfter(endAt)))
classes += 'current';
// if there is an end date, don't need to flag the due date // if there is an end date, don't need to flag the due date
else if (endAt) classes += ''; else if (endAt)
else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue'; classes += '';
else if (now.diff(theDate, 'minute') >= 0) classes += 'due'; else if (now.diff(theDate, 'days') >= 2)
else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due'; classes += 'long-overdue';
else if (now.diff(theDate, 'minute') >= 0)
classes += 'due';
else if (now.diff(theDate, 'days') >= -1)
classes += 'almost-due';
return classes; return classes;
} }
@ -252,9 +337,12 @@ class CardEndDate extends CardDate {
let classes = 'end-date' + ' '; let classes = 'end-date' + ' ';
const dueAt = this.data().getDue(); const dueAt = this.data().getDue();
const theDate = this.date.get(); const theDate = this.date.get();
if (!dueAt) classes += ''; if (theDate.diff(dueAt, 'days') >= 2)
else if (theDate.isBefore(dueAt)) classes += 'current'; classes += 'long-overdue';
else if (theDate.isAfter(dueAt)) classes += 'due'; else if (theDate.diff(dueAt, 'days') >= 0)
classes += 'due';
else if (theDate.diff(dueAt, 'days') >= -2)
classes += 'almost-due';
return classes; return classes;
} }
@ -270,130 +358,26 @@ 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));
});
}
showWeek() {
return this.date.get().week().toString();
}
showWeekOfYear() {
return ReactiveCache.getCurrentUser().isShowWeekOfYear();
}
showDate() {
// this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5
// until then, the date is displayed in the "L" format
return this.date.get().calendar(null, {
sameElse: 'llll',
});
}
showTitle() {
return `${this.date.get().format('LLLL')}`;
}
classes() {
return 'customfield-date';
}
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');
} }
}.register('minicardReceivedDate')); }).register('minicardReceivedDate');
(class extends CardStartDate { (class extends CardStartDate {
showDate() { showDate() {
return this.date.get().format('YYYY-MM-DD HH:mm'); return this.date.get().format('l');
} }
}.register('minicardStartDate')); }).register('minicardStartDate');
(class extends CardDueDate { (class extends CardDueDate {
showDate() { showDate() {
return this.date.get().format('YYYY-MM-DD HH:mm'); return this.date.get().format('l');
} }
}.register('minicardDueDate')); }).register('minicardDueDate');
(class extends CardEndDate { (class extends CardEndDate {
showDate() { showDate() {
return this.date.get().format('YYYY-MM-DD HH:mm'); return this.date.get().format('l');
} }
}.register('minicardEndDate')); }).register('minicardEndDate');
(class extends CardCustomFieldDate {
showDate() {
return this.date.get().format('L');
}
}.register('minicardCustomFieldDate'));
class VoteEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getVoteEnd()));
});
}
classes() {
const classes = 'end-date' + ' ';
return classes;
}
showDate() {
return this.date.get().format('L LT');
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
}
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editVoteEndDate'),
});
}
}
VoteEndDate.register('voteEndDate');
class PokerEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(moment(self.data().getPokerEnd()));
});
}
classes() {
const classes = 'end-date' + ' ';
return classes;
}
showDate() {
return this.date.get().format('l LT');
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
}
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editPokerEndDate'),
});
}
}
PokerEndDate.register('pokerEndDate');

View file

@ -0,0 +1,59 @@
.card-date
display: block
border-radius: 4px
padding: 1px 3px
background-color: #dbdbdb
&:hover, &.is-active
background-color: #b3b3b3
&.current, &.almost-due, &.due, &.long-overdue
color: #fff
&.current
background-color: #5ba639
&:hover, &.is-active
background-color: darken(#5ba639, 10)
&.almost-due
background-color: #edc909
&:hover, &.is-active
background-color: darken(#edc909, 10)
&.due
background-color: #fa3f00
&:hover, &.is-active
background-color: darken(#fa3f00, 10)
&.long-overdue
background-color: #fd5d47
&:hover, &.is-active
background-color: darken(#fd5d47, 7)
&.end-date
time
&::before
content: "\f253" // symbol: fa-hourglass-end
&.due-date
time
&::before
content: "\f090" // symbol: fa-sign-in
&.start-date
time
&::before
content: "\f251" // symbol: fa-hourglass-start
&.received-date
time
&::before
content: "\f08b" // symbol: fa-sign-out
time
&::before
font: normal normal normal 14px/1 FontAwesome
font-size: inherit
-webkit-font-smoothing: antialiased
margin-right: 0.3em

View file

@ -1,56 +0,0 @@
.new-description {
position: relative;
margin: 0 0 20px 0;
}
.new-description.is-open .helper {
display: inline-block;
}
.new-description.is-open textarea {
min-height: 100px;
color: #4d4d4d;
cursor: auto;
overflow: hidden;
word-wrap: break-word;
}
.new-description .too-long {
margin-top: 8px;
}
.new-description textarea {
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
height: 36px;
margin: 4px 4px 6px 0;
padding: 9px 11px;
width: 100%;
}
.new-description textarea:hover,
.new-description textarea:is-open {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.33);
border: 0;
cursor: pointer;
}
.new-description textarea:is-open {
cursor: auto;
}
.description-item {
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
color: #8c8c8c;
height: 36px;
margin: 4px 4px 6px 0;
width: 92%;
}
.description-item:hover {
background: #e0e0e0;
}
.description-item.add-description {
display: flex;
margin: 5px;
}
.description-item.add-description a {
display: block;
margin: auto;
}

View file

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

View file

@ -1,37 +0,0 @@
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)) {
const submitButton = this.find('button[type=submit]');
if (submitButton) {
submitButton.click();
}
}
},
},
];
},
}).register('descriptionForm');

View file

@ -1,598 +0,0 @@
.assignee {
border-radius: 3px;
display: block;
position: relative;
float: left;
height: 30px;
width: 30px;
margin: .3vh;
cursor: pointer;
user-select: none;
z-index: 1;
text-decoration: none;
border-radius: 50%;
}
.assignee .avatar {
overflow: hidden;
border-radius: 50%;
}
.assignee .avatar.avatar-assignee-initials {
height: 70%;
width: 70%;
padding: 15%;
background-color: #dbdbdb;
color: #444;
position: absolute;
}
.assignee .avatar.avatar-image {
object-fit: cover;
object-position: center;
height: 100%;
width: 100%;
}
.assignee .assignee-presence-status {
background-color: #b3b3b3;
border: 1px solid #fff;
border-radius: 50%;
height: 7px;
width: 7px;
position: absolute;
right: -1px;
bottom: -1px;
border: 1px solid #fff;
z-index: 15;
}
.assignee .assignee-presence-status.active {
background: #64c464;
border-color: #daf1da;
}
.assignee .assignee-presence-status.idle {
background: #e4e467;
border-color: #f7f7d4;
}
.assignee .assignee-presence-status.disconnected {
background: #bdbdbd;
border-color: #ededed;
}
.assignee .assignee-presence-status.pending {
background: #e44242;
border-color: #f1dada;
}
.assignee.add-assignee {
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 2px #bfbfbf inset;
}
.assignee.add-assignee:hover,
.assignee.add-assignee.is-active {
box-shadow: 0 0 0 2px #666 inset;
}
.copied-tooltip {
display: none;
padding: 0px 10px;
background-color: rgba(0,0,0,0.875);
color: #fff;
border-radius: 5px;
}
.card-details {
padding: 0;
flex-shrink: 0;
flex-basis: 600px;
will-change: flex-basis;
overflow-y: scroll;
overflow-x: hidden;
background: #f7f7f7;
border-radius: bottom 3px;
z-index: 30;
animation: flexGrowIn 0.1s;
box-shadow: 0 0 7px 0 #b3b3b3;
transition: flex-basis 0.1s;
box-sizing: border-box;
}
.card-details .mCustomScrollBox {
padding-left: 0;
}
.card-details .card-details-canvas {
width: auto;
padding: 0 20px;
}
.card-details .card-details-header {
margin: 0 -20px 5px;
padding: 7px 20px;
background: #ededed;
border-bottom: 1px solid #dbdbdb;
position: sticky;
top: 0px;
z-index: 500;
}
.card-details .card-details-header .card-number {
color: #b3b3b3;
display: inline-block;
margin-right: 5px;
}
.card-details .card-details-header .close-card-details,
.card-details .card-details-header .maximize-card-details,
.card-details .card-details-header .minimize-card-details,
.card-details .card-details-header .card-details-menu,
.card-details .card-details-header .card-copy-button,
.card-details .card-details-header .card-copy-mobile-button,
.card-details .card-details-header .close-card-details-mobile-web,
.card-details .card-details-header .card-details-menu-mobile-web,
.card-details .card-details-header .copied-tooltip {
float: right;
}
.card-details .card-details-header .close-card-details,
.card-details .card-details-header .maximize-card-details,
.card-details .card-details-header .minimize-card-details {
font-size: 24px;
padding: 5px 10px 5px 10px;
margin-right: -8px;
}
.card-details .card-details-header .close-card-details-mobile-web {
font-size: 24px;
padding: 5px;
margin-right: 40px;
}
.card-details .card-details-header .card-copy-button {
font-size: 17px;
padding: 10px;
margin-right: 10px;
}
.card-details .card-details-header .card-copy-mobile-button {
font-size: 17px;
padding: 10px;
margin-right: 10px;
}
.card-details .card-details-header .card-details-menu {
font-size: 17px;
padding: 10px;
}
.card-details .card-details-header .card-details-menu-mobile-web {
font-size: 17px;
padding: 10px;
margin-right: 30px;
}
.card-details .card-details-header .card-details-watch {
font-size: 17px;
padding-left: 7px;
color: #a6a6a6;
}
.card-details .card-details-header .card-details-title {
font-weight: bold;
font-size: 1.33em;
margin: 7px 0 0;
padding: 0;
}
.card-details .card-details-header .linked-card-location {
font-style: italic;
font-size: 1em;
margin-bottom: 0;
}
.card-details .card-details-header .linked-card-location p {
margin-bottom: 0;
}
.card-details .card-details-header form.inlined-form {
margin-top: 5px;
margin-bottom: 10px;
}
.card-details .card-details-header form.inlined-form .copied-tooltip {
padding: 0px 10px;
}
.card-details .card-details-header .card-details-list {
font-size: 0.85em;
margin-bottom: 3px;
}
.card-details .card-details-header .card-details-list a.card-details-list-title {
font-weight: bold;
}
.card-details .card-details-header .card-details-list a.card-details-list-title.is-editable {
display: inline-block;
background: #e6e6e6;
border-radius: 3px;
padding: 0px 5px;
}
.card-details .card-details-header .copied-tooltip {
margin-right: 10px;
padding: 10px;
}
.card-details .card-description i.fa.fa-pencil-square-o {
float: right;
}
.card-details .card-description textarea {
min-height: 100px;
}
.card-details .card-details-items {
display: flex;
flex-wrap: wrap;
margin: 15px 0;
}
.card-details .card-details-items .card-details-item {
margin-right: 0.5em;
flex-grow: 1;
}
.card-details .card-details-items .card-details-item:last-child {
margin-right: 0;
}
.card-details .card-details-items .card-details-item.card-details-item-labels {
display: block;
word-wrap: break-word;
max-width: 95%;
}
.card-details .card-details-items .card-details-item.card-details-item-members,
.card-details .card-details-items .card-details-item.card-details-item-assignees,
.card-details .card-details-items .card-details-item.card-details-item-customfield,
.card-details .card-details-items .card-details-item.card-details-item-name {
display: block;
word-wrap: break-word;
max-width: 36%;
}
.card-details .card-details-items .card-details-item.card-details-item-creator,
.card-details .card-details-items .card-details-item.card-details-item-received,
.card-details .card-details-items .card-details-item.card-details-item-start,
.card-details .card-details-items .card-details-item.card-details-item-due,
.card-details .card-details-items .card-details-item.card-details-item-end {
display: block;
word-wrap: break-word;
max-width: 28%;
}
.card-details .card-details-items .card-details-item.custom-fields {
padding-left: 10px;
}
.card-details .card-details-item-title {
font-size: 16px;
font-weight: bold;
color: #4d4d4d;
}
.card-details .activities {
padding-top: 10px;
}
@media screen and (min-width: 801px) {
.card-details {
top: 97px;
left: calc(50% - (600px / 2));
width: 600px;
bottom: 0;
position: fixed;
resize: both;
}
.card-details-maximized {
padding: 0;
flex-shrink: 0;
flex-basis: calc(100% - 20px);
will-change: flex-basis;
overflow-y: scroll;
overflow-x: scroll;
background: #f7f7f7;
border-radius: bottom 3px;
z-index: 100;
animation: flexGrowIn 0.1s;
box-shadow: 0 0 7px 0 #b3b3b3;
transition: flex-basis 0.1s;
box-sizing: border-box;
top: 97px;
left: 0px;
height: calc(100% - 100px);
width: calc(100% - 20px);
float: left;
}
.card-details-maximized .card-details-left {
float: left;
top: 60px;
left: 20px;
width: 47%;
border-right: solid 2px #dbdbdb;
padding-right: 10px;
}
.card-details-maximized .card-details-right {
position: absolute;
float: right;
left: 50%;
margin: 15px 0;
}
.card-details-maximized .card-details-header {
width: 100%;
}
}
input[type="text"].attachment-add-link-input {
float: left;
margin: 0 0 8px;
width: 80%;
}
input[type="submit"].attachment-add-link-submit {
float: left;
margin: 0 0 8px 4px;
padding: 6px 12px;
width: 18%;
}
@media screen and (max-width: 800px) {
.card-details {
width: calc(100% - 1px);
padding: 0px 20px 0px 20px;
margin: 0px;
transition: none;
overflow-y: revert;
overflow-x: revert;
}
.card-details .card-details-canvas {
width: 100%;
padding-left: 0px;
}
.card-details .card-details-header .close-card-details {
margin-right: 0px;
}
.card-details .card-details-header .card-details-menu {
margin-right: 40px;
}
.card-details .card-details-header .maximize-card-details {
margin-right: 40px;
}
.card-details .card-details-header .minimize-card-details {
margin-right: 40px;
}
.card-details-popup {
padding: 0px 10px;
}
.pop-over > .content-wrapper > .popup-container-depth-0 {
width: 100%;
}
.pop-over > .content-wrapper > .popup-container-depth-0 > .content {
width: calc(100% - 10px);
}
.pop-over > .content-wrapper > .popup-container-depth-0 > .content > .card-details-popup hr {
margin: 15px 0px;
}
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
margin: 0;
}
}
.card-details-white {
background: #fff !important;
color: #000 !important;
border: 1px solid #eee;
}
.card-details-green {
background: #3cb500 !important;
color: #fff !important;
}
.card-details-yellow {
background: #fad900 !important;
color: #000 !important;
}
.card-details-orange {
background: #ff9f19 !important;
color: #000 !important;
}
.card-details-red {
background: #eb4646 !important;
color: #fff !important;
}
.card-details-purple {
background: #a632db !important;
color: #fff !important;
}
.card-details-blue {
background: #0079bf !important;
color: #fff !important;
}
.card-details-pink {
background: #ff78cb !important;
color: #000 !important;
}
.card-details-sky {
background: #00c2e0 !important;
color: #fff !important;
}
.card-details-black {
background: #4d4d4d !important;
color: #fff !important;
}
.card-details-lime {
background: #51e898 !important;
color: #000 !important;
}
.card-details-silver {
background: #c0c0c0 !important;
color: #000 !important;
}
.card-details-peachpuff {
background: #ffdab9 !important;
color: #000 !important;
}
.card-details-crimson {
background: #dc143c !important;
color: #fff !important;
}
.card-details-plum {
background: #dda0dd !important;
color: #000 !important;
}
.card-details-darkgreen {
background: #006400 !important;
color: #fff !important;
}
.card-details-slateblue {
background: #6a5acd !important;
color: #fff !important;
}
.card-details-magenta {
background: #f0f !important;
color: #fff !important;
}
.card-details-gold {
background: #ffd700 !important;
color: #000 !important;
}
.card-details-navy {
background: #000080 !important;
color: #fff !important;
}
.card-details-gray {
background: #808080 !important;
color: #fff !important;
}
.card-details-saddlebrown {
background: #8b4513 !important;
color: #fff !important;
}
.card-details-paleturquoise {
background: #afeeee !important;
color: #000 !important;
}
.card-details-mistyrose {
background: #ffe4e1 !important;
color: #000 !important;
}
.card-details-indigo {
background: #4b0082 !important;
color: #fff !important;
}
.voted {
opacity: 0.7;
}
.vote-title {
display: flex;
justify-content: space-between;
}
.vote-title .js-edit-date {
align-self: baseline;
margin-left: 5px;
}
.vote-result {
display: flex;
}
.js-show-positive-votes {
cursor: pointer;
}
.poker-voted {
opacity: 0.7;
}
.poker-title {
display: flex;
justify-content: space-between;
}
.poker-title .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: 0.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;
}
.estimation-add input {
display: inline-block;
float: right;
margin: auto;
margin-right: 10px;
width: 100px;
}
.estimation-add 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 #aaa;
text-align: center;
position: relative;
cursor: pointer;
}
.poker-card .inner {
display: table-cell;
vertical-align: middle;
border-radius: 5px;
overflow: hidden;
background-color: #cecece;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,134 @@
@import 'nib'
.card-details
padding: 0 20px
flex-shrink: 0
flex-basis: 470px
will-change: flex-basis
overflow-y: scroll
overflow-x: hidden
background: darken(white, 3%)
border-radius: bottom 3px
z-index: 20 !important
animation: flexGrowIn 0.1s
box-shadow: 0 0 7px 0 darken(white, 30%)
transition: flex-basis 0.1s
.card-details-canvas
width: 470px
.card-details-header
margin: 0 -20px 5px
padding 7px 16px
background: darken(white, 7%)
border-bottom: 1px solid darken(white, 14%)
.close-card-details,
.card-details-menu
float: right
.close-card-details
font-size: 24px
padding: 5px
margin-right: -8px
.card-details-menu
font-size: 17px
padding: 10px
.card-details-watch
font-size: 17px
padding-left: 7px
color: #a6a6a6
.card-details-title
font-weight: bold
font-size: 1.33em
margin: 7px 0 0
padding: 0
.linked-card-location
font-style: italic
font-size: 1em
margin-bottom: 0
& p
margin-bottom: 0
form.inlined-form
margin-top: 5px
margin-bottom: 10px
.card-details-list
font-size: 0.85em
margin-bottom: 3px
a.card-details-list-title
font-weight: bold
&.is-editable
display: inline-block
background: darken(white, 10%)
border-radius: 3px
padding: 0px 5px
.card-description textarea
min-height: 100px
.card-details-items
display: flex
flex-wrap: wrap
margin: 15px 0
.card-details-item
margin-right: 0.5em
&:last-child
margin-right: 0
&.card-details-item-labels,
&.card-details-item-members,
&.card-details-item-received,
&.card-details-item-start,
&.card-details-item-due,
&.card-details-item-end,
&.card-details-item-customfield,
&.card-details-item-name
max-width: 50%
flex-grow: 1
.card-details-item-title
font-size: 16px
color: #000
.card-label
padding-top: 5px
padding-bottom: 5px
.activities
padding-top: 10px
input[type="text"].attachment-add-link-input
float: left
margin: 0 0 8px
width: 80%
input[type="submit"].attachment-add-link-submit
float: left
margin: 0 0 8px 4px
padding: 6px 12px
width: 18%
@media screen and (max-width: 800px)
.card-details
width: calc(100% - 40px)
padding: 0px 20px 0px 20px
margin: 0px
transition: none
.card-details-canvas
width: 100%
.card-details-header
.close-card-details
margin-right: 0px
.card-details-menu
margin-right: 10px

View file

@ -1,18 +0,0 @@
.card-time {
display: block;
border-radius: 4px;
padding: 1px 3px;
color: #fff;
background-color: #dbdbdb;
}
.card-time:hover,
.card-time.is-active {
background-color: #b3b3b3;
}
.card-time time::before {
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
-webkit-font-smoothing: antialiased;
content: "\f017";
margin-right: 0.3em;
}

View file

@ -1,5 +1,3 @@
import { TAPi18n } from '/imports/i18n';
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template() { template() {
return 'editCardSpentTime'; return 'editCardSpentTime';
@ -11,6 +9,7 @@ BlazeComponent.extendComponent({
toggleOvertime() { toggleOvertime() {
this.card.setIsOvertime(!this.card.getIsOvertime()); this.card.setIsOvertime(!this.card.getIsOvertime());
$('#overtime .materialCheckBox').toggleClass('is-checked'); $('#overtime .materialCheckBox').toggleClass('is-checked');
$('#overtime').toggleClass('is-checked'); $('#overtime').toggleClass('is-checked');
}, },
storeTime(spentTime, isOvertime) { storeTime(spentTime, isOvertime) {
@ -19,37 +18,31 @@ BlazeComponent.extendComponent({
}, },
deleteTime() { deleteTime() {
this.card.setSpentTime(null); this.card.setSpentTime(null);
this.card.setIsOvertime(false);
}, },
events() { events() {
return [ return [{
{ //TODO : need checking this portion
//TODO : need checking this portion 'submit .edit-time'(evt) {
'submit .edit-time'(evt) { evt.preventDefault();
evt.preventDefault();
const spentTime = parseFloat(evt.target.time.value); const spentTime = parseFloat(evt.target.time.value);
//const isOvertime = this.card.getIsOvertime(); const isOvertime = this.card.getIsOvertime();
let isOvertime = false;
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) { if (spentTime >= 0) {
isOvertime = true; this.storeTime(spentTime, isOvertime);
} Popup.close();
if (spentTime >= 0) { } else {
this.storeTime(spentTime, isOvertime); this.error.set('invalid-time');
Popup.back(); evt.target.time.focus();
} else { }
this.error.set('invalid-time');
evt.target.time.focus();
}
},
'click .js-delete-time'(evt) {
evt.preventDefault();
this.deleteTime();
Popup.back();
},
'click a.js-toggle-overtime': this.toggleOvertime,
}, },
]; 'click .js-delete-time'(evt) {
evt.preventDefault();
this.deleteTime();
Popup.close();
},
'click a.js-toggle-overtime': this.toggleOvertime,
}];
}, },
}).register('editCardSpentTimePopup'); }).register('editCardSpentTimePopup');
@ -63,23 +56,23 @@ BlazeComponent.extendComponent({
}, },
showTitle() { showTitle() {
if (this.data().getIsOvertime()) { if (this.data().getIsOvertime()) {
return `${TAPi18n.__( return `${TAPi18n.__('overtime')} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
'overtime',
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
} else { } else {
return `${TAPi18n.__( return `${TAPi18n.__('card-spent')} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
'card-spent',
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
} }
}, },
showTime() { showTime() {
return this.data().getSpentTime(); return this.data().getSpentTime();
}, },
events() { events() {
return [ return [{
{ 'click .js-edit-time': Popup.open('editCardSpentTime'),
'click .js-edit-time': Popup.open('editCardSpentTime'), }];
},
];
}, },
}).register('cardSpentTime'); }).register('cardSpentTime');
Template.timeBadge.helpers({
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
});

View file

@ -0,0 +1,17 @@
.card-time
display: block
border-radius: 4px
padding: 1px 3px
color: #fff
background-color: #dbdbdb
&:hover, &.is-active
background-color: #b3b3b3
time
&::before
font: normal normal normal 14px/1 FontAwesome
font-size: inherit
-webkit-font-smoothing: antialiased
content: "\f017" // clock symbol
margin-right: 0.3em

View file

@ -1,191 +0,0 @@
.js-add-checklist {
color: #8c8c8c;
}
textarea.js-add-checklist-item,
textarea.js-edit-checklist-item {
overflow: hidden;
word-wrap: break-word;
resize: none;
height: 34px;
}
.delete-text,
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {
color: #8c8c8c;
text-decoration: underline;
word-wrap: break-word;
float: right;
padding-top: 6px;
}
.delete-text:hover,
.js-delete-checklist-item:hover,
.js-convert-checklist-item-to-card:hover {
color: inherit;
}
.checklists-title {
display: flex;
justify-content: space-between;
}
.checklist-progress-bar-container {
display: flex;
flex-direction: row;
align-items: center;
}
.checklist-progress-bar-container .checklist-progress-text {
margin-right: 10px;
}
.checklist-progress-bar-container .checklist-progress-bar {
width: 80%;
height: 10px;
}
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
color: #fff !important;
background-color: #2196f3 !important;
padding: 0.01em 16px;
border-radius: 16px;
height: 100%;
}
.checklist-title {
padding: 10px;
}
.checklist-title .checkbox {
float: left;
width: 30px;
height: 30px;
font-size: 18px;
line-height: 30px;
}
.checklist-title .title {
font-size: 18px;
line-height: 25px;
}
.checklist-title .checklist-stat {
margin: 0 0.5em;
float: right;
padding-top: 6px;
}
.checklist-title .checklist-stat.is-finished {
color: #3cb500;
}
.checklist-title span.fa.checklist-handle {
padding-right: 20px;
padding-top: 3px;
float: left;
}
#card-details-overlay {
top: 0;
bottom: -600px;
right: 0;
}
.checklist {
background: #f7f7f7;
}
.checklist.placeholder {
background: #ccc;
border-radius: 2px;
}
.checklist.ui-sortable-helper {
box-shadow: -2px 2px 8px rgba(0,0,0,0.3), 0 0 1px rgba(0,0,0,0.5);
transform: rotate(4deg);
cursor: grabbing;
}
.checklist-item {
margin: 0 0 0 0.1em;
line-height: 18px;
font-size: 1.1em;
margin-top: 3px;
display: flex;
background: #f7f7f7;
opacity: 1;
transition: height 0ms 400ms, opacity 400ms 0ms;
height: auto;
overflow: hidden;
}
.checklist-item.is-checked.invisible {
opacity: 0;
height: 0;
transition: height 0ms 0ms, opacity 600ms 0ms;
margin-top: 0;
margin-bottom: 0;
}
.checklist-item.placeholder {
background: #ccc;
border-radius: 2px;
}
.checklist-item.ui-sortable-helper {
box-shadow: -2px 2px 8px rgba(0,0,0,0.3), 0 0 1px rgba(0,0,0,0.5);
transform: rotate(4deg);
cursor: grabbing;
}
.checklist-item:hover {
background-color: #ebebeb;
}
.checklist-item .check-box-container {
padding-right: 10px;
}
.checklist-item .check-box {
margin: 0.1em 0 0 0;
}
.checklist-item .check-box.is-checked {
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
}
.checklist-item .item-title {
flex: 1;
}
.checklist-item .item-title.is-checked {
color: #8c8c8c;
font-style: italic;
text-decoration: line-through;
}
.checklist-item .item-title .viewer p {
margin-bottom: 2px;
display: block;
word-wrap: break-word;
max-width: 420px;
}
.checklist-item span.fa.checklistitem-handle {
padding-top: 2px;
padding-right: 10px;
}
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {
margin: 0 0 0.5em 1.33em;
padding: 12px 0 0 0;
}
.add-checklist-item {
margin: 0.2em 0 0.5em 1.33em;
}
.add-checklist-item.js-open-inlined-form,
.add-checklist.js-open-inlined-form {
display: block;
width: 50%;
}
.add-checklist-item.js-open-inlined-form:hover,
.add-checklist.js-open-inlined-form:hover {
background: #dbdbdb;
color: #222;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.add-checklist-top {
/* more space to checklists title */
padding-left: 20px;
/* + is easier clickable */
padding-right: 20px;
}
.add-checklist-top.js-open-inlined-form:hover {
background: #dbdbdb;
color: #222;
box-shadow: 0 1px 2px rgba(0,0,0,.2);
}
.card-details-item-title {
/* max width for adding checklist at top */
width: 100%;
}
.checklist-details-menu {
float: right;
padding: 6px 10px 6px 10px;
}
.edit-controls label.toggle-label {
margin-left: 2px;
}

View file

@ -1,132 +1,92 @@
template(name="checklists") template(name="checklists")
.checklists-title h3 {{_ 'checklists'}}
h3.card-details-item-title if toggleDeleteDialog.get
i.fa.fa-check .board-overlay#card-details-overlay
| {{_ 'checklists'}} +checklistDeleteDialog(checklist = checklistToDelete)
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
+addChecklistItemForm
else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
i.fa.fa-plus
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
//span.toggle-switch-title
if card.hideFinishedChecklistIfItemsAreHidden
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist")
label.toggle-label(for="toggleHideFinishedChecklist")
.card-checklist-items .card-checklist-items
each checklist in checklists each checklist in currentCard.checklists
if checklist.showChecklist card.hideFinishedChecklistIfItemsAreHidden +checklistDetail(checklist = checklist)
+checklistDetail(checklist = checklist card = card)
if canModifyCard if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId) +inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false) +addChecklistItemForm
else else
a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}") a.js-open-inlined-form
i.fa.fa-plus i.fa.fa-plus
| {{_ 'add-checklist'}}...
template(name="checklistDetail") template(name="checklistDetail")
.js-checklist.checklist.nodragscroll .js-checklist.checklist
+inlinedForm(classNames="js-edit-checklist-title" checklist = checklist) +inlinedForm(classNames="js-edit-checklist-title" checklist = checklist)
+editChecklistItemForm(checklist = checklist) +editChecklistItemForm(checklist = checklist)
else else
.checklist-title .checklist-title
span span
if canModifyCard if canModifyCard
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}") a.js-delete-checklist.toggle-delete-checklist-dialog {{_ "delete"}}...
if canModifyCard if canModifyCard
h4.title.js-open-inlined-form.is-editable h2.title.js-open-inlined-form.is-editable
if isTouchScreenOrShowDesktopDragHandles
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
+viewer +viewer
= checklist.title = checklist.title
else else
h4.title h2.title
+viewer +viewer
= checklist.title = checklist.title
+checklistItems(checklist = checklist)
if $gt finishedPercent 0 template(name="checklistDeleteDialog")
.checklist-progress-bar-container .js-confirm-checklist-delete
.checklist-progress-text {{finishedPercent}}% p
.checklist-progress-bar i(class="fa fa-exclamation-triangle" aria-hidden="true")
.checklist-progress(style="width:{{finishedPercent}}%") p
+checklistItems(checklist = checklist card = card) | {{_ 'confirm-checklist-delete-dialog'}}
span {{checklist.title}}
template(name="checklistDeletePopup") | ?
p {{_ 'confirm-checklist-delete-popup'}} .js-checklist-delete-buttons
button.js-confirm.negate.full(type="submit") {{_ 'delete'}} button.confirm-checklist-delete(type="button") {{_ 'delete'}}
button.toggle-delete-checklist-dialog(type="button") {{_ 'cancel'}}
template(name="addChecklistItemForm") template(name="addChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-add-checklist-item(rows='1' autofocus) textarea.js-add-checklist-item(rows='1' autofocus)
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}} button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}") a.fa.fa-times-thin.js-close-inlined-form
if showNewlineBecomesNewChecklistItem
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
label.toggle-label(for="toggleNewlineBecomesNewChecklistItem")
| {{_ 'newLineNewItem'}}
if $eq position 'top'
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItemOriginOrder'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItemOriginOrder")
label.toggle-label(for="toggleNewlineBecomesNewChecklistItemOriginOrder")
| {{_ 'originOrder'}}
template(name="editChecklistItemForm") template(name="editChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}") textarea.js-edit-checklist-item(rows='1' autofocus)
span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item' if $eq type 'item'
= item.title = item.title
else else
= checklist.title = checklist.title
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}} button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}") 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-checklist-item {{_ "delete"}}... a.js-delete-checklist-item {{_ "delete"}}...
a.js-convert-checklist-item-to-card
i.fa.fa-copy
| {{_ 'convertChecklistItemToCardPopup-title'}}
template(name="checklistItems") template(name="checklistItems")
if checklist.items.length
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist position="top")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
.checklist-items.js-checklist-items .checklist-items.js-checklist-items
each item in checklist.items each item in checklist.items
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist) +inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
+editChecklistItemForm(type = 'item' item = item checklist = checklist) +editChecklistItemForm(type = 'item' item = item checklist = checklist)
else else
+checklistItemDetail(item = item checklist = checklist card = card) +checklistItemDetail(item = item checklist = checklist)
if canModifyCard if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist) +inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true) +addChecklistItemForm
else else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}") a.add-checklist-item.js-open-inlined-form
i.fa.fa-plus i.fa.fa-plus
| {{_ 'add-checklist-item'}}...
template(name='checklistItemDetail') template(name='checklistItemDetail')
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}" .js-checklist-item.checklist-item
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
if canModifyCard if canModifyCard
.check-box-container .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
if isTouchScreenOrShowDesktopDragHandles
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
@ -135,65 +95,3 @@ template(name='checklistItemDetail')
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}") .item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer +viewer
= item.title = item.title
template(name="checklistActionsPopup")
ul.pop-over-list
li
a.js-delete-checklist.delete-checklist
i.fa.fa-trash
| {{_ "delete"}} ...
a.js-move-checklist.move-checklist
i.fa.fa-arrow-right
| {{_ "moveChecklist"}} ...
a.js-copy-checklist.copy-checklist
i.fa.fa-copy
| {{_ "copyChecklist"}} ...
a.js-hide-checked-checklist-items
i.fa.fa-eye-slash
| {{_ "hideCheckedChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
if checklist.hideCheckedChecklistItems
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
a.js-hide-all-checklist-items
i.fa.fa-ban
| {{_ "hideAllChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
if checklist.hideAllChecklistItems
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
template(name="copyChecklistPopup")
+copyAndMoveChecklist
template(name="moveChecklistPopup")
+copyAndMoveChecklist
template(name="copyAndMoveChecklist")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}

View file

@ -1,11 +1,4 @@
import { ReactiveCache } from '/imports/reactiveCache'; const { calculateIndexData, enableClickOnTouch } = Utils;
import { TAPi18n } from '/imports/i18n';
import Cards from '/models/cards';
import Boards from '/models/boards';
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
const subManager = new SubsManager();
const { calculateIndexData, capitalize } = Utils;
function initSorting(items) { function initSorting(items) {
items.sortable({ items.sortable({
@ -13,13 +6,13 @@ 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: 'parent', appendTo: '.board-canvas',
distance: 7, distance: 7,
placeholder: 'checklist-item placeholder', placeholder: 'checklist-item placeholder',
scroll: true, scroll: false,
start(evt, ui) { start(evt, ui) {
ui.placeholder.height(ui.helper.height()); ui.placeholder.height(ui.helper.height());
EscapeActions.clickExecute(evt.target, 'inlinedForm'); EscapeActions.executeUpTo('popup-close');
}, },
stop(evt, ui) { stop(evt, ui) {
const parent = ui.item.parents('.js-checklist-items'); const parent = ui.item.parents('.js-checklist-items');
@ -43,6 +36,9 @@ 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({
@ -50,113 +46,84 @@ BlazeComponent.extendComponent({
const self = this; const self = this;
self.itemsDom = this.$('.js-checklist-items'); self.itemsDom = this.$('.js-checklist-items');
initSorting(self.itemsDom); initSorting(self.itemsDom);
self.itemsDom.mousedown(function (evt) { self.itemsDom.mousedown(function(evt) {
evt.stopPropagation(); evt.stopPropagation();
}); });
function userIsMember() { function userIsMember() {
return ReactiveCache.getCurrentUser()?.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
self.autorun(() => { self.autorun(() => {
const $itemsDom = $(self.itemsDom); const $itemsDom = $(self.itemsDom);
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) { if ($itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', !userIsMember()); $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(self.itemsDom).sortable({
handle: 'span.fa.checklistitem-handle',
});
}
} }
}); });
}, },
/** returns the finished percent of the checklist */ canModifyCard() {
finishedPercent() { return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
const ret = this.data().checklist.finishedPercent();
return ret;
}, },
}).register('checklistDetail'); }).register('checklistDetail');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
addChecklist(event) { addChecklist(event) {
event.preventDefault(); event.preventDefault();
const textarea = this.find('textarea.js-add-checklist-item'); const textarea = this.find('textarea.js-add-checklist-item');
const title = textarea.value.trim(); const title = textarea.value.trim();
let cardId = this.currentData().cardId; let cardId = this.currentData().cardId;
const card = ReactiveCache.getCard(cardId); const card = Cards.findOne(cardId);
//if (card.isLinked()) cardId = card.linkedId; if (card.isLinked())
if (card.isLinkedCard()) {
cardId = card.linkedId; cardId = card.linkedId;
}
let sortIndex;
let checklistItemIndex;
if (this.currentData().position === 'top') {
sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base;
checklistItemIndex = 0;
} else {
sortIndex = Utils.calculateIndexData(card.lastChecklist(), null).base;
checklistItemIndex = -1;
}
if (title) { if (title) {
Checklists.insert({ Checklists.insert({
cardId, cardId,
title, title,
sort: sortIndex, sort: card.checklists().count(),
}); });
this.closeAllInlinedForms();
setTimeout(() => { setTimeout(() => {
this.$('.add-checklist-item') this.$('.add-checklist-item').last().click();
.eq(checklistItemIndex)
.click();
}, 100); }, 100);
} }
textarea.value = '';
textarea.focus();
}, },
addChecklistItem(event) { addChecklistItem(event) {
event.preventDefault(); event.preventDefault();
const textarea = this.find('textarea.js-add-checklist-item'); const textarea = this.find('textarea.js-add-checklist-item');
const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
const newlineBecomesNewChecklistItemOriginOrder = this.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
const title = textarea.value.trim(); const title = textarea.value.trim();
const checklist = this.currentData().checklist; const checklist = this.currentData().checklist;
if (title) { if (title) {
let checklistItems = [title]; ChecklistItems.insert({
if (newlineBecomesNewChecklistItem.checked) { title,
checklistItems = title.split('\n').map(_value => _value.trim()); checklistId: checklist._id,
if (this.currentData().position === 'top') { cardId: checklist.cardId,
if (newlineBecomesNewChecklistItemOriginOrder.checked === false) { sort: checklist.itemCount(),
checklistItems = checklistItems.reverse(); });
}
}
}
let addIndex;
let sortIndex;
if (this.currentData().position === 'top') {
sortIndex = Utils.calculateIndexData(null, checklist.firstItem()).base;
addIndex = -1;
} else {
sortIndex = Utils.calculateIndexData(checklist.lastItem(), null).base;
addIndex = 1;
}
for (let checklistItem of checklistItems) {
ChecklistItems.insert({
title: checklistItem,
checklistId: checklist._id,
cardId: checklist.cardId,
sort: sortIndex,
});
sortIndex += addIndex;
}
} }
// We keep the form opened, empty it. // We keep the form opened, empty it.
textarea.value = ''; textarea.value = '';
textarea.focus(); textarea.focus();
}, },
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
deleteChecklist() {
const checklist = this.currentData().checklist;
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
this.toggleDeleteDialog.set(false);
}
},
deleteItem() { deleteItem() {
const checklist = this.currentData().checklist; const checklist = this.currentData().checklist;
const item = this.currentData().item; const item = this.currentData().item;
@ -182,6 +149,11 @@ BlazeComponent.extendComponent({
item.setTitle(title); item.setTitle(title);
}, },
onCreated() {
this.toggleDeleteDialog = new ReactiveVar(false);
this.checklistToDelete = null; //Store data context to pass to checklistDeleteDialog template
},
pressKey(event) { pressKey(event) {
//If user press enter key inside a form, submit it //If user press enter key inside a form, submit it
//Unless the user is also holding down the 'shift' key //Unless the user is also holding down the 'shift' key
@ -192,161 +164,55 @@ 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();
}
},
/** closes all inlined forms (checklist and checklist-item input fields) */
closeAllInlinedForms() {
this.$('.js-close-inlined-form').click();
},
events() { events() {
return [ const events = {
{ 'click .toggle-delete-checklist-dialog'(event) {
'click .js-open-checklist-details-menu': Popup.open('checklistActions'), if($(event.target).hasClass('js-delete-checklist')){
'submit .js-add-checklist': this.addChecklist, this.checklistToDelete = this.currentData().checklist; //Store data context
'submit .js-edit-checklist-title': this.editChecklist, }
'submit .js-add-checklist-item': this.addChecklistItem, this.toggleDeleteDialog.set(!this.toggleDeleteDialog.get());
'submit .js-edit-checklist-item': this.editChecklistItem,
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
'click .js-delete-checklist-item': this.deleteItem,
'focus .js-add-checklist-item': this.focusChecklistItem,
// add and delete checklist / checklist-item
'click .js-open-inlined-form': this.closeAllInlinedForms,
'click #toggleHideFinishedChecklist'(event) {
event.preventDefault();
this.data().card.toggleHideFinishedChecklist();
},
keydown: this.pressKey,
}, },
]; };
return [{
...events,
'submit .js-add-checklist': this.addChecklist,
'submit .js-edit-checklist-title': this.editChecklist,
'submit .js-add-checklist-item': this.addChecklistItem,
'submit .js-edit-checklist-item': this.editChecklistItem,
'click .js-delete-checklist-item': this.deleteItem,
'click .confirm-checklist-delete': this.deleteChecklist,
keydown: this.pressKey,
}];
}, },
}).register('checklists'); }).register('checklists');
BlazeComponent.extendComponent({ Template.checklistDeleteDialog.onCreated(() => {
onCreated() { const $cardDetails = this.$('.card-details');
subManager.subscribe('board', Session.get('currentBoard'), false); this.scrollState = { position: $cardDetails.scrollTop(), //save current scroll position
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard')); top: false, //required for smooth scroll animation
}, };
//Callback's purpose is to only prevent scrolling after animation is complete
$cardDetails.animate({ scrollTop: 0 }, 500, () => { this.scrollState.top = true; });
boards() { //Prevent scrolling while dialog is open
const ret = ReactiveCache.getBoards( $cardDetails.on('scroll', () => {
{ if(this.scrollState.top) { //If it's already in position, keep it there. Otherwise let animation scroll
archived: false, $cardDetails.scrollTop(0);
'members.userId': Meteor.userId(), }
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() }, });
},
{
sort: { sort: 1 /* boards default sorting */ },
},
);
return ret;
},
swimlanes() {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
return board.swimlanes();
},
aBoardLists() {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
return board.lists();
},
events() {
return [
{
'change .js-select-boards'(event) {
this.selectedBoardId.set($(event.currentTarget).val());
subManager.subscribe('board', this.selectedBoardId.get(), false);
},
},
];
},
}).register('boardsSwimlanesAndLists');
Template.checklists.helpers({
checklists() {
const card = ReactiveCache.getCard(this.cardId);
const ret = card.checklists();
return ret;
},
}); });
BlazeComponent.extendComponent({ Template.checklistDeleteDialog.onDestroyed(() => {
onRendered() { const $cardDetails = this.$('.card-details');
autosize(this.$('textarea.js-add-checklist-item')); $cardDetails.off('scroll'); //Reactivate scrolling
}, $cardDetails.animate( { scrollTop: this.scrollState.position });
events() { });
return [
{
'click a.fa.fa-copy'(event) {
const $editor = this.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = this.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
}
];
}
}).register('addChecklistItemForm');
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
const checklist = this.checklist;
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
}
}),
'click .js-move-checklist': Popup.open('moveChecklist'),
'click .js-copy-checklist': Popup.open('copyChecklist'),
'click .js-hide-checked-checklist-items'(event) {
event.preventDefault();
this.data().checklist.toggleHideCheckedChecklistItems();
Popup.back();
},
'click .js-hide-all-checklist-items'(event) {
event.preventDefault();
this.data().checklist.toggleHideAllChecklistItems();
Popup.back();
},
}
]
}
}).register('checklistActionsPopup');
BlazeComponent.extendComponent({
onRendered() {
autosize(this.$('textarea.js-edit-checklist-item'));
},
events() {
return [
{
'click a.fa.fa-copy'(event) {
const $editor = this.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = this.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
}
];
}
}).register('editChecklistItemForm');
Template.checklistItemDetail.helpers({ Template.checklistItemDetail.helpers({
canModifyCard() {
return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly();
},
}); });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
@ -358,34 +224,8 @@ BlazeComponent.extendComponent({
} }
}, },
events() { events() {
return [ return [{
{ 'click .js-checklist-item .check-box': this.toggleItem,
'click .js-checklist-item .check-box-container': this.toggleItem, }];
},
];
}, },
}).register('checklistItemDetail'); }).register('checklistItemDetail');
/** Move Checklist Dialog */
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
return ret;
}
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
this.data().checklist.move(cardId);
}
}).register('moveChecklistPopup');
/** Copy Checklist Dialog */
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
return ret;
}
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
this.data().checklist.copy(cardId);
}
}).register('copyChecklistPopup');

View file

@ -0,0 +1,139 @@
.js-add-checklist
color: #8c8c8c
textarea.js-add-checklist-item, textarea.js-edit-checklist-item
overflow: hidden
word-wrap: break-word
resize: none
height: 34px
.delete-text
color: #8c8c8c
text-decoration: underline
word-wrap: break-word
float: right
padding-top: 6px
&:hover
color: inherit
.checklist-title
.checkbox
float: left
width: 30px
height 30px
font-size: 18px
line-height: 30px
.title
font-size: 18px
line-height: 25px
.checklist-stat
margin: 0 0.5em
float: right
padding-top: 6px
&.is-finished
color: #3cb500
.js-delete-checklist
@extends .delete-text
.js-confirm-checklist-delete
background-color: darken(white, 3%)
position: absolute
float: left;
width: 60%
margin-top: 0
margin-left: 13%
padding-bottom: 2%
padding-left: 3%
padding-right: 3%
z-index: 17
border-radius: 3px
p
position: relative
margin-top: 3%
width: 100%
text-align: center
span
font-weight: bold
i
font-size: 2em
.js-checklist-delete-buttons
position: relative
padding: left 2% right 2%
.confirm-checklist-delete
margin-left: 12%
float: left
.toggle-delete-checklist-dialog
margin-right: 12%
float: right
#card-details-overlay
top: 0
bottom: -600px
right: 0
.checklist
background: darken(white, 3%)
&.placeholder
background: darken(white, 20%)
border-radius: 2px
&.ui-sortable-helper
box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
0 0 1px rgba(0, 0, 0, .5)
transform: rotate(4deg)
cursor: grabbing
.checklist-item
margin: 0 0 0 0.1em
line-height: 18px
font-size: 1.1em
margin-top: 3px
display: flex
background: darken(white, 3%)
&.placeholder
background: darken(white, 20%)
border-radius: 2px
&.ui-sortable-helper
box-shadow: -2px 2px 8px rgba(0, 0, 0, .3),
0 0 1px rgba(0, 0, 0, .5)
transform: rotate(4deg)
cursor: grabbing
&:hover
background-color: darken(white, 8%)
.check-box
margin: 0.1em 0 0 0;
&.is-checked
border-bottom: 2px solid #3cb500
border-right: 2px solid #3cb500
.item-title
flex: 1
padding-left: 10px;
&.is-checked
color: #8c8c8c
font-style: italic
& .viewer
p
margin-bottom: 2px
.js-delete-checklist-item
margin: 0 0 0.5em 1.33em
@extends .delete-text
padding: 12px 0 0 0
.add-checklist-item
margin: 0.2em 0 0.5em 1.33em
display: inline-block

View file

@ -1,230 +0,0 @@
.card-label {
border: 1px solid #000;
border-radius: 4px;
color: #fff;
display: inline-block;
font-weight: 700;
font-size: 13px;
margin-right: 4px;
margin-bottom: 5px;
padding: 3px 8px;
max-width: 210px;
min-width: 8px;
word-wrap: break-word;
min-height: 18px;
vertical-align: middle;
white-space: initial;
overflow: initial;
}
.card-label:hover {
color: #fff;
}
.card-label.square {
height: 30px;
width: 30px;
padding: 0;
}
.card-label.add-label {
box-shadow: 0 0 0 2px #bfbfbf inset;
border: initial;
}
.card-label.add-label:hover,
.card-label.add-label.is-active {
box-shadow: 0 0 0 2px #666 inset;
}
.card-label p {
margin: 0px;
}
.palette-colors {
display: flex;
flex-wrap: wrap;
}
.palette-colors .palette-color {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
.card-label-white {
background-color: #fff;
color: #000;
}
.card-label-white:hover {
color: #aaa;
}
.card-label-green {
background-color: #3cb500;
}
.card-label-green:hover {
color: #000;
}
.card-label-yellow {
background-color: #fad900;
color: #000;
}
.card-label-orange {
background-color: #ff9f19;
color: #000;
}
.card-label-red {
background-color: #eb4646;
}
.card-label-purple {
background-color: #a632db;
}
.card-label-blue {
background-color: #0079bf;
}
.card-label-pink {
background-color: #ff78cb;
color: #000;
}
.card-label-sky {
background-color: #00c2e0;
}
.card-label-black {
background-color: #4d4d4d;
}
.card-label-lime {
background-color: #51e898;
color: #000;
}
.card-label-silver {
background-color: #c0c0c0;
color: #000;
}
.card-label-peachpuff {
background-color: #ffdab9;
color: #000;
}
.card-label-crimson {
background-color: #dc143c;
}
.card-label-plum {
background-color: #dda0dd;
color: #000;
}
.card-label-darkgreen {
background-color: #006400;
}
.card-label-slateblue {
background-color: #6a5acd;
}
.card-label-magenta {
background-color: #f0f;
}
.card-label-gold {
background-color: #ffd700;
color: #000;
}
.card-label-navy {
background-color: #000080;
}
.card-label-gray {
background-color: #808080;
}
.card-label-saddlebrown {
background-color: #8b4513;
}
.card-label-paleturquoise {
background-color: #afeeee;
color: #000;
}
.card-label-mistyrose {
background-color: #ffe4e1;
color: #000;
}
.card-label-indigo {
background-color: #4b0082;
}
.edit-label .card-label,
.create-label .card-label {
float: left;
height: 25px;
margin: 0px 3% 7px 0px;
width: 10.5%;
max-width: 10.5%;
cursor: pointer;
}
.edit-labels input[type="text"] {
margin: 4px 0 6px 38px;
width: 243px;
}
.edit-labels .card-label {
height: 30px;
left: 0;
padding: 1px 5px;
position: absolute;
top: 0;
width: 24px;
}
.edit-labels .labels-static .card-label {
line-height: 30px;
margin-bottom: 4px;
position: relative;
top: auto;
left: 0;
width: 260px;
}
.edit-labels-pop-over {
margin-bottom: 8px;
}
.edit-labels-pop-over .card-label .viewer p {
margin: 0;
}
.edit-labels-pop-over .shortcut {
display: inline-block;
}
.card-label-selectable {
border-radius: 3px;
cursor: pointer;
margin: 0;
margin-bottom: 3px;
width: 190px;
min-height: 18px;
padding: 8px;
position: relative;
transition: margin-right 0.1s;
}
.card-label-selectable .card-label-selectable-icon {
position: absolute;
top: 8px;
right: -20px;
}
.card-label-selectable.active:hover,
.card-label-selectable.active,
.card-label-selectable.active.selected:hover,
.card-label-selectable.active.selected {
padding-right: 32px;
}
.card-label-selectable.active:hover .card-label-selectable-icon,
.card-label-selectable.active .card-label-selectable-icon,
.card-label-selectable.active.selected:hover .card-label-selectable-icon,
.card-label-selectable.active.selected .card-label-selectable-icon {
right: 6px;
}
.card-label-selectable.selected,
.card-label-selectable:hover {
opacity: 0.8;
}
.active .card-label-selectable,
.active .card-label-selectable:hover {
margin-right: 0;
}
.active .card-label-selectable .card-label-selectable-icon {
right: 8px;
}
.card-label-edit-button {
border-radius: 3px;
float: right;
padding: 8px;
}
.card-label-edit-button:hover {
background: #dbdbdb;
}
ul.edit-labels-pop-over span.fa.label-handle {
padding-right: 10px;
}
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
max-width: 180px;
}

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