mirror of
https://github.com/wekan/wekan.git
synced 2025-04-24 05:57:13 -04:00
Compare commits
No commits in common. "main" and "v1.64" have entirely different histories.
1468 changed files with 44020 additions and 597826 deletions
12
.babelrc
12
.babelrc
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-stage-3"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"COVERAGE": {
|
|
||||||
"plugins": [
|
|
||||||
"istanbul"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"]
|
|
|
@ -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
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
packages/*
|
|
||||||
.snap-meteor-1.8/*
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 you’re 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
|
|
|
@ -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
|
|
|
@ -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 you’re 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
|
|
|
@ -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 you’re 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
3
.gitattributes
vendored
|
@ -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
3
.github/FUNDING.yml
vendored
|
@ -1,3 +0,0 @@
|
||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
custom: ['https://wekan.team/commercial-support/']
|
|
61
.github/ISSUE_TEMPLATE.md
vendored
61
.github/ISSUE_TEMPLATE.md
vendored
|
@ -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*
|
||||||
|
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
|
@ -1,6 +0,0 @@
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
14
.github/workflows/depsreview.yaml
vendored
14
.github/workflows/depsreview.yaml
vendored
|
@ -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
|
|
63
.github/workflows/docker-publish.yml
vendored
63
.github/workflows/docker-publish.yml
vendored
|
@ -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 }}
|
|
20
.github/workflows/dockerimage.yml
vendored
20
.github/workflows/dockerimage.yml
vendored
|
@ -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)
|
|
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
|
@ -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 }}"
|
|
163
.github/workflows/test_suite.yml
vendored
163
.github/workflows/test_suite.yml
vendored
|
@ -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
28
.gitignore
vendored
|
@ -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
10
.gitpod.Dockerfile
vendored
|
@ -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/
|
|
|
@ -1,4 +0,0 @@
|
||||||
tasks:
|
|
||||||
- init: npm install
|
|
||||||
image:
|
|
||||||
file: .gitpod.Dockerfile
|
|
|
@ -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
|
|
||||||
|
|
117
.meteor/packages
117
.meteor/packages
|
@ -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
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
METEOR@2.14
|
METEOR@1.6.0.1
|
||||||
|
|
273
.meteor/versions
273
.meteor/versions
|
@ -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
|
|
||||||
|
|
13
.pkgr.yml
13
.pkgr.yml
|
@ -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:
|
|
|
@ -1,8 +0,0 @@
|
||||||
packages/
|
|
||||||
node_modules/
|
|
||||||
.build/
|
|
||||||
.meteor/
|
|
||||||
.vscode/
|
|
||||||
.tx/
|
|
||||||
.github/
|
|
||||||
.snap-meteor-1.8/
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 80,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
60
.tx/config
60
.tx/config
|
@ -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
57
.vscode/launch.json
vendored
|
@ -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"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
11024
CHANGELOG.md
11024
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
415
Dockerfile
415
Dockerfile
|
@ -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"]
|
|
||||||
|
|
|
@ -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"]
|
|
||||||
|
|
|
@ -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"]
|
|
40
FUTURE.md
40
FUTURE.md
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
150
README.md
|
@ -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)
|
[](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]
|
||||||
|
[](http://webchat.freenode.net?channels=%23wekan&uio=d4)
|
||||||
|
|
||||||
## Standards
|
[](https://github.com/wekan/wekan/graphs/contributors)
|
||||||
|
[](https://quay.io/repository/wekan/wekan)
|
||||||
|
[](https://hub.docker.com/r/wekanteam/wekan)
|
||||||
|
[](https://hub.docker.com/r/wekanteam/wekan)
|
||||||
|
[![Wekan Build Status][travis_badge]][travis_status]
|
||||||
|
[](https://www.codacy.com/app/xet7/wekan?utm_source=github.com&utm_medium=referral&utm_content=wekan/wekan&utm_campaign=Badge_Grade)
|
||||||
|
[](https://codeclimate.com/github/wekan/wekan)
|
||||||
|
[](https://david-dm.org/wekan/wekan)
|
||||||
|
[](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 you’re maintaining a personal todo list, planning your holidays with some friends,
|
Whether you’re 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 don’t have to trust us with your data and can
|
Since Wekan is a free software, you don’t 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
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
# 🔐 WeKan — Login System Overview
|
|
||||||
|
|
||||||
This document provides a detailed overview of WeKan’s **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 Meteor’s `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)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
160
SECURITY.md
160
SECURITY.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
752
api.py
|
@ -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 -----------
|
|
|
@ -1,6 +0,0 @@
|
||||||
// PWA
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
navigator.serviceWorker.register('/pwa-service-worker.js');
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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 }}
|
|
||||||
|
|
|
@ -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 [
|
|
||||||
'👍',
|
|
||||||
'👎',
|
|
||||||
'👀',
|
|
||||||
'✅',
|
|
||||||
'❌',
|
|
||||||
'🙏',
|
|
||||||
'👏',
|
|
||||||
'🎉',
|
|
||||||
'🚀',
|
|
||||||
'😊',
|
|
||||||
'🤔',
|
|
||||||
'😔'];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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 }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
48
client/components/activities/activities.styl
Normal file
48
client/components/activities/activities.styl
Normal 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%)
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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}
|
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
48
client/components/activities/comments.styl
Normal file
48
client/components/activities/comments.styl
Normal 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
|
|
@ -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'}}
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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">×</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');
|
||||||
|
|
55
client/components/boards/boardBody.styl
Normal file
55
client/components/boards/boardBody.styl
Normal 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
89
client/components/boards/boardColors.styl
Normal file
89
client/components/boards/boardColors.styl
Normal 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)
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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'}}")
|
||||||
|
|
|
@ -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');
|
|
||||||
|
|
22
client/components/boards/boardHeader.styl
Normal file
22
client/components/boards/boardHeader.styl
Normal 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;
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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'}}
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
169
client/components/boards/boardsList.styl
Normal file
169
client/components/boards/boardsList.styl
Normal 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%
|
|
@ -1,8 +0,0 @@
|
||||||
template(name="miniboard")
|
|
||||||
.minicard(
|
|
||||||
class="minicard-{{colorClass}}")
|
|
||||||
.minicard-title
|
|
||||||
.handle
|
|
||||||
.fa.fa-arrows
|
|
||||||
+viewer
|
|
||||||
= title
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'}}
|
|
||||||
|
|
|
@ -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');
|
|
||||||
|
|
85
client/components/cards/attachments.styl
Normal file
85
client/components/cards/attachments.styl
Normal 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
|
|
@ -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
|
|
|
@ -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');
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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}}
|
|
||||||
|
|
|
@ -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');
|
|
||||||
|
|
59
client/components/cards/cardDate.styl
Normal file
59
client/components/cards/cardDate.styl
Normal 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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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}}
|
|
|
@ -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');
|
|
|
@ -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
134
client/components/cards/cardDetails.styl
Normal file
134
client/components/cards/cardDetails.styl
Normal 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
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
17
client/components/cards/cardTime.styl
Normal file
17
client/components/cards/cardTime.styl
Normal 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
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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'}}
|
|
||||||
|
|
|
@ -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');
|
|
||||||
|
|
139
client/components/cards/checklists.styl
Normal file
139
client/components/cards/checklists.styl
Normal 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
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue