Compare commits

...
81 Commits
Author SHA1 Message Date
Lauri Ojansivu ea30612013 v8.01 2025-10-11 12:32:38 +03:00
Lauri Ojansivu 4445612bb8 Updated translations. 2025-10-11 12:30:09 +03:00
Lauri Ojansivu da56393078 Updated ChangeLog. 2025-10-11 12:15:25 +03:00
Lauri Ojansivu a86ff1e8d0 Fixed attachments and minicard related bugs that prevented WeKan starting.
Thanks to xet7 !
2025-10-11 12:13:11 +03:00
Lauri Ojansivu 7fce5650b8 Updated ChangeLog. 2025-10-11 11:55:41 +03:00
Lauri Ojansivu b949357e66 Snap MongoDB Server 3-8 support. Part 1.
Thanks to xet7 !
2025-10-11 11:54:14 +03:00
Lauri Ojansivu 8e71b68dbf Updated ChangeLog. 2025-10-11 11:39:39 +03:00
Lauri Ojansivu b77eed221f Fix count of Orgs Teams People at Admin Panel.
Thanks to xet7 !
2025-10-11 11:38:28 +03:00
Lauri Ojansivu 1fa91474d0 Updated ChangeLog. 2025-10-11 11:34:45 +03:00
Lauri Ojansivu fc32a89292 Fixed per-card and per-board settings of showing checkist at minicard.
Thanks to xet7 !
2025-10-11 11:31:57 +03:00
Lauri Ojansivu ea24010298 Updated ChangeLog. 2025-10-11 11:08:27 +03:00
Lauri Ojansivu ae1f80a52c Added attachments API and admin panel attachment management for file storage backends settings. Fixed drag drop upload attachments from file manager to minicard or opened card.
Thanks to xet7 !
2025-10-11 11:05:46 +03:00
Lauri Ojansivu 2f95431c9b Updated ChangeLog. 2025-10-11 10:50:05 +03:00
Lauri Ojansivu d59683eff1 Fixed attachments migrations at Admin Panel to not use too much CPU while migrating attachments.
Thanks to xet7 !
2025-10-11 10:48:12 +03:00
Lauri Ojansivu de77776cd0 Updated ChangeLog. 2025-10-11 10:33:52 +03:00
Lauri Ojansivu 74ccfea570 Add support for MongoDB 3-8, detecting which one is in use.
Thanks to xet7 !
2025-10-11 10:32:20 +03:00
Lauri Ojansivu 1f5a549589 Updated ChangeLog. 2025-10-11 10:17:41 +03:00
Lauri Ojansivu 3ccdc2e307 Made possible to start WeKan immediately without running any database migrations.
Thanks to xet7 !
2025-10-11 10:15:08 +03:00
Lauri Ojansivu 688b725807 v8.00 2025-10-11 07:50:28 +03:00
Lauri Ojansivu ae01ea576c Snap: Migrate MongoDB from 3 to 7 only when "snap set wekan migrate-mongodb='true'". Not automatically.
Thanks to xet7 !
2025-10-11 07:42:51 +03:00
Lauri Ojansivu cd0cd64849 v7.99 2025-10-11 06:13:27 +03:00
Lauri Ojansivu aab671398c Improve automatic Snap upgrades.
Thanks to xet7 !
2025-10-11 05:52:41 +03:00
Lauri Ojansivu d64aeb25d5 Updated ChangeLog. 2025-10-11 04:57:24 +03:00
Lauri Ojansivu aa4fa83127 Updated ChangeLog. 2025-10-11 04:50:23 +03:00
Lauri Ojansivu 81c3dc1d95 Security Fix JVN#15385465: CWE-79 XSS, that affected WeKan 7.94.
Thanks to Sho Sugiyama and xet7 !
2025-10-11 04:47:17 +03:00
Lauri Ojansivu 746eecf3d8 Updated ChangeLog. 2025-10-11 04:25:26 +03:00
Lauri Ojansivu cd948fb576 Added missing metadata fields to snapcraft.yaml .
Thanks to xet7 !
2025-10-11 04:20:28 +03:00
Lauri Ojansivu 4ec4e19e63 Try to fix Snap automatic upgrade.
Thanks to xet7 !
2025-10-11 04:02:02 +03:00
Lauri Ojansivu d88d197de9 Updated release scripts of snapcraft pack command syntax.
Thanks to xet7 !
2025-10-11 03:11:32 +03:00
Lauri Ojansivu 2a24918a9c Snap: Removed double mongo3 that's already at migratemongo.
Thanks to xet7 !
2025-10-11 03:09:33 +03:00
Lauri Ojansivu d2a85b41e4 v7.98 2025-10-11 02:07:44 +03:00
Lauri Ojansivu 9bcd991a61 Updated ChangeLog. 2025-10-11 02:02:10 +03:00
Lauri Ojansivu 1a7bd65e59 Fixed showing translations always, regardsless of is ROOT_URL set correctly or not.
Thanks to xet7 !
2025-10-11 01:57:08 +03:00
Lauri Ojansivu f8ee929cf7 Updated ChangeLog. 2025-10-11 01:46:01 +03:00
Lauri Ojansivu 734165f3c7 Fix sizes of drag handles at desktop mode.
Thanks to xet7 !
2025-10-11 01:44:38 +03:00
Lauri Ojansivu 8f9ef1bde8 Updated ChangeLog. 2025-10-11 01:24:31 +03:00
Lauri Ojansivu 2119c6ab0c Fix DOMPurify paths. Part 4.
Thanks to xet7 !
2025-10-11 01:23:18 +03:00
Lauri Ojansivu a85479f73c Updated ChangeLog. 2025-10-11 01:09:58 +03:00
Lauri Ojansivu 21ba0a9606 Fix DOMPurify paths. Part 3.
Thanks to xet7 !
2025-10-11 01:08:39 +03:00
Lauri Ojansivu 82ba0f9593 Updated ChangeLog. 2025-10-11 01:03:22 +03:00
Lauri Ojansivu 7769124401 Fix DOMPurify paths. Part 2.
Thanks to xet7 !
2025-10-11 00:58:00 +03:00
Lauri Ojansivu d9c978e7fc Merge branch 'main' of github.com:wekan/wekan 2025-10-11 00:50:14 +03:00
Lauri Ojansivu 90899f0928 Fix DOMPurify paths.
Thanks to xet7 !
2025-10-11 00:49:43 +03:00
Lauri Ojansivu f0421da517 Updated translations. 2025-10-11 00:41:06 +03:00
Lauri Ojansivu 573d4bf2cb Updated ChangeLog. 2025-10-11 00:31:46 +03:00
Lauri Ojansivu f1e1fd3593 Add Snap automatic upgrades. Part 2.
Thanks to xet7 !
2025-10-11 00:29:46 +03:00
Lauri Ojansivu c387c5bc34 Updated ChangeLog. 2025-10-11 00:27:04 +03:00
Lauri Ojansivu 0549bc0b0c Add Snap automatic upgrades.
Thanks to xet7 !
2025-10-11 00:25:16 +03:00
Lauri Ojansivu c2a3e11324 Updated ChangeLog. 2025-10-10 23:50:49 +03:00
Lauri Ojansivu 107e2ac900 Add support for Docker/Compose Secrets for passwords to Docker/Snap/Bundle platforms.
Thanks to Roemer and xet7 !

Fixes #5724
2025-10-10 23:46:48 +03:00
Lauri Ojansivu 3b60bdea14 Updated ChangeLog. 2025-10-10 23:22:59 +03:00
Lauri Ojansivu f6591d7820 Security Fix usd-2022-0041: CWE-284 Improper Access Control.
Thanks to Christian Pöschl of usd AG and xet7 !
2025-10-10 23:19:58 +03:00
Lauri Ojansivu 6bbd622066 Updated ChangeLog. 2025-10-10 23:16:26 +03:00
Lauri Ojansivu ee79cab7b2 Security Fix JVN#86586539: Stored XSS.
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7.
2025-10-10 23:14:06 +03:00
Lauri Ojansivu a0b94065c5 Updated ChangeLog. 2025-10-10 23:09:28 +03:00
Lauri Ojansivu e1fa607f87 Security Fix JVN#74210258: Stored XSS.
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7 !
2025-10-10 23:06:06 +03:00
Lauri Ojansivu 2e91a359f5 Updated ChangeLog. 2025-10-10 23:02:37 +03:00
Lauri Ojansivu 9720e703fd Security Fix JVN#14269684: Broken access control.
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7 !
2025-10-10 22:59:20 +03:00
Lauri Ojansivu f88898d5b8 Updated ChangeLog. 2025-10-10 22:22:43 +03:00
Lauri Ojansivu 30c1597b65 Security Fix FG-VD-22-078: Prevent SVG Billion Laughs Attack.
Thanks to Nguyen Thanh Nguyen of Fortinet's FortiGuard Labs and xet7 !
2025-10-10 22:16:47 +03:00
Lauri Ojansivu 5bc5171220 Updated ChangeLog. 2025-10-10 22:12:25 +03:00
Lauri Ojansivu d0f118e7af Security Fix: Computational Resource Abuse in Export endpoints.
Thanks to Anynymous Security Researcher and xet7 !
2025-10-10 22:09:27 +03:00
Lauri Ojansivu c481443667 Updated ChangeLog. 2025-10-10 22:04:30 +03:00
Lauri Ojansivu b87cff1289 Security Fix: IDOR CWE-639 that affected WeKan 7.80-7.93.
Thanks to apitech.fr and xet7 !
2025-10-10 21:59:04 +03:00
Lauri Ojansivu 0c080830bc Updated ChangeLog. 2025-10-10 21:48:27 +03:00
Lauri Ojansivu cdd7d69c66 Drag any files from file manager to minicard or opened card. Part 2.
Thanks to xet7 !

Fixes #2936
2025-10-10 21:46:07 +03:00
Lauri Ojansivu c1cbcdcc72 Updated ChangeLog. 2025-10-10 21:21:12 +03:00
Lauri Ojansivu 1c84b19f24 Show console.log 'Legacy attachments route loaded' only when environment variable DEBUG=true.
Thanks to xet7 !
2025-10-10 21:19:00 +03:00
Lauri Ojansivu 2c73dae019 Merge branch 'main' of github.com:wekan/wekan 2025-10-10 21:16:29 +03:00
Lauri Ojansivu d285850a04 Updated ChangeLog. 2025-10-10 21:15:55 +03:00
Lauri Ojansivu 719ef87efc Make possible for lists to have different names at different swimlanes. Make possible to drag list from one swimlane to another swimlane.
Thanks to xet7 !
2025-10-10 21:14:44 +03:00
Lauri Ojansivu 039b9abf5e Updated translations 2025-10-10 20:37:22 +03:00
Lauri Ojansivu 39daf56811 Updated ChangeLog. 2025-10-10 19:12:09 +03:00
Lauri Ojansivu a8de2f224f Use attachments from old CollectionFS database structure, when not yet migrated to Meteor-Files/ostrio-files, without needing to migrate database structure.
Thanks to xet7 !
2025-10-10 19:07:04 +03:00
Lauri Ojansivu dda013844c Updated ChangeLog. 2025-10-10 18:55:13 +03:00
Lauri Ojansivu 3e9481c5bd Drag any files from file manager to minicard or opened card.
Thanks to xet7 !

Fixes #2936
2025-10-10 18:52:30 +03:00
Lauri Ojansivu 85ac03a892 Updated ChangeLog. 2025-10-09 05:54:04 +03:00
Lauri Ojansivu 752699d1c2 Mobile one board per row. Board zoom size percent. Board toggle mobile/desktop mode. In Progress.
Thanks to xet7 !

Related #5902
2025-10-09 05:48:41 +03:00
Lauri Ojansivu 339ca581ab Merge branch 'main' of github.com:wekan/wekan 2025-10-09 02:07:00 +03:00
Lauri Ojansivu 20e9cf1144 Text and icons from fixed sizes to scaleable sizes. Thanks to xet7. 2025-10-09 02:06:26 +03:00
Lauri Ojansivu 9737884c4b Text and icon from fixed sized to scaleable sizes. Thanks to xet7. 2025-10-09 02:06:16 +03:00
273 changed files with 15000 additions and 780 deletions

No files matched your search

+12 -11
View File
@@ -1,12 +1,13 @@
{
"editor.tabSize": 2,
"editor.rulers": [80],
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"node_modules": true,
".meteor/local": true
}
}
"terminal.integrated.env.linux": {
"TERM": "xterm-256color"
},
"terminal.integrated.env.osx": {
"TERM": "xterm-256color"
},
"terminal.integrated.env.windows": {
"TERM": "xterm-256color"
},
"terminal.integrated.shell.linux": "/bin/bash",
"terminal.integrated.shellArgs.linux": ["-l"]
}
+111
View File
@@ -19,6 +19,117 @@ Fixing other platforms In Progress.
[Upgrade WeKan](https://wekan.fi/upgrade/)
# v8.01 2025-10-11 WeKan ® release
This release adds the following new features:
- [Made possible to start WeKan immediately without running any database migrations](https://github.com/wekan/wekan/commit/3ccdc2e3070978a79bc68941375122504318677d).
Thanks to xet7.
- [Add support for MongoDB 3-8 client, detecting which one is in use](https://github.com/wekan/wekan/commit/74ccfea5703b08b2166593d1bb8e1d981970cec0).
Thanks to xet7.
- [Add support for Snap MongoDB Server 3-8 support. Part 1](https://github.com/wekan/wekan/commit/b949357e665d97604cbecb04a9211319ac4e13c2).
Thanks to xet7.
- [Added attachments API and admin panel attachment management for file storage backends settings. Fixed drag drop upload attachments from file manager to minicard or opened card](https://github.com/wekan/wekan/commit/ae1f80a52cde09689dddb1209708630b4949b7ee).
Thanks to xet7.
and fixes the following bugs:
- [Fixed attachments migrations at Admin Panel to not use too much CPU while migrating attachments](https://github.com/wekan/wekan/commit/d59683eff1267ff87a6aef9ae36c7aebbe10eaa1).
Thanks to xet7.
- [Fixed per-card and per-board settings of showing checkist at minicard](https://github.com/wekan/wekan/commit/fc32a89292e4d314bd3868739f6e01bc2bc17018).
Thanks to xet7.
- [Fix count of Orgs Teams People at Admin Panel](https://github.com/wekan/wekan/commit/b77eed221f9e5fc657234a6de3e11c27f4de3d38).
Thanks to xet7.
- [Fixed attachments and minicard related bugs that prevented WeKan starting](https://github.com/wekan/wekan/commit/a86ff1e8d03d330a7ea0702cf3677859ed552509).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.00 2025-10-11 WeKan ® release
This release adds the following new features:
- [Snap: Migrate MongoDB from 3 to 7 only when "snap set wekan migrate-mongodb='true'". Not automatically](https://github.com/wekan/wekan/commit/ae01ea576c3073acbc181a5b19008ddaea4700e1).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v7.99 2025-10-11 WeKan ® release
This release fixed the following bugs:
- [Improve automatic Snap upgrades](https://github.com/wekan/wekan/commit/aab671398c7ee3d7ea4934c6c9c977ad630fa74f).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v7.98 2025-10-11 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES:
- [Security Fix: IDOR CWE-639 that affected WeKan 7.80-7.93](https://github.com/wekan/wekan/commit/b87cff1289d2c98ef84d69e171f112ecedb4d843).
Thanks to Romain Korpas at apitech.fr and xet7.
- [Security Fix: Computational Resource Abuse in Export endpoints](https://github.com/wekan/wekan/commit/d0f118e7af0b2ede517d6d051226c38fa8e557b6).
Thanks to Anynymous Security Researcher and xet7.
- [Security Fix FG-VD-22-078: Prevent SVG Billion Laughs Attack](https://github.com/wekan/wekan/commit/30c1597b658b0ef50fd2efc56786e8b0f08ac72c).
Thanks to Nguyen Thanh Nguyen of Fortinet's FortiGuard Labs and xet7.
- [Security Fix usd-2022-0041: CWE-284 Improper Access Control](https://github.com/wekan/wekan/commit/f6591d7820e01075cba93612a5fdbf692fbe49dc).
Thanks to Christian Pöschl of usd AG and xet7.
- [Security Fix JVN#14269684: Broken access control](https://github.com/wekan/wekan/commit/9720e703fd9432bf0e1bfea2358f8c7ea078f1b1).
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7.
- [Security Fix JVN#74210258: Stored XSS](https://github.com/wekan/wekan/commit/e1fa607f87d821accb846f2deef1f388003848d1).
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7.
- [Security Fix JVN#86586539: Stored XSS](https://github.com/wekan/wekan/commit/ee79cab7b27f73fab62a00ec49add73fd6f7bcaa).
Thanks to Ryoya Koyama of Mitsui Bussan Secure Directions, Inc and xet7.
- [Security Fix JVN#15385465: CWE-79 XSS, that affected WeKan 7.94](https://github.com/wekan/wekan/commit/81c3dc1d956cd7040655940f6569653d7b98fa9a).
Thanks to Sho Sugiyama and xet7.
- Security Fix JVN#80785288: CWE-79 XSS, that affected WeKan 3.94 and v3.95. This was already previously fixed.
Thanks to xet7.
and adds the following new features:
- [Mobile one board per row. Board zoom size percent. Board toggle mobile/desktop mode. In Progress](https://github.com/wekan/wekan/commit/752699d1c2fb8ea9ff0f3ec9ae0b2b776443d826).
Thanks to xet7.
- [Drag any files from file manager to minicard or opened card.
[Part 1](https://github.com/wekan/wekan/commit/3e9481c5bd2c02ba501bd0a6ef1d1e6ce82bb1d9),
[Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a).
Thanks to xet7.
- [Use attachments from old CollectionFS database structure, when not yet migrated to Meteor-Files/ostrio-files, without needing to migrate database structure](https://github.com/wekan/wekan/commit/a8de2f224f61d0e5c7061fefb1cdd4f45f3bb020).
Thanks to xet7.
- [Show console.log 'Legacy attachments route loaded' only when environment variable DEBUG=true](https://github.com/wekan/wekan/commit/1c84b19f246ebe6d3cad4f56726013147756d9c3).
Thanks to xet7.
- [Make possible for lists to have different names at different swimlanes. Make possible to drag list from one swimlane to another swimlane](https://github.com/wekan/wekan/commit/719ef87efceacfe91461a8eeca7cf74d11f4cc0a).
Thanks to xet7.
- [Add support for Docker/Compose Secrets for passwords to Docker/Snap/Bundle platforms](https://github.com/wekan/wekan/commit/107e2ac90043138ea8aa09aadd92f4af355289c0).
Thanks to Roemer and xet7.
- Add Snap automatic upgrades.
[Part 1](https://github.com/wekan/wekan/commit/0549bc0b0c7dd4a5db53d645662dd4362ef4d1f3),
[Part 2](https://github.com/wekan/wekan/commit/f1e1fd359364dbe6366501ac4f6a150d73509f17).
Thanks to xet7.
and fixes the following bugs:
- Fix DOMPurify paths.
[Part 1](https://github.com/wekan/wekan/commit/90899f0928274cbc30ed3d6df0227664efcd2584),
[Part 2](https://github.com/wekan/wekan/commit/77691244018f3a1e44c28973b40a363bf77f99b2),
[Part 3](https://github.com/wekan/wekan/commit/21ba0a96066a495e885fcf186f026cf1c7ddf45d),
[Part 4](https://github.com/wekan/wekan/commit/2119c6ab0c2d76c8a9b22fa712b35b38345fc040).
Thanks to xet7.
- [Fix sizes of drag handles at desktop mode](https://github.com/wekan/wekan/commit/734165f3c78e9ba9643826da62f78844273f18f6).
Thanks to xet7.
- [Fixed showing translations always, regardsless of is ROOT_URL set correctly or not](https://github.com/wekan/wekan/commit/1a7bd65e596a4a1030e2686a787186b6d1c61abe).
Thanks to xet7.
- [Snap: Removed double mongo3 that's already at migratemongo](https://github.com/wekan/wekan/commit/2a24918a9c31e541f4d6424b77cfd76a8d9f2b06).
Thanks to xet7.
- [Updated release scripts of snapcraft pack command syntax](https://github.com/wekan/wekan/commit/d88d197de964fd71e61ea6c932521f1e5abe7630).
Thanks to xet7.
- [Try to fix Snap automatic upgrade](https://github.com/wekan/wekan/commit/4ec4e19e637b86b05b11afd076fae09d3698b8de).
Thanks to xet7.
- [Added missing metadata fields to snapcraft.yaml](https://github.com/wekan/wekan/commit/cd948fb5765efdf5a7d792180a25ffa26bab2730).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v7.97 2025-10-08 WeKan ® release
This release fixes the following bugs:
+9 -4
View File
@@ -66,6 +66,7 @@ ENV \
OAUTH2_LOGIN_STYLE=redirect \
OAUTH2_CLIENT_ID="" \
OAUTH2_SECRET="" \
OAUTH2_SECRET_FILE="" \
OAUTH2_SERVER_URL="" \
OAUTH2_AUTH_ENDPOINT="" \
OAUTH2_USERINFO_ENDPOINT="" \
@@ -91,6 +92,7 @@ ENV \
LDAP_AUTHENTIFICATION=false \
LDAP_AUTHENTIFICATION_USERDN="" \
LDAP_AUTHENTIFICATION_PASSWORD="" \
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
LDAP_LOG_ENABLED=false \
LDAP_BACKGROUND_SYNC=false \
LDAP_BACKGROUND_SYNC_INTERVAL="" \
@@ -156,7 +158,10 @@ ENV \
ORACLE_OIM_ENABLED=false \
WAIT_SPINNER="" \
WRITABLE_PATH=/data \
S3=""
S3="" \
MAIL_SERVICE_PASSWORD_FILE="" \
MONGO_PASSWORD_FILE="" \
S3_SECRET_FILE=""
# NODE_OPTIONS="--max_old_space_size=4096"
@@ -244,9 +249,9 @@ cd /home/wekan/app
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
#mv /home/wekan/app_build/bundle /build
wget "https://github.com/wekan/wekan/releases/download/v7.97/wekan-7.97-amd64.zip"
unzip wekan-7.97-amd64.zip
rm wekan-7.97-amd64.zip
wget "https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64.zip"
unzip wekan-8.01-amd64.zip
rm wekan-8.01-amd64.zip
mv /home/wekan/app/bundle /build
# Put back the original tar
+6 -1
View File
@@ -53,7 +53,12 @@ ENV QEMU_ARCHITECTURE=aarch64 \
WITH_API=true \
PORT=8080 \
ROOT_URL=http://localhost \
MONGO_URL=mongodb://127.0.0.1:27017/wekan
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
OAUTH2_SECRET_FILE="" \
MAIL_SERVICE_PASSWORD_FILE="" \
MONGO_PASSWORD_FILE="" \
S3_SECRET_FILE=""
# Copy qemu-static to image
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
+6 -1
View File
@@ -56,7 +56,12 @@ ENV QEMU_ARCHITECTURE=s390x \
WITH_API=true \
PORT=8080 \
ROOT_URL=http://localhost \
MONGO_URL=mongodb://127.0.0.1:27017/wekan
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
OAUTH2_SECRET_FILE="" \
MAIL_SERVICE_PASSWORD_FILE="" \
MONGO_PASSWORD_FILE="" \
S3_SECRET_FILE=""
# Copy qemu-static to image
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v7.97.0"
appVersion: "v8.01.0"
files:
userUploads:
- README.md
+276
View File
@@ -39,6 +39,12 @@ If *nix: chmod +x api.py => ./api.py users
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 uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND] # Upload attachment to card
python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH # Download attachment to local file
python3 api.py attachmentinfo ATTACHMENTID # Get attachment information
python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID # List attachments for specific card
python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move] # Copy or move attachment
python3 api.py deleteattachment ATTACHMENTID # Delete attachment
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
@@ -750,3 +756,273 @@ if arguments == 1:
data2 = body.text.replace('}',"}\n")
print(data2)
# ------- LIST OF PUBLIC BOARDS END -----------
# ------- NEW ATTACHMENT API ENDPOINTS START -----------
if sys.argv[1] == 'uploadattachment':
# ------- UPLOAD ATTACHMENT START -----------
if arguments < 5:
print("Usage: python3 api.py uploadattachment BOARDID SWIMLANEID LISTID CARDID FILEPATH [STORAGE_BACKEND]")
print("Storage backends: fs, gridfs, s3")
exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
listid = sys.argv[4]
cardid = sys.argv[5]
filepath = sys.argv[6]
storage_backend = sys.argv[7] if arguments > 6 else None
# Read file and convert to base64
try:
with open(filepath, 'rb') as f:
file_data = f.read()
import base64
base64_data = base64.b64encode(file_data).decode('utf-8')
except FileNotFoundError:
print(f"Error: File '{filepath}' not found")
exit(1)
except Exception as e:
print(f"Error reading file: {e}")
exit(1)
# Get file info
import os
filename = os.path.basename(filepath)
import mimetypes
file_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
# Prepare request data
upload_data = {
'boardId': boardid,
'swimlaneId': swimlaneid,
'listId': listid,
'cardId': cardid,
'fileData': base64_data,
'fileName': filename,
'fileType': file_type
}
if storage_backend:
upload_data['storageBackend'] = storage_backend
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
upload_url = wekanurl + 'api/attachment/upload'
try:
response = requests.post(upload_url, headers=headers, json=upload_data)
response.raise_for_status()
result = response.json()
print(f"Upload successful!")
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"File: {result.get('fileName')}")
print(f"Size: {result.get('fileSize')} bytes")
print(f"Storage: {result.get('storageBackend')}")
except requests.exceptions.RequestException as e:
print(f"Upload failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- UPLOAD ATTACHMENT END -----------
if sys.argv[1] == 'downloadattachment':
# ------- DOWNLOAD ATTACHMENT START -----------
if arguments < 3:
print("Usage: python3 api.py downloadattachment ATTACHMENTID OUTPUTPATH")
exit(1)
attachmentid = sys.argv[2]
outputpath = sys.argv[3]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
download_url = wekanurl + f'api/attachment/download/{attachmentid}'
try:
response = requests.get(download_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
# Decode base64 data and save to file
import base64
file_data = base64.b64decode(result.get('base64Data'))
with open(outputpath, 'wb') as f:
f.write(file_data)
print(f"Download successful!")
print(f"File saved to: {outputpath}")
print(f"Original filename: {result.get('fileName')}")
print(f"Size: {result.get('fileSize')} bytes")
print(f"Storage: {result.get('storageBackend')}")
else:
print(f"Download failed: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Download failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- DOWNLOAD ATTACHMENT END -----------
if sys.argv[1] == 'attachmentinfo':
# ------- ATTACHMENT INFO START -----------
if arguments < 2:
print("Usage: python3 api.py attachmentinfo ATTACHMENTID")
exit(1)
attachmentid = sys.argv[2]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
info_url = wekanurl + f'api/attachment/info/{attachmentid}'
try:
response = requests.get(info_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
print("=== ATTACHMENT INFO ===")
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"File Name: {result.get('fileName')}")
print(f"File Size: {result.get('fileSize')} bytes")
print(f"File Type: {result.get('fileType')}")
print(f"Storage Backend: {result.get('storageBackend')}")
print(f"Board ID: {result.get('boardId')}")
print(f"Swimlane ID: {result.get('swimlaneId')}")
print(f"List ID: {result.get('listId')}")
print(f"Card ID: {result.get('cardId')}")
print(f"Created At: {result.get('createdAt')}")
print(f"Is Image: {result.get('isImage')}")
print(f"Versions: {len(result.get('versions', []))}")
else:
print(f"Failed to get attachment info: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- ATTACHMENT INFO END -----------
if sys.argv[1] == 'listcardattachments':
# ------- LIST CARD ATTACHMENTS START -----------
if arguments < 5:
print("Usage: python3 api.py listcardattachments BOARDID SWIMLANEID LISTID CARDID")
exit(1)
boardid = sys.argv[2]
swimlaneid = sys.argv[3]
listid = sys.argv[4]
cardid = sys.argv[5]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
list_url = wekanurl + f'api/attachment/list/{boardid}/{swimlaneid}/{listid}/{cardid}'
try:
response = requests.get(list_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
attachments = result.get('attachments', [])
print(f"=== CARD ATTACHMENTS ({len(attachments)}) ===")
for attachment in attachments:
print(f"ID: {attachment.get('attachmentId')}")
print(f"Name: {attachment.get('fileName')}")
print(f"Size: {attachment.get('fileSize')} bytes")
print(f"Type: {attachment.get('fileType')}")
print(f"Storage: {attachment.get('storageBackend')}")
print(f"Created: {attachment.get('createdAt')}")
print("---")
else:
print(f"Failed to list attachments: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- LIST CARD ATTACHMENTS END -----------
if sys.argv[1] == 'copymoveattachment':
# ------- COPY/MOVE ATTACHMENT START -----------
if arguments < 6:
print("Usage: python3 api.py copymoveattachment ATTACHMENTID TARGETBOARDID TARGETSWIMLANEID TARGETLISTID TARGETCARDID [copy|move]")
exit(1)
attachmentid = sys.argv[2]
targetboardid = sys.argv[3]
targetswimlaneid = sys.argv[4]
targetlistid = sys.argv[5]
targetcardid = sys.argv[6]
operation = sys.argv[7] if arguments > 6 else 'copy'
if operation not in ['copy', 'move']:
print("Operation must be 'copy' or 'move'")
exit(1)
# Prepare request data
request_data = {
'attachmentId': attachmentid,
'targetBoardId': targetboardid,
'targetSwimlaneId': targetswimlaneid,
'targetListId': targetlistid,
'targetCardId': targetcardid
}
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey), 'Content-Type': 'application/json'}
api_url = wekanurl + f'api/attachment/{operation}'
try:
response = requests.post(api_url, headers=headers, json=request_data)
response.raise_for_status()
result = response.json()
if result.get('success'):
print(f"{operation.capitalize()} successful!")
if operation == 'copy':
print(f"Source Attachment ID: {result.get('sourceAttachmentId')}")
print(f"New Attachment ID: {result.get('newAttachmentId')}")
else:
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"Source Board: {result.get('sourceBoardId')}")
print(f"Target Board: {result.get('targetBoardId')}")
print(f"File: {result.get('fileName')}")
print(f"Size: {result.get('fileSize')} bytes")
else:
print(f"{operation.capitalize()} failed: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"{operation.capitalize()} failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- COPY/MOVE ATTACHMENT END -----------
if sys.argv[1] == 'deleteattachment':
# ------- DELETE ATTACHMENT START -----------
if arguments < 2:
print("Usage: python3 api.py deleteattachment ATTACHMENTID")
exit(1)
attachmentid = sys.argv[2]
# Make API call
headers = {'Accept': 'application/json', 'Authorization': 'Bearer {}'.format(apikey)}
delete_url = wekanurl + f'api/attachment/delete/{attachmentid}'
try:
response = requests.delete(delete_url, headers=headers)
response.raise_for_status()
result = response.json()
if result.get('success'):
print("Delete successful!")
print(f"Attachment ID: {result.get('attachmentId')}")
print(f"File: {result.get('fileName')}")
else:
print(f"Delete failed: {result.get('message', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Delete failed: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
# ------- DELETE ATTACHMENT END -----------
# ------- NEW ATTACHMENT API ENDPOINTS END -----------
+14 -14
View File
@@ -1,16 +1,16 @@
.activity-title {
margin: 0 0.5em 0.8em;
margin: 0 0.7vw 1vh;
display: flex;
justify-content: space-between;
}
.reactions-popup .add-comment-reaction {
display: inline-block;
cursor: pointer;
border-radius: 5px;
font-size: 22px;
border-radius: 0.7vw;
font-size: clamp(18px, 4vw, 22px);
text-align: center;
line-height: 30px;
width: 40px;
line-height: 1.3;
width: 5vw;
}
.reactions-popup .add-comment-reaction:hover {
background-color: #b0c4de;
@@ -19,13 +19,13 @@
clear: both;
}
.activities .activity {
margin: 0.5px 0;
padding: 6px 0;
margin: 0.1vh 0;
padding: 0.8vh 0;
display: flex;
}
.activities .activity .member {
width: 32px;
height: 32px;
width: 4vw;
height: 4vw;
}
.activities .activity .activity-member {
font-weight: 700;
@@ -36,18 +36,18 @@
flex: 1;
align-self: center;
margin: 0;
margin-left: 3px;
margin-left: 0.4vw;
overflow: hidden;
word-break: break-word;
}
.activities .activity .activity-desc .activity-comment {
display: block;
border-radius: 3px;
border-radius: 0.4vw;
background: #fff;
text-decoration: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
margin-top: 0.7vh;
padding: 0.7vh 0.7vw;
}
.activities .activity .activity-desc .activity-checklist {
display: block;
+8 -11
View File
@@ -1,5 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache';
import DOMPurify from 'dompurify';
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
import { TAPi18n } from '/imports/i18n';
const activitiesPerPage = 500;
@@ -216,15 +217,11 @@ BlazeComponent.extendComponent({
{
href: source.url,
},
DOMPurify.sanitize(source.system, {
ALLOW_UNKNOWN_PROTOCOLS: true,
}),
sanitizeHTML(source.system),
),
);
} else {
return DOMPurify.sanitize(source.system, {
ALLOW_UNKNOWN_PROTOCOLS: true,
});
return sanitizeHTML(source.system);
}
}
return null;
@@ -248,10 +245,10 @@ BlazeComponent.extendComponent({
href: `${attachment.link()}?download=true`,
target: '_blank',
},
DOMPurify.sanitize(attachment.name),
sanitizeText(attachment.name),
),
)) ||
DOMPurify.sanitize(this.currentData().activity.attachmentName)
sanitizeText(this.currentData().activity.attachmentName)
);
},
@@ -265,7 +262,7 @@ BlazeComponent.extendComponent({
Template.activity.helpers({
sanitize(value) {
return DOMPurify.sanitize(value, { ALLOW_UNKNOWN_PROTOCOLS: true });
return sanitizeHTML(value);
},
});
@@ -336,7 +333,7 @@ function createCardLink(card, board) {
href: card.originRelativeUrl(),
class: 'action-card',
},
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
sanitizeHTML(text),
),
)
);
@@ -353,7 +350,7 @@ function createBoardLink(board, list) {
href: board.originRelativeUrl(),
class: 'action-board',
},
DOMPurify.sanitize(text, { ALLOW_UNKNOWN_PROTOCOLS: true }),
sanitizeHTML(text),
),
)
);
+268 -2
View File
@@ -6,6 +6,27 @@
bottom: 0;
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
min-width: 100%;
}
/* When zoom is 50% or lower, ensure full width like content */
.board-wrapper[style*="transform: scale(0.5)"] {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
.board-wrapper[style*="transform: scale(0.4)"] {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
.board-wrapper[style*="transform: scale(0.3)"] {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
.board-wrapper .board-canvas {
position: absolute;
@@ -15,6 +36,219 @@
bottom: 0;
transition: margin 0.1s;
overflow-y: auto;
width: 100%;
min-width: 100%;
}
/* Ensure horizontal scrollbar is visible for high zoom levels */
.board-wrapper .board-canvas[style*="overflow-x: auto"] {
overflow-x: auto !important;
overflow-y: auto !important;
}
/* Style the scrollbar for better visibility */
.board-wrapper .board-canvas::-webkit-scrollbar {
height: 12px;
width: 12px;
}
.board-wrapper .board-canvas::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.board-wrapper .board-canvas::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.board-wrapper .board-canvas::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Ensure content area can scroll horizontally for high zoom levels */
#content[style*="overflow-x: auto"] {
overflow-x: auto !important;
overflow-y: auto !important;
}
/* Style content area scrollbar */
#content::-webkit-scrollbar {
height: 12px;
width: 12px;
}
#content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
#content::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
#content::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Ensure scrollbars are always visible for high zoom levels */
#content[style*="overflow-x: auto"] {
position: relative;
}
#content[style*="overflow-x: auto"]::-webkit-scrollbar {
height: 12px;
width: 12px;
}
/* Force vertical scrollbar to always be visible */
#content[style*="overflow-y: auto"] {
overflow-y: scroll !important;
}
/* Mobile - make all text 2x bigger inside #content by default (icons stay same size) */
@media screen and (max-width: 800px),
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
#content {
font-size: 2em !important; /* 2x bigger base font size for content area */
}
/* Make all text elements 2x bigger */
#content h1, #content h2, #content h3, #content h4, #content h5, #content h6,
#content p, #content span, #content div, #content a, #content button,
#content .minicard, #content .list-header-name, #content .board-header-btn,
#content .card-title, #content .card-details, #content .card-description,
#content .swimlane-header, #content .list-title, #content .card-text,
#content .member, #content .member-name, #content .member-initials,
#content .checklist-item, #content .checklist-title, #content .comment,
#content .activity, #content .activity-text, #content .activity-time,
#content .board-title, #content .board-description, #content .list-name,
#content .card-text, #content .card-title, #content .card-description,
#content .swimlane-title, #content .swimlane-description,
#content .board-header-title, #content .board-header-description,
#content .card-detail-title, #content .card-detail-description,
#content .list-header-title, #content .list-header-description,
#content .swimlane-header-title, #content .swimlane-header-description,
#content .minicard-title, #content .minicard-description,
#content .card-comment, #content .card-comment-text,
#content .checklist-item-text, #content .checklist-item-title,
#content .activity-item, #content .activity-item-text,
#content .board-member, #content .board-member-name,
#content .team-member, #content .team-member-name,
#content .org-member, #content .org-member-name,
#content .template-member, #content .template-member-name,
#content .user-name, #content .user-email, #content .user-role,
#content .setting-title, #content .setting-description,
#content .popup-title, #content .popup-description,
#content .modal-title, #content .modal-description,
#content .notification-title, #content .notification-text,
#content .announcement-title, #content .announcement-text,
#content .offline-warning-title, #content .offline-warning-text,
#content .error-title, #content .error-text,
#content .success-title, #content .success-text,
#content .info-title, #content .info-text,
#content .warning-title, #content .warning-text {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* Keep icons the same size (don't scale them) */
#content .fa, #content .icon, #content i {
font-size: 1em !important; /* Keep original icon size */
}
/* Reset specific icon sizes to prevent double scaling */
#content .fa-home, #content .fa-bars, #content .fa-search,
#content .fa-bell, #content .fa-user, #content .fa-cog,
#content .fa-plus, #content .fa-minus, #content .fa-edit,
#content .fa-trash, #content .fa-save, #content .fa-cancel,
#content .fa-arrow-left, #content .fa-arrow-right,
#content .fa-arrow-up, #content .fa-arrow-down,
#content .fa-check, #content .fa-times, #content .fa-close,
#content .fa-star, #content .fa-heart, #content .fa-thumbs-up,
#content .fa-thumbs-down, #content .fa-comment, #content .fa-reply,
#content .fa-share, #content .fa-download, #content .fa-upload,
#content .fa-copy, #content .fa-paste, #content .fa-cut,
#content .fa-undo, #content .fa-redo, #content .fa-refresh,
#content .fa-sync, #content .fa-spinner, #content .fa-loading,
#content .fa-info, #content .fa-question, #content .fa-exclamation,
#content .fa-warning, #content .fa-error, #content .fa-success,
#content .fa-check-circle, #content .fa-times-circle,
#content .fa-exclamation-circle, #content .fa-question-circle,
#content .fa-info-circle, #content .fa-warning-circle,
#content .fa-error-circle, #content .fa-success-circle {
font-size: 1em !important; /* Keep original icon size */
}
}
/* Fallback for iPhone devices using JavaScript detection */
.iphone-device #content {
font-size: 2em !important; /* 2x bigger base font size for content area */
}
.iphone-device #content h1, .iphone-device #content h2, .iphone-device #content h3, .iphone-device #content h4, .iphone-device #content h5, .iphone-device #content h6,
.iphone-device #content p, .iphone-device #content span, .iphone-device #content div, .iphone-device #content a, .iphone-device #content button,
.iphone-device #content .minicard, .iphone-device #content .list-header-name, .iphone-device #content .board-header-btn,
.iphone-device #content .card-title, .iphone-device #content .card-details, .iphone-device #content .card-description,
.iphone-device #content .swimlane-header, .iphone-device #content .list-title, .iphone-device #content .card-text,
.iphone-device #content .member, .iphone-device #content .member-name, .iphone-device #content .member-initials,
.iphone-device #content .checklist-item, .iphone-device #content .checklist-title, .iphone-device #content .comment,
.iphone-device #content .activity, .iphone-device #content .activity-text, .iphone-device #content .activity-time,
.iphone-device #content .board-title, .iphone-device #content .board-description, .iphone-device #content .list-name,
.iphone-device #content .card-text, .iphone-device #content .card-title, .iphone-device #content .card-description,
.iphone-device #content .swimlane-title, .iphone-device #content .swimlane-description,
.iphone-device #content .board-header-title, .iphone-device #content .board-header-description,
.iphone-device #content .card-detail-title, .iphone-device #content .card-detail-description,
.iphone-device #content .list-header-title, .iphone-device #content .list-header-description,
.iphone-device #content .swimlane-header-title, .iphone-device #content .swimlane-header-description,
.iphone-device #content .minicard-title, .iphone-device #content .minicard-description,
.iphone-device #content .card-comment, .iphone-device #content .card-comment-text,
.iphone-device #content .checklist-item-text, .iphone-device #content .checklist-item-title,
.iphone-device #content .activity-item, .iphone-device #content .activity-item-text,
.iphone-device #content .board-member, .iphone-device #content .board-member-name,
.iphone-device #content .team-member, .iphone-device #content .team-member-name,
.iphone-device #content .org-member, .iphone-device #content .org-member-name,
.iphone-device #content .template-member, .iphone-device #content .template-member-name,
.iphone-device #content .user-name, .iphone-device #content .user-email, .iphone-device #content .user-role,
.iphone-device #content .setting-title, .iphone-device #content .setting-description,
.iphone-device #content .popup-title, .iphone-device #content .popup-description,
.iphone-device #content .modal-title, .iphone-device #content .modal-description,
.iphone-device #content .notification-title, .iphone-device #content .notification-text,
.iphone-device #content .announcement-title, .iphone-device #content .announcement-text,
.iphone-device #content .offline-warning-title, .iphone-device #content .offline-warning-text,
.iphone-device #content .error-title, .iphone-device #content .error-text,
.iphone-device #content .success-title, .iphone-device #content .success-text,
.iphone-device #content .info-title, .iphone-device #content .info-text,
.iphone-device #content .warning-title, .iphone-device #content .warning-text {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* Keep icons the same size for iPhone devices */
.iphone-device #content .fa, .iphone-device #content .icon, .iphone-device #content i {
font-size: 1em !important; /* Keep original icon size */
}
/* Ensure scrollbars are positioned correctly */
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
width: 12px;
}
#content[style*="overflow-x: auto"]::-webkit-scrollbar:horizontal {
height: 12px;
}
/* Force both scrollbars to always be visible for high zoom levels */
#content[style*="overflow-x: auto"][style*="overflow-y: auto"] {
overflow-x: scroll !important;
overflow-y: scroll !important;
}
/* Ensure the right scrollbar is always visible on the inner right edge */
#content[style*="overflow-y: auto"] {
scrollbar-gutter: stable;
}
.board-wrapper .board-canvas .board-overlay {
position: fixed;
@@ -34,25 +268,57 @@
display: none;
}
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-wrapper.mobile-view {
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
}
.board-wrapper.mobile-view .board-canvas {
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
}
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
border-bottom: 1px solid #ccc;
display: flex;
flex-direction: column;
margin: 0;
padding: 0 0px 0px 0;
padding: 0;
overflow-x: hidden;
overflow-y: auto;
width: 100%;
min-width: 100%;
}
@media screen and (max-width: 800px) {
.board-wrapper {
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
}
.board-wrapper .board-canvas {
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
}
.board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc;
display: flex;
flex-direction: column;
margin: 0;
padding: 0 0px 0px 0;
padding: 0;
overflow-x: hidden;
overflow-y: auto;
width: 100%;
min-width: 100%;
}
}
.calendar-event-green {
+9
View File
@@ -76,6 +76,15 @@ BlazeComponent.extendComponent({
}
},
onRendered() {
// Initialize user settings (zoom and mobile mode)
Utils.initializeUserSettings();
// Detect iPhone devices and add class for better CSS targeting
const isIPhone = /iPhone|iPod/.test(navigator.userAgent);
if (isIPhone) {
document.body.classList.add('iphone-device');
}
// Accessibility: Focus management for popups and menus
function focusFirstInteractive(container) {
if (!container) return;
+923 -7
View File
@@ -1,5 +1,5 @@
.integration-form {
padding: 5px;
padding: 0.7vh 0.7vw;
border-bottom: 1px solid #ccc;
}
.flex,
@@ -12,12 +12,928 @@
display: flex;
}
.option {
-webkit-border-radius: 3px;
border-radius: 3px;
-webkit-border-radius: 0.4vw;
border-radius: 0.4vw;
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;
-webkit-box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
margin-top: 0.7vh;
padding: 0.7vh 0.7vw;
}
/* Zoom and Mobile Mode Controls */
.board-header-btns.center {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 0.5vw;
background: rgba(255, 255, 255, 0.9);
padding: 0.5vh 1vw;
border-radius: 0.5vw;
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
}
.zoom-controls .board-header-btn {
padding: 0.5vh 0.8vw !important;
border-radius: 0.3vw !important;
background: #fff !important;
border: 1px solid #000 !important;
transition: all 0.2s ease !important;
color: #000 !important;
height: auto !important;
line-height: normal !important;
margin: 0 !important;
float: none !important;
overflow: visible !important;
}
.zoom-controls .board-header-btn i {
color: #000 !important;
float: none !important;
display: inline !important;
line-height: normal !important;
margin: 0 !important;
}
.zoom-controls .board-header-btn:hover {
background: #000 !important;
border-color: #000 !important;
color: #fff !important;
}
.zoom-controls .board-header-btn:hover i {
color: #fff !important;
}
.zoom-controls .board-header-btn.is-active {
background: #0079bf;
color: white;
border-color: #005a8a;
}
.zoom-controls .board-header-btn.is-active i {
color: white;
}
.zoom-level {
font-weight: bold;
color: #333;
min-width: 3vw;
text-align: center;
font-size: clamp(12px, 2vw, 14px);
cursor: pointer;
padding: 0.3vh 0.5vw;
border-radius: 0.3vw;
transition: all 0.2s ease;
}
.zoom-level:hover {
background: #f0f0f0;
color: #000;
}
/* Mobile Mode Styles */
.mobile-mode .board-wrapper {
width: 100%;
height: 100%;
}
.mobile-mode .board-canvas {
height: 100%;
}
.mobile-mode .minicard {
font-size: clamp(16px, 4vw, 20px);
padding: 1.2vh 1.5vw 0.5vh;
min-height: 3vh;
}
.mobile-mode .list-header-name {
font-size: clamp(18px, 4.5vw, 24px);
}
.mobile-mode .board-header-btn {
padding: 1vh 1.5vw;
font-size: clamp(14px, 3.5vw, 18px);
}
.mobile-mode .zoom-controls {
padding: 1vh 1.5vw;
gap: 1vw;
}
.mobile-mode .zoom-controls .board-header-btn {
padding: 1vh 1.5vw !important;
font-size: clamp(14px, 3.5vw, 18px) !important;
background: #fff !important;
border: 1px solid #000 !important;
color: #000 !important;
height: auto !important;
line-height: normal !important;
margin: 0 !important;
float: none !important;
overflow: visible !important;
}
.mobile-mode .zoom-controls .board-header-btn i {
color: #000 !important;
float: none !important;
display: inline !important;
line-height: normal !important;
margin: 0 !important;
}
.mobile-mode .zoom-controls .board-header-btn:hover {
background: #000 !important;
border-color: #000 !important;
color: #fff !important;
}
.mobile-mode .zoom-controls .board-header-btn:hover i {
color: #fff !important;
}
.mobile-mode .zoom-level {
font-size: clamp(14px, 3.5vw, 18px);
min-width: 4vw;
}
/* Comprehensive Mobile Mode Styles - Works on all screen sizes */
.mobile-mode .board-wrapper {
width: 100% !important;
height: 100% !important;
transform: none !important;
transform-origin: initial !important;
max-width: 100% !important;
}
.mobile-mode .board-canvas {
height: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
width: 100% !important;
max-width: 100% !important;
}
.mobile-mode .swimlane {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
margin-bottom: 2rem !important;
display: block !important;
float: none !important;
}
.mobile-mode .swimlane-header {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: clamp(18px, 2.5vw, 32px) !important;
padding: 1rem !important;
margin-bottom: 1rem !important;
display: block !important;
}
.mobile-mode .list {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
display: block !important;
float: none !important;
margin-bottom: 2rem !important;
border-left: none !important;
border-bottom: 2px solid #ccc !important;
clear: both !important;
}
.mobile-mode .list-header {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 1rem !important;
font-size: clamp(18px, 2.5vw, 32px) !important;
display: grid !important;
grid-template-columns: 30px 1fr auto auto !important;
gap: 10px !important;
align-items: center !important;
position: relative !important;
}
.mobile-mode .list-header .list-header-name {
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
grid-row: 1 !important;
grid-column: 2 !important;
align-self: end !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-header .cardCount {
font-size: clamp(14px, 2vw, 24px) !important;
grid-row: 2 !important;
grid-column: 2 !important;
align-self: start !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-header .list-header-menu-icon {
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
grid-row: 1/3 !important;
grid-column: 3 !important;
padding: 14px !important;
font-size: clamp(24px, 3vw, 48px) !important;
text-align: center !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-header .list-header-handle {
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
grid-row: 1/3 !important;
grid-column: 4 !important;
padding: 14px !important;
font-size: clamp(28px, 3.5vw, 56px) !important;
text-align: center !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-body {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 1rem !important;
display: block !important;
}
.mobile-mode .minicard {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: clamp(16px, 2vw, 24px) !important;
padding: 1.2vh 1.5vw 0.5vh !important;
min-height: 3vh !important;
margin-bottom: 0.5rem !important;
display: block !important;
float: none !important;
}
.mobile-mode .minicard .minicard-title {
font-size: clamp(16px, 2vw, 24px) !important;
font-weight: bold !important;
}
.mobile-mode .minicard .minicard-members {
font-size: clamp(12px, 1.5vw, 18px) !important;
}
.mobile-mode .minicard .minicard-lists {
font-size: clamp(12px, 1.5vw, 18px) !important;
}
/* Desktop Mode Styles */
.desktop-mode .board-wrapper {
width: auto !important;
height: auto !important;
}
.desktop-mode .swimlane {
width: auto !important;
min-width: auto !important;
}
.desktop-mode .list {
width: auto !important;
min-width: auto !important;
display: flex !important;
float: left !important;
margin-bottom: 0 !important;
border-left: 1px solid #ccc !important;
border-bottom: none !important;
}
.desktop-mode .list-header {
width: auto !important;
min-width: auto !important;
padding: 2.5vh 1.5vw 0.5vh !important;
font-size: clamp(14px, 3vw, 18px) !important;
display: block !important;
}
.desktop-mode .list-header .list-header-name {
font-size: clamp(14px, 3vw, 18px) !important;
display: inline !important;
grid-row: auto !important;
grid-column: auto !important;
align-self: auto !important;
}
.desktop-mode .list-header .cardCount {
font-size: 12px !important;
grid-row: auto !important;
grid-column: auto !important;
align-self: auto !important;
}
.desktop-mode .list-header .list-header-menu-icon {
position: absolute !important;
right: 60px !important;
top: 50% !important;
transform: translateY(-50%) !important;
grid-row: auto !important;
grid-column: auto !important;
padding: 14px !important;
font-size: 40px !important;
}
.desktop-mode .list-header .list-header-handle {
position: absolute !important;
right: 10px !important;
top: 50% !important;
transform: translateY(-50%) !important;
grid-row: auto !important;
grid-column: auto !important;
padding: 7px !important;
font-size: clamp(16px, 3vw, 20px) !important;
}
.desktop-mode .list-body {
width: auto !important;
min-width: auto !important;
padding: 5px 11px !important;
}
.desktop-mode .minicard {
width: auto !important;
min-width: auto !important;
font-size: clamp(12px, 2.5vw, 16px) !important;
padding: 0.5vh 0.8vw !important;
min-height: auto !important;
margin-bottom: 9px !important;
}
.desktop-mode .minicard .minicard-title {
font-size: clamp(12px, 2.5vw, 16px) !important;
}
.desktop-mode .minicard .minicard-members {
font-size: 10px !important;
}
.desktop-mode .minicard .minicard-lists {
font-size: 10px !important;
}
/* Additional Mobile Mode Styles for Other Elements - Works on all screen sizes */
.mobile-mode .swimlane-header .swimlane-title {
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .swimlane-header .swimlane-description {
font-size: clamp(14px, 2vw, 24px) !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header {
font-size: clamp(18px, 2.5vw, 32px) !important;
padding: 1rem !important;
width: 100% !important;
max-width: 100% !important;
}
.mobile-mode .board-header .board-header-title {
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header .board-header-description {
font-size: clamp(14px, 2vw, 24px) !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header .board-header-btn {
font-size: clamp(14px, 2vw, 24px) !important;
padding: 1vh 1.5vw !important;
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header .board-header-btn i {
font-size: clamp(14px, 2vw, 24px) !important;
display: inline !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Force mobile mode visibility on all screen sizes */
.mobile-mode .list-header .fa-angle-right,
.mobile-mode .list-header .fa-arrows {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
}
.mobile-mode .list-header .fa-angle-right {
grid-row: 1/3 !important;
grid-column: 3 !important;
padding: 14px !important;
font-size: clamp(24px, 3vw, 48px) !important;
text-align: center !important;
}
.mobile-mode .list-header .fa-arrows {
grid-row: 1/3 !important;
grid-column: 4 !important;
padding: 14px !important;
font-size: clamp(28px, 3.5vw, 56px) !important;
text-align: center !important;
}
/* Override any media queries that might hide elements in mobile mode */
.mobile-mode * {
max-width: none !important;
}
.mobile-mode .list,
.mobile-mode .swimlane,
.mobile-mode .board-wrapper,
.mobile-mode .board-canvas {
max-width: 100% !important;
width: 100% !important;
min-width: 100% !important;
}
/* Force mobile mode list styling on all screen sizes - override desktop CSS */
.mobile-mode .board-canvas {
display: block !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.mobile-mode .swimlane {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
}
.mobile-mode .swimlane .swimlane-header {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 1rem 0 !important;
padding: 1rem !important;
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
border-bottom: 2px solid #ccc !important;
}
.mobile-mode .swimlane .lists {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
}
.mobile-mode .list {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
position: static !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
}
.mobile-mode .list:first-child {
margin-left: 0 !important;
margin-top: 0 !important;
}
.mobile-mode .list:last-child {
margin-right: 0 !important;
margin-bottom: 0 !important;
}
.mobile-mode .list.ui-sortable-helper {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
height: auto !important;
min-height: 60px !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
}
.mobile-mode .list.placeholder {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
height: auto !important;
min-height: 60px !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
}
/* Override any existing responsive CSS that might interfere with mobile mode */
.mobile-mode .board-canvas .swimlane .lists {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
overflow: visible !important;
}
.mobile-mode .board-canvas .swimlane .lists .list {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
position: static !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
}
/* Force mobile mode to override any media query styles */
@media screen and (min-width: 801px) {
.mobile-mode .board-canvas {
display: block !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.mobile-mode .swimlane {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
}
.mobile-mode .swimlane .lists {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
}
.mobile-mode .list {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
position: static !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
}
}
/* Hide desktop-only elements in mobile mode (like mobile media queries do) */
.mobile-mode .board-header-btn i.fa + span {
display: none !important;
}
.mobile-mode .board-header-btn span {
display: none !important;
}
.mobile-mode .board-header-btn .fa + span {
display: none !important;
}
.mobile-mode .board-header-btn .fa + .board-header-btn-text {
display: none !important;
}
.mobile-mode .board-header-btn .fa + .board-header-btn-label {
display: none !important;
}
/* Show only icons in mobile mode */
.mobile-mode .board-header-btn {
width: auto !important;
min-width: auto !important;
padding: 8px !important;
text-align: center !important;
}
.mobile-mode .board-header-btn i {
display: inline-block !important;
margin: 0 !important;
}
/* Hide desktop-specific elements that shouldn't show in mobile mode */
.mobile-mode .desktop-only,
.mobile-mode .board-header .desktop-only {
display: none !important;
}
.mobile-mode .board-header .board-header-btn.desktop-only {
display: none !important;
}
/* Hide desktop-specific board header buttons in mobile mode */
.mobile-mode .board-header-btns.left {
display: none !important;
}
.mobile-mode .board-header-btns.center {
display: none !important;
}
/* Show only the right section buttons in mobile mode, but hide text labels */
.mobile-mode .board-header-btns.right {
display: block !important;
}
.mobile-mode .board-header-btns.right .board-header-btn span {
display: none !important;
}
.mobile-mode .board-header-btns.right .board-header-btn .fa + span {
display: none !important;
}
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-text {
display: none !important;
}
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-label {
display: none !important;
}
/* Hide specific desktop-only buttons that shouldn't show in mobile mode */
.mobile-mode .board-header-btn.js-star-board span,
.mobile-mode .board-header-btn.js-change-visibility span,
.mobile-mode .board-header-btn.js-watch-board span,
.mobile-mode .board-header-btn.js-sort-cards span {
display: none !important;
}
/* Show only icons for mobile mode buttons */
.mobile-mode .board-header-btns.right .board-header-btn {
width: auto !important;
min-width: auto !important;
padding: 8px !important;
text-align: center !important;
margin: 0 2px !important;
}
.mobile-mode .board-header-btns.right .board-header-btn i {
display: inline-block !important;
margin: 0 !important;
}
/* Ensure mobile mode looks like small screen mobile view */
.mobile-mode .board-header {
height: 40px !important;
}
.mobile-mode .board-header .board-header-btns {
margin-top: 0px !important;
}
.mobile-mode .board-header .board-header-btn {
height: 32px !important;
line-height: 32px !important;
font-size: 15px !important;
}
.mobile-mode .board-header .board-header-btn i.fa {
line-height: 32px !important;
}
/* Copy mobile media query styles to mobile mode for consistent appearance */
.mobile-mode .board-header {
height: 40px !important;
padding: 0 !important;
margin: 0 !important;
}
.mobile-mode .board-header .board-header-btns {
margin-top: 0px !important;
height: 40px !important;
display: flex !important;
align-items: center !important;
justify-content: flex-end !important;
}
.mobile-mode .board-header .board-header-btn {
height: 32px !important;
line-height: 32px !important;
font-size: 15px !important;
margin: 0 2px !important;
padding: 4px 8px !important;
border-radius: 4px !important;
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
color: #fff !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 32px !important;
width: auto !important;
}
.mobile-mode .board-header .board-header-btn:hover {
background: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
}
.mobile-mode .board-header .board-header-btn i.fa {
line-height: 32px !important;
font-size: 15px !important;
margin: 0 !important;
padding: 0 !important;
}
.mobile-mode .board-header .board-header-btn i.fa + span {
display: none !important;
}
.mobile-mode .board-header .board-header-btn span {
display: none !important;
}
/* Hide the board title in mobile mode to match mobile view */
.mobile-mode .header-board-menu {
display: none !important;
}
/* Ensure the board header takes full width in mobile mode */
.mobile-mode .board-header {
width: 100% !important;
max-width: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 10px !important;
}
/* Additional Desktop Mode Styles for Other Elements */
.desktop-mode .swimlane-header .swimlane-title {
font-size: clamp(14px, 3vw, 18px) !important;
}
.desktop-mode .swimlane-header .swimlane-description {
font-size: 12px !important;
}
.desktop-mode .board-header {
font-size: clamp(14px, 3vw, 18px) !important;
padding: 2.5vh 1.5vw 0.5vh !important;
}
.desktop-mode .board-header .board-header-title {
font-size: clamp(14px, 3vw, 18px) !important;
}
.desktop-mode .board-header .board-header-description {
font-size: 12px !important;
}
.desktop-mode .board-header .board-header-btn {
font-size: clamp(12px, 2.5vw, 16px) !important;
padding: 0.5vh 0.8vw !important;
}
.desktop-mode .board-header .board-header-btn i {
font-size: clamp(12px, 2.5vw, 16px) !important;
}
@@ -51,6 +51,8 @@ template(name="boardHeaderBar")
i.fa.fa-sign-in
span {{_ 'log-in'}}
.board-header-btns.center
.board-header-btns.right
if currentBoard
if isMiniScreen
+2
View File
@@ -33,6 +33,8 @@ BlazeComponent.extendComponent({
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
},
isStarred() {
const boardId = Session.get('currentBoard');
const user = ReactiveCache.getCurrentUser();
+533 -25
View File
@@ -1,5 +1,86 @@
@import url("../../../css/reset.css") print, screen;
/* Board List Header with Zoom Controls */
.board-list-header {
display: flex;
justify-content: center;
margin: 1vh 0 2vh 0;
padding: 1vh 0;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 0.5vw;
background: rgba(255, 255, 255, 0.9);
padding: 0.5vh 1vw;
border-radius: 0.5vw;
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
}
.zoom-controls .board-header-btn {
padding: 0.5vh 0.8vw !important;
border-radius: 0.3vw !important;
background: #fff !important;
border: 1px solid #000 !important;
transition: all 0.2s ease !important;
text-decoration: none !important;
color: #000 !important;
display: flex !important;
align-items: center !important;
gap: 0.3vw !important;
height: auto !important;
line-height: normal !important;
margin: 0 !important;
float: none !important;
overflow: visible !important;
}
.zoom-controls .board-header-btn i {
color: #000 !important;
float: none !important;
display: inline !important;
line-height: normal !important;
margin: 0 !important;
}
.zoom-controls .board-header-btn:hover {
background: #000 !important;
border-color: #000 !important;
color: #fff !important;
}
.zoom-controls .board-header-btn:hover i {
color: #fff !important;
}
.zoom-controls .board-header-btn.is-active {
background: #0079bf;
color: white;
border-color: #005a8a;
}
.zoom-controls .board-header-btn.is-active i {
color: white;
}
.zoom-level {
font-weight: bold;
color: #333;
min-width: 3vw;
text-align: center;
font-size: clamp(12px, 2vw, 14px);
cursor: pointer;
padding: 0.3vh 0.5vw;
border-radius: 0.3vw;
transition: all 0.2s ease;
}
.zoom-level:hover {
background: #f0f0f0;
color: #000;
}
.board-list {
margin: 0 8px;
}
@@ -194,15 +275,33 @@
}
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-list.mobile-view {
height: 100%;
overflow: scroll;
height: calc(100vh - 120px);
overflow-y: scroll !important;
overflow-x: hidden;
padding: 0 1rem;
margin: 0;
scrollbar-width: auto !important;
scrollbar-color: #888 #f1f1f1;
}
.board-list.mobile-view li {
width: 50%;
width: 100%;
float: none;
display: block;
margin-bottom: 1rem;
padding-right: 50px; /* Space for drag handle */
}
.board-list.mobile-view .board-list-item {
overflow: hidden;
overflow: visible;
height: 8rem;
width: 100%;
margin: 0;
padding-right: 50px; /* Ensure content doesn't overlap with drag handle */
}
.board-list.mobile-view .board-list-item .details {
padding-right: 50px; /* Extra space for drag handle */
width: 100%;
box-sizing: border-box;
}
.board-list.mobile-view .board-list-item-sub-name {
position: relative;
@@ -216,19 +315,89 @@
transform: translateY(-50%);
right: 10px;
font-size: 24px;
color: #fff;
background: rgba(0,0,0,0.3);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s ease;
}
@media screen and (max-width: 800px) {
.board-list.mobile-view .board-handle:hover {
background: rgba(255, 255, 0, 0.8) !important; /* Yellow hover */
}
/* Force scrollbar to be visible on mobile view */
.board-list.mobile-view::-webkit-scrollbar {
width: 12px !important;
display: block !important;
visibility: visible !important;
}
.board-list.mobile-view::-webkit-scrollbar-track {
background: #f1f1f1 !important;
border-radius: 6px !important;
display: block !important;
visibility: visible !important;
}
.board-list.mobile-view::-webkit-scrollbar-thumb {
background: #888 !important;
border-radius: 6px !important;
display: block !important;
visibility: visible !important;
}
.board-list.mobile-view::-webkit-scrollbar-thumb:hover {
background: #555 !important;
}
/* Force mobile view to have scrollable content */
.board-list.mobile-view {
min-height: 100vh; /* Force content to be tall enough to scroll */
}
.board-list.mobile-view::after {
content: '';
display: block;
height: 100px;
}
@media screen and (max-width: 800px),
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
.board-list {
height: 100%;
overflow: scroll;
overflow-y: auto;
overflow-x: hidden;
padding: 0 1rem;
margin: 0;
}
.board-list li {
width: 50%;
width: 100%;
float: none;
display: block;
margin-bottom: 1rem;
padding-right: 50px; /* Space for drag handle */
}
.board-list .board-list-item {
overflow: hidden;
overflow: visible;
height: 8rem;
width: 100%;
margin: 0;
padding-right: 50px; /* Ensure content doesn't overlap with drag handle */
}
.board-list .board-list-item .details {
padding-right: 50px; /* Extra space for drag handle */
width: 100%;
box-sizing: border-box;
}
.board-list .board-list-item-sub-name {
position: relative;
@@ -242,32 +411,296 @@
transform: translateY(-50%);
right: 10px;
font-size: 24px;
color: #fff;
background: rgba(0,0,0,0.3);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s ease;
}
}
/* Mobile view styles for very small screens - applied when isMiniScreen is true */
.board-list.mobile-view li {
width: 50%;
}
.board-list.mobile-view .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-list .board-handle:hover {
background: rgba(255, 255, 0, 0.8) !important; /* Yellow hover */
}
.board-handle {
}
/* Very small screens - ensure one board per row */
@media screen and (max-width: 360px) {
.board-list li {
width: 100% !important;
float: none !important;
display: block !important;
}
.board-list .board-handle {
position: absolute;
padding: 7px;
top: 50%;
transform: translateY(-50%);
right: 10px;
font-size: 24px;
color: #fff;
background: rgba(0,0,0,0.3);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
}
/* Mobile - make all text and icons 2x bigger above #content on All Boards page */
@media screen and (max-width: 800px),
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
.wrapper {
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
}
.wrapper * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
.wrapper .fa, .wrapper .icon {
font-size: 2em !important; /* 2x bigger icons */
}
.board-list-header {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list-header h1 {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.AllBoardTeamsOrgs {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.AllBoardTeamsOrgs select {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.AllBoardTeamsOrgs input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.AllBoardTeamsOrgs .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list .board-list-item {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list .board-list-item-name {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list .board-list-item-desc {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list .minicard-members {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list .minicard-lists {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.board-list .board-handle {
font-size: 1em !important; /* Use inherited 2x scaling */
}
}
/* Fallback for iPhone devices using JavaScript detection - All Boards page */
.iphone-device .wrapper {
font-size: 2em !important; /* 2x bigger base font size for All Boards page */
}
.iphone-device .wrapper * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
.iphone-device .wrapper .fa, .iphone-device .wrapper .icon {
font-size: 2em !important; /* 2x bigger icons */
}
.iphone-device .board-list-header {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list-header h1 {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .AllBoardTeamsOrgs {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .AllBoardTeamsOrgs select {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .AllBoardTeamsOrgs input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .AllBoardTeamsOrgs .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list .board-list-item {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list .board-list-item-name {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list .board-list-item-desc {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list .minicard-members {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list .minicard-lists {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .board-list .board-handle {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* iPhone 12 Mini and very small screens - make everything much larger */
@media screen and (max-width: 400px) and (max-height: 900px) {
.board-list {
height: calc(100vh - 120px) !important;
overflow-y: scroll !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
padding: 0 0.5rem;
}
.board-list li {
width: 100% !important;
float: none !important;
display: block !important;
margin-bottom: 1.5rem !important;
}
.board-list .board-list-item {
height: 12rem !important; /* Much taller */
width: 100% !important;
margin: 0 !important;
padding: 1rem !important; /* More padding */
font-size: 18px !important; /* Much larger text */
line-height: 1.4 !important;
}
.board-list .board-list-item .board-list-item-name {
font-size: 20px !important; /* Larger board names */
font-weight: bold !important;
margin-bottom: 0.5rem !important;
}
.board-list .board-list-item .board-list-item-desc {
font-size: 16px !important; /* Larger descriptions */
line-height: 1.3 !important;
}
.board-list .board-list-item .minicard-members {
font-size: 14px !important; /* Larger member avatars */
}
.board-list .board-list-item .minicard-lists {
font-size: 14px !important; /* Larger list counters */
}
.board-list .board-handle {
position: absolute;
padding: 10px !important;
top: 50%;
transform: translateY(-50%);
right: 15px !important;
font-size: 28px !important; /* Much larger drag handle */
color: #fff;
background: rgba(0,0,0,0.4) !important;
border-radius: 50%;
width: 50px !important; /* Larger handle */
height: 50px !important;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s ease;
}
.board-list .board-handle:hover {
background: rgba(255, 255, 0, 0.8) !important; /* Yellow hover */
}
/* Force scrollbar to be visible and larger */
.board-list::-webkit-scrollbar {
width: 16px !important; /* Much wider scrollbar */
display: block !important;
visibility: visible !important;
}
.board-list::-webkit-scrollbar-track {
background: #f1f1f1 !important;
border-radius: 8px !important;
display: block !important;
visibility: visible !important;
}
.board-list::-webkit-scrollbar-thumb {
background: #666 !important; /* Darker for better visibility */
border-radius: 8px !important;
display: block !important;
visibility: visible !important;
min-height: 50px !important; /* Minimum thumb size */
}
.board-list::-webkit-scrollbar-thumb:hover {
background: #333 !important;
}
/* Ensure scrollbar is always visible */
.board-list {
scrollbar-gutter: stable;
scrollbar-width: auto !important;
min-height: 100vh !important;
}
.board-list::after {
content: '';
display: block;
height: 200px !important; /* More space to ensure scrolling */
}
}
.AllBoardTeamsOrgs {
@@ -318,3 +751,78 @@
padding-right: 6px;
text-align: center;
}
/* Fix multiple scrollbars issue on mobile */
@media screen and (max-width: 800px),
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
.wrapper {
overflow: hidden;
height: 100vh;
}
.board-list {
overflow-y: scroll !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: #888 #f1f1f1;
height: calc(100vh - 120px); /* Ensure there's content to scroll */
}
/* Force scrollbar to always be visible */
.board-list::-webkit-scrollbar {
width: 12px !important;
display: block !important;
visibility: visible !important;
}
.board-list::-webkit-scrollbar-track {
background: #f1f1f1 !important;
border-radius: 6px !important;
display: block !important;
visibility: visible !important;
}
.board-list::-webkit-scrollbar-thumb {
background: #888 !important;
border-radius: 6px !important;
display: block !important;
visibility: visible !important;
}
.board-list::-webkit-scrollbar-thumb:hover {
background: #555 !important;
}
/* Ensure scrollbar is always visible */
.board-list::-webkit-scrollbar-corner {
background: #f1f1f1 !important;
}
/* Force scrollbar to be visible */
.board-list {
scrollbar-gutter: stable;
scrollbar-width: auto !important;
min-height: 100vh; /* Force content to be tall enough to scroll */
}
/* Ensure there's always content to scroll */
.board-list::after {
content: '';
display: block;
height: 100px;
}
/* Ensure only one scrollbar is visible */
body {
overflow: hidden;
}
#content {
overflow: hidden;
}
}
+2
View File
@@ -1,5 +1,7 @@
template(name="boardList")
.wrapper
.board-list-header
ul.AllBoardTeamsOrgs
li.AllBoardTeams
if userHasTeams
+3
View File
@@ -22,6 +22,9 @@ Template.boardListHeaderBar.events({
},
});
Template.boardList.events({
});
Template.boardListHeaderBar.helpers({
title() {
//if (FlowRouter.getRouteName() === 'template-container') {
+136
View File
@@ -106,6 +106,142 @@
color: white;
cursor: pointer;
font-size: 4em;
}
/* Upload progress indicators for drag-and-drop uploads */
.minicard-upload-progress,
.card-details-upload-progress {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 12px;
margin: 8px 0;
font-size: 14px;
}
.upload-progress-header {
display: flex;
align-items: center;
margin-bottom: 8px;
font-weight: bold;
color: #495057;
}
.upload-progress-header i {
margin-right: 8px;
color: #007bff;
}
.upload-progress-item {
display: flex;
flex-direction: column;
margin-bottom: 8px;
padding: 8px;
background: white;
border-radius: 3px;
border: 1px solid #dee2e6;
}
.upload-progress-item.upload-error {
border-color: #dc3545;
background: #f8d7da;
}
.upload-progress-filename {
font-weight: 500;
margin-bottom: 4px;
color: #495057;
word-break: break-all;
}
.upload-progress-bar {
width: 100%;
height: 6px;
background: #e9ecef;
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.upload-progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
border-radius: 3px;
}
.upload-progress-item.upload-error .upload-progress-fill {
background: #dc3545;
}
.upload-progress-error,
.upload-progress-success {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 500;
}
.upload-progress-error {
color: #dc3545;
}
.upload-progress-success {
color: #28a745;
}
.upload-progress-error i,
.upload-progress-success i {
margin-right: 4px;
}
/* Minicard specific styles */
.minicard-upload-progress {
margin: 4px 0;
padding: 8px;
font-size: 12px;
}
.minicard-upload-progress .upload-progress-item {
padding: 6px;
margin-bottom: 6px;
}
.minicard-upload-progress .upload-progress-filename {
font-size: 11px;
}
/* Card details specific styles */
.card-details-upload-progress {
margin: 12px 0;
padding: 16px;
}
.card-details-upload-progress .upload-progress-header {
font-size: 16px;
margin-bottom: 12px;
}
.card-details-upload-progress .upload-progress-item {
padding: 12px;
margin-bottom: 10px;
}
.card-details-upload-progress .upload-progress-filename {
font-size: 14px;
}
/* Drag over state for minicards */
.minicard.is-dragging-over {
border: 2px dashed #007bff !important;
background: rgba(0, 123, 255, 0.1) !important;
}
/* Drag over state for card details */
.js-card-details.is-dragging-over {
border: 2px dashed #007bff !important;
background: rgba(0, 123, 255, 0.05) !important;
}
top: 0;
right: 8px;
position: absolute;
+106 -37
View File
@@ -1,6 +1,8 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ObjectID } from 'bson';
import DOMPurify from 'dompurify';
import { sanitizeHTML, sanitizeText } from '/imports/lib/secureDOMPurify';
import uploadProgressManager from '../../lib/uploadProgressManager';
const filesize = require('filesize');
const prettyMilliseconds = require('pretty-ms');
@@ -268,7 +270,7 @@ Template.attachmentGallery.helpers({
return ret;
},
sanitize(value) {
return DOMPurify.sanitize(value);
return sanitizeHTML(value);
},
});
@@ -296,44 +298,13 @@ Template.cardAttachmentsPopup.events({
const files = event.currentTarget.files;
if (files) {
let uploads = [];
for (const file of files) {
const fileId = new ObjectID().toString();
let fileName = DOMPurify.sanitize(file.name);
const uploaders = handleFileUpload(card, files);
// If sanitized filename is not same as original filename,
// 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,
);
uploaders.forEach(uploader => {
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);
@@ -341,8 +312,7 @@ Template.cardAttachmentsPopup.events({
Popup.back();
}
});
uploader.start();
}
});
}
},
'click .js-computer-upload'(event, templateInstance) {
@@ -356,6 +326,105 @@ const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
let pastedResults = null;
// Shared upload logic for drag-and-drop functionality
export function handleFileUpload(card, files) {
if (!files || files.length === 0) {
return [];
}
// Check if board allows attachments
const board = card.board();
if (!board || !board.allowsAttachments) {
if (process.env.DEBUG === 'true') {
console.warn('Attachments not allowed on this board');
}
return [];
}
// Check if user can modify the card
if (!card.canModifyCard()) {
if (process.env.DEBUG === 'true') {
console.warn('User does not have permission to modify this card');
}
return [];
}
const uploads = [];
for (const file of files) {
// Basic file validation
if (!file || !file.name) {
if (process.env.DEBUG === 'true') {
console.warn('Invalid file object');
}
continue;
}
const fileId = new ObjectID().toString();
let fileName = sanitizeText(file.name);
// If sanitized filename is not same as original filename,
// 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;
try {
const uploader = Attachments.insert(
config,
false,
);
// Add to progress manager for tracking
const uploadId = uploadProgressManager.addUpload(card._id, uploader, file);
uploader.on('uploaded', (error, fileRef) => {
if (!error) {
if (fileRef.isImage) {
card.setCover(fileRef._id);
if (process.env.DEBUG === 'true') {
console.log(`Set cover image for card ${card._id}: ${fileRef.name}`);
}
}
} else {
if (process.env.DEBUG === 'true') {
console.error('Upload error:', error);
}
}
});
uploader.on('error', (error) => {
if (process.env.DEBUG === 'true') {
console.error('Upload error:', error);
}
});
uploads.push(uploader);
uploader.start();
} catch (error) {
if (process.env.DEBUG === 'true') {
console.error('Failed to create uploader:', error);
}
}
}
return uploads;
}
Template.previewClipboardImagePopup.onRendered(() => {
// we can paste image from clipboard
const handle = results => {
@@ -498,7 +567,7 @@ BlazeComponent.extendComponent({
const name = this.$('.js-edit-attachment-name')[0]
.value
.trim() + this.data().extensionWithDot;
if (name === DOMPurify.sanitize(name)) {
if (name === sanitizeText(name)) {
Meteor.call('renameAttachment', this.data()._id, name);
}
Popup.back();
+22 -14
View File
@@ -1,11 +1,11 @@
.assignee {
border-radius: 3px;
border-radius: 0.4vw;
display: block;
position: relative;
float: left;
height: 30px;
width: 30px;
margin: .3vh;
height: 4vw;
width: 4vw;
margin: 0.4vh;
cursor: pointer;
user-select: none;
z-index: 1;
@@ -34,11 +34,11 @@
background-color: #b3b3b3;
border: 1px solid #fff;
border-radius: 50%;
height: 7px;
width: 7px;
height: 1vw;
width: 1vw;
position: absolute;
right: -1px;
bottom: -1px;
right: -0.1vw;
bottom: -0.1vw;
border: 1px solid #fff;
z-index: 15;
}
@@ -70,23 +70,23 @@
}
.copied-tooltip {
display: none;
padding: 0px 10px;
padding: 0 1.3vw;
background-color: rgba(0,0,0,0.875);
color: #fff;
border-radius: 5px;
border-radius: 0.7vw;
}
.card-details {
padding: 0;
flex-shrink: 0;
flex-basis: 600px;
flex-basis: min(600px, 80vw);
will-change: flex-basis;
overflow-y: scroll;
overflow-x: hidden;
background: #f7f7f7;
border-radius: bottom 3px;
border-radius: bottom 0.4vw;
z-index: 30;
animation: flexGrowIn 0.1s;
box-shadow: 0 0 7px 0 #b3b3b3;
box-shadow: 0 0 0.9vh 0 #b3b3b3;
transition: flex-basis 0.1s;
box-sizing: border-box;
}
@@ -95,7 +95,7 @@
}
.card-details .card-details-canvas {
width: auto;
padding: 0 20px;
padding: 0 2.5vw;
}
.card-details .card-details-header {
margin: 0 -20px 5px;
@@ -596,3 +596,11 @@ input[type="submit"].attachment-add-link-submit {
overflow: hidden;
background-color: #cecece;
}
/* Drag and drop file upload visual feedback */
.js-card-details.is-dragging-over {
border: 2px dashed #0079bf;
background-color: #e3f2fd !important;
transform: scale(1.01);
transition: all 0.2s ease;
}
+28
View File
@@ -65,6 +65,26 @@ template(name="cardDetails")
else
p.warning {{_ 'card-archived'}}
// Upload progress indicator for drag-and-drop uploads
if hasActiveUploads
.card-details-upload-progress
.upload-progress-header
i.fa.fa-upload
span {{_ 'uploading-files'}} ({{uploadCount}})
each uploads
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
.upload-progress-filename {{file.name}}
.upload-progress-bar
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-exclamation-triangle
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
span {{_ 'upload-completed'}}
.card-details-left
.card-details-items
@@ -675,6 +695,14 @@ template(name="cardDetailsActionsPopup")
a.js-set-card-color
i.fa.fa-paint-brush
| {{_ 'setCardColorPopup-title'}}
li
a.js-toggle-show-list-on-minicard
if showListOnMinicard
i.fa.fa-eye
| {{_ 'hide-list-on-minicard'}}
else
i.fa.fa-eye-slash
| {{_ 'show-list-on-minicard'}}
hr
ul.pop-over-list
li
+75
View File
@@ -12,6 +12,8 @@ import CardComments from '/models/cardComments';
import { ALLOWED_COLORS } from '/config/const';
import { UserAvatar } from '../users/userAvatar';
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
const subManager = new SubsManager();
const { calculateIndexData } = Utils;
@@ -481,6 +483,59 @@ BlazeComponent.extendComponent({
}
}
},
// Drag and drop file upload handlers
'dragover .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
},
'dragenter .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (card.canModifyCard() && board && board.allowsAttachments) {
// Check if the drag contains files
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
},
'drop .js-card-details'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// Check permissions
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a checklist item reorder)
const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
// Check if the drop contains files (not just text/HTML)
if (!dataTransfer.types.includes('Files')) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
},
},
];
},
@@ -490,6 +545,16 @@ Template.cardDetails.helpers({
isPopup() {
let ret = !!Utils.getPopupCardId();
return ret;
},
// Upload progress helpers
hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id);
},
uploads() {
return uploadProgressManager.getUploadsForCard(this._id);
},
uploadCount() {
return uploadProgressManager.getUploadCountForCard(this._id);
}
});
Template.cardDetailsPopup.onDestroyed(() => {
@@ -592,6 +657,10 @@ Template.cardDetailsActionsPopup.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
showListOnMinicard() {
return this.showListOnMinicard;
},
});
Template.cardDetailsActionsPopup.events({
@@ -637,6 +706,12 @@ Template.cardDetailsActionsPopup.events({
if (!err && ret) Popup.close();
});
},
'click .js-toggle-show-list-on-minicard'() {
const currentCard = this;
const newValue = !currentCard.showListOnMinicard;
Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } });
Popup.close();
},
});
BlazeComponent.extendComponent({
+83 -60
View File
@@ -3,11 +3,11 @@
position: relative;
display: flex;
align-items: center;
margin-bottom: 9px;
margin-bottom: 1.2vh;
}
.minicard-wrapper.placeholder {
background: #ccc;
border-radius: 9px;
border-radius: 1.2vw;
}
.minicard-wrapper.ui-sortable-helper {
cursor: grabbing;
@@ -16,28 +16,28 @@
}
.minicard-wrapper.ui-sortable-helper .and-n-other {
width: 100%;
height: 16px;
padding: 4px;
height: 2.5vh;
padding: 0.5vh;
background-color: #f2f2f2;
text-align: center;
border-radius: 3px;
border-radius: 0.4vw;
}
.minicard-wrapper.ui-sortable-helper .multi-selection-checkbox {
display: none;
}
.minicard-wrapper .multi-selection-checkbox + .minicard {
margin-left: 8px;
margin-left: 1vw;
}
@media only screen {
.minicard {
padding: 6px 8px 2px;
padding: 0.8vh 1vw 0.3vh;
position: relative;
flex: 1;
flex-wrap: wrap;
background-color: #fff;
min-height: 20px;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
border-radius: 2px;
min-height: 2.5vh;
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
border-radius: 0.3vw;
color: #4d4d4d;
overflow: hidden;
transition: transform 0.2s, border-radius 0.2s;
@@ -45,14 +45,14 @@
}
.minicard-details-menu-with-handle {
float: right;
font-size: 18px;
padding-right: 30px;
padding-left: 5px;
font-size: clamp(14px, 3vw, 18px);
padding-right: 4vw;
padding-left: 0.7vw;
}
.minicard-details-menu {
float: right;
font-size: 18px;
padding-left: 5px;
font-size: clamp(14px, 3vw, 18px);
padding-left: 0.7vw;
}
@media print {
.minicard-details-menu,
@@ -63,7 +63,7 @@
.minicard.linked-board .linked-icon,
.minicard.linked-card .linked-icon {
display: inline-block;
margin-right: 11px;
margin-right: 1.5vw;
vertical-align: baseline;
font-size: 0.9em;
}
@@ -72,11 +72,11 @@
color: #937760;
}
.is-selected .minicard {
transform: translateX(11px);
transform: translateX(1.5vw);
border-bottom-right-radius: 0;
border-top-right-radius: 0;
z-index: 25;
box-shadow: -2px 1px 2px rgba(0,0,0,0.2);
box-shadow: -0.3vw 0.2vh 0.3vh rgba(0,0,0,0.2);
}
.minicard:hover:not(.minicard-composer),
.is-selected .minicard,
@@ -90,20 +90,20 @@
background-position: center;
background-repeat: no-repeat;
background-size: contain;
height: 145px;
height: 20vh;
user-select: none;
margin: 6px -8px 6px -8px;
border-radius: top 2px;
margin: 0.8vh -1vw 0.8vh -1vw;
border-radius: top 0.3vw;
}
.minicard .minicard-labels {
float: none;
}
.minicard .minicard-labels .minicard-label {
width: 11px;
height: 11px;
border-radius: 2px;
margin-right: 3px;
margin-bottom: 3px;
width: 1.5vw;
height: 1.5vw;
border-radius: 0.3vw;
margin-right: 0.4vw;
margin-bottom: 0.4vh;
}
.minicard .minicard-labels-no-text {
display: flex;
@@ -119,22 +119,22 @@
flex-grow: 1;
display: block;
word-wrap: break-word;
max-width: 100px;
margin-right: 4px;
max-width: 13vw;
margin-right: 0.5vw;
}
.minicard .minicard-custom-field-item-fullwidth {
flex-grow: 1;
display: block;
word-wrap: break-word;
max-width: 100%;
margin-right: 4px;
margin-right: 0.5vw;
}
.minicard .handle {
width: 20px;
height: 20px;
width: 2.5vw;
height: 2.5vw;
position: absolute;
right: 5px;
top: 5px;
right: 0.7vw;
top: 0.7vh;
display: none;
}
@media only screen {
@@ -143,13 +143,13 @@
}
}
.minicard .handle .fa-arrows {
font-size: 20px;
font-size: clamp(16px, 3vw, 20px);
color: #ccc;
}
.minicard .minicard-title .card-number {
color: #b3b3b3;
display: inline-block;
margin-right: 5px;
margin-right: 0.7vw;
}
@media only screen {
.minicard .minicard-title p:last-child {
@@ -166,11 +166,11 @@
flex-wrap: wrap;
}
.minicard .date {
margin-right: 3px;
margin-right: 0.4vw;
}
.minicard .badges {
float: left;
margin-top: 7px;
margin-top: 1vh;
color: #808080;
}
.minicard .badges:empty {
@@ -178,14 +178,14 @@
}
.minicard .badges .badge {
float: left;
margin-right: 11px;
margin-bottom: 3px;
margin-right: 1.5vw;
margin-bottom: 0.4vh;
font-size: 0.9em;
}
.minicard .badges .badge.is-finished {
background: #3cb500;
padding: 0px 3px;
border-radius: 3px;
padding: 0 0.4vw;
border-radius: 0.4vw;
color: #fff;
}
.minicard .badges .badge:last-of-type {
@@ -201,41 +201,41 @@
}
.minicard .badges .badge .badge-text {
font-size: 0.9em;
padding-left: 2px;
line-height: 14px;
padding-left: 0.3vw;
line-height: 1.2;
}
.minicard .badges .badge .check-list-text {
padding-left: 0px;
line-height: 12px;
line-height: 1.1;
}
.minicard .minicard-members,
.minicard .minicard-assignees,
.minicard .minicard-creator {
float: right;
margin-left: 5px;
margin-bottom: 4px;
margin-left: 0.7vw;
margin-bottom: 0.5vh;
}
.minicard .minicard-members .member,
.minicard .minicard-assignees .member,
.minicard .minicard-creator .member {
float: right;
border-radius: 50%;
height: 28px;
width: 28px;
margin-bottom: 4px;
height: 3.5vw;
width: 3.5vw;
margin-bottom: 0.5vh;
}
.minicard .minicard-members .assignee,
.minicard .minicard-assignees .assignee,
.minicard .minicard-creator .assignee {
float: right;
border-radius: 50%;
height: 28px;
width: 28px;
height: 3.5vw;
width: 3.5vw;
}
.minicard .minicard-members + .badges,
.minicard .minicard-assignees + .badges,
.minicard .minicard-creator + .badges {
margin-top: 5px;
margin-top: 0.7vh;
}
.minicard .minicard-assignees {
border-bottom: 1px solid #f00;
@@ -248,13 +248,13 @@
display: none;
}
.minicard .minicard-description {
padding: 6px 0 0 8px;
padding: 0.8vh 0 0 1vw;
color: #000;
background-color: #eee;
width: 100%;
margin-bottom: 2px;
margin-left: -4px;
border-radius: 3px;
margin-bottom: 0.3vh;
margin-left: -0.5vw;
border-radius: 0.4vw;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
@@ -262,7 +262,7 @@
-webkit-box-orient: vertical;
}
.minicard.minicard-composer {
margin-bottom: 10px;
margin-bottom: 1.3vh;
}
.minicard.minicard-composer textarea.minicard-composer-textarea,
.minicard.minicard-composer textarea.minicard-composer-textarea:focus {
@@ -273,9 +273,9 @@
height: auto;
margin: 0;
padding: 0;
max-height: 162px;
min-height: 36px;
margin-bottom: 20px;
max-height: 22vh;
min-height: 5vh;
margin-bottom: 2.5vh;
overflow-y: auto;
}
.parent-prefix {
@@ -580,3 +580,26 @@
.text-green {
color: #008000;
}
/* Drag and drop file upload visual feedback */
.minicard.is-dragging-over {
border: 2px dashed #0079bf;
background-color: #e3f2fd !important;
transform: scale(1.02);
transition: all 0.2s ease;
}
/* List name display on minicard */
.minicard-list-name {
font-size: 0.75em;
color: #8c8c8c;
margin-top: 0.2vh;
display: flex;
align-items: center;
gap: 0.3vw;
}
.minicard-list-name i.fa {
font-size: 0.8em;
opacity: 0.7;
}
+31 -6
View File
@@ -31,6 +31,27 @@ template(name="minicard")
if cover
if currentBoard.allowsCoverAttachmentOnMinicard
.minicard-cover(style="background-image: url('{{cover.link 'original'}}?dummyReloadAfterSessionEstablished={{sess}}');")
// Upload progress indicator for drag-and-drop uploads
if hasActiveUploads
.minicard-upload-progress
.upload-progress-header
i.fa.fa-upload
span {{_ 'uploading-files'}} ({{uploadCount}})
each uploads
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
.upload-progress-filename {{file.name}}
.upload-progress-bar
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-exclamation-triangle
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
span {{_ 'upload-completed'}}
.minicard-title
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
.parent-prefix
@@ -51,12 +72,6 @@ template(name="minicard")
span.card-number
| ##{getCardNumber}
= getTitle
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
| {{ parentString ' > ' }}
if $eq 'subtext-with-parent' currentBoard.presentParentTask
.parent-subtext
| {{ parentCardName }}
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
@@ -176,6 +191,16 @@ template(name="minicard")
.minicard-description
+viewer
| {{ getDescription }}
if shouldShowListOnMinicard
.minicard-list-name
i.fa.fa-list
| {{ listName }}
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
| {{ parentString ' > ' }}
if $eq 'subtext-with-parent' currentBoard.presentParentTask
.parent-subtext
| {{ parentCardName }}
template(name="editCardSortOrderPopup")
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
+77 -1
View File
@@ -1,6 +1,8 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { CustomFieldStringTemplate } from '/client/lib/customFields'
import { CustomFieldStringTemplate } from '/client/lib/customFields';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
// Template.cards.events({
// 'click .member': Popup.open('cardMember')
@@ -107,6 +109,59 @@ BlazeComponent.extendComponent({
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
'click .minicard-labels' : this.cardLabelsPopup,
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
// Drag and drop file upload handlers
'dragover .minicard'(event) {
event.preventDefault();
event.stopPropagation();
},
'dragenter .minicard'(event) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (card.canModifyCard() && board && board.allowsAttachments) {
// Check if the drag contains files
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .minicard'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
},
'drop .minicard'(event) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// Check permissions
if (!card.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a card reorder)
const dataTransfer = event.originalEvent.dataTransfer;
if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
// Check if the drop contains files (not just text/HTML)
if (!dataTransfer.types.includes('Files')) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
},
}
];
},
@@ -131,6 +186,27 @@ Template.minicard.helpers({
},
isWatching() {
return this.findWatcher(Meteor.userId());
},
// Upload progress helpers
hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id);
},
uploads() {
return uploadProgressManager.getUploadsForCard(this._id);
},
uploadCount() {
return uploadProgressManager.getUploadCountForCard(this._id);
},
listName() {
const list = this.list();
return list ? list.title : '';
},
shouldShowListOnMinicard() {
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
return this.currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
}
});
+29 -29
View File
@@ -5,11 +5,11 @@ button {
box-sizing: border-box;
background-color: #ebebeb;
border: 1px solid #ccc;
border-radius: 3px;
border-radius: 0.4vw;
display: block;
margin-bottom: 12px;
min-height: 34px;
padding: 7px;
margin-bottom: 1.5vh;
min-height: 4.5vh;
padding: 1vh 1vw;
}
select.full,
textarea.full,
@@ -31,7 +31,7 @@ button:focus {
outline: 0;
}
input[type="file"] {
margin-bottom: 16px;
margin-bottom: 2vh;
}
input[type="radio"] {
-webkit-appearance: radio;
@@ -42,7 +42,7 @@ input[type="text"],
input[type="password"],
input[type="email"] {
transition: background 85ms ease-in, border-color 85ms ease-in;
width: 250px;
width: min(250px, 30vw);
}
input[type="text"].inline-input,
input[type="password"].inline-input,
@@ -50,10 +50,10 @@ input[type="email"].inline-input {
background: none;
border: 0;
margin: 0;
padding: 2px;
padding: 0.3vh;
min-height: 0;
height: 18px;
width: 200px;
height: 2.5vh;
width: min(200px, 25vw);
}
input[type="text"].full-line,
input[type="password"].full-line,
@@ -103,9 +103,9 @@ textarea:disabled {
user-select: none;
}
select {
max-height: 300px;
width: 256px;
margin-bottom: 8px;
max-height: 40vh;
width: min(256px, 32vw);
margin-bottom: 1vh;
}
select.inline {
width: 100%;
@@ -114,14 +114,14 @@ option[disabled] {
color: #222;
}
textarea {
height: 150px;
height: 20vh;
transition: background 85ms ease-in, border-color 85ms ease-in;
resize: vertical;
width: 100%;
}
textarea.editor {
resize: none;
padding-bottom: 22px;
padding-bottom: 3vh;
}
.button {
border-radius: 3px;
@@ -136,14 +136,14 @@ button {
cursor: pointer;
display: inline-block;
font-weight: 700;
line-height: 22px;
padding: 7px 20px;
line-height: 1.3;
padding: 1vh 2.5vw;
text-align: center;
}
input[type="submit"] .wide,
button .wide {
padding-left: 30px;
padding-right: 30px;
padding-left: 4vw;
padding-right: 4vw;
}
input[type="submit"]:hover,
button:hover,
@@ -224,8 +224,8 @@ button.disabled:active,
}
fieldset {
border: 1px solid #bfbfbf;
padding: 15px;
margin-bottom: 15px;
padding: 2vh 2vw;
margin-bottom: 2vh;
}
input[type="hidden"] {
display: none;
@@ -233,18 +233,18 @@ input[type="hidden"] {
.radio-div,
.check-div {
display: block;
margin: 0 0 4px 20px;
min-height: 20px;
margin: 0 0 0.5vh 2.5vw;
min-height: 2.5vh;
position: relative;
}
.radio-div input,
.check-div input {
left: -18px;
left: -2.5vw;
min-height: 0;
margin: 0;
padding: 0;
position: absolute;
top: 2px;
top: 0.3vh;
}
.radio-div label,
.check-div label {
@@ -253,7 +253,7 @@ input[type="hidden"] {
label {
display: block;
font-weight: 700;
margin-bottom: 4px;
margin-bottom: 0.5vh;
}
label.form-error {
color: #d32f2f;
@@ -269,7 +269,7 @@ textarea::-moz-placeholder {
display: flex;
align-items: center;
margin-top: 0px;
margin-bottom: 12px;
margin-bottom: 1.5vh;
}
@media print {
.add-controls {
@@ -281,13 +281,13 @@ textarea::-moz-placeholder {
.edit-controls input[type=button],
.add-controls input[type=button] {
float: left;
height: 32px;
height: 4.5vh;
margin-bottom: 0px;
}
.edit-controls .fa-times-thin,
.add-controls .fa-times-thin {
font-size: 26px;
margin: 3px 10px;
font-size: clamp(20px, 4vw, 26px);
margin: 0.5vh 1.5vw;
}
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
+8 -8
View File
@@ -5,17 +5,17 @@
.map-members .mapping-list {
display: flex;
flex-wrap: wrap;
margin: 0 -4px;
margin: 0 -0.5vw;
}
.map-members .mapping-list .mapping-item {
max-width: 300px;
min-width: 200px;
padding: 6px;
margin: 5px;
max-width: min(300px, 40vw);
min-width: min(200px, 26vw);
padding: 0.8vh 0.8vw;
margin: 0.7vh;
flex: 1;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
border-radius: 0.4vw;
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
}
.map-members .mapping-list .mapping-item:hover {
background: #f2f2f2;
@@ -37,7 +37,7 @@
}
.map-members .mapping-list .wekan {
display: inline-block;
width: 35px;
width: 4.5vw;
}
.map-members .mapping-list .wekan .member {
float: none;
+155 -17
View File
@@ -9,7 +9,7 @@
float: left;
}
[id^="swimlane-"] .list:first-child {
min-width: 20px;
min-width: 2.5vw;
}
.list.list-auto-width {
flex: 1;
@@ -33,7 +33,7 @@
background-color: rgba(0,0,0,0.2);
border-color: transparent;
box-shadow: none;
height: 100px;
height: 15vh;
}
.list.list-collapsed {
flex: none;
@@ -45,24 +45,24 @@
.list.list-composer .list-name-input,
.list .list-composer .list-name-input {
background: #fff;
margin: -3px 0 8px;
margin: -0.4vh 0 1vh;
}
.list-header-add {
flex: 0 0 auto;
padding: 12px;
padding: 1.5vh 1.5vw;
position: relative;
min-height: 20px;
min-height: 2.5vh;
}
.list-header {
flex: 0 0 auto;
padding: 20px 12px 4px;
padding: 2.5vh 1.5vw 0.5vh;
position: relative;
min-height: 20px;
min-height: 2.5vh;
background-color: #e4e4e4;
border-bottom: 6px solid #e4e4e4;
border-bottom: 0.8vh solid #e4e4e4;
}
.list-header.list-header-card-count {
min-height: 35px;
min-height: 4.5vh;
height: auto;
}
.list-header.ui-sortable-handle {
@@ -73,21 +73,21 @@
}
.list-header .list-header-name {
display: inline;
font-size: 16px;
line-height: 17px;
font-size: clamp(14px, 3vw, 18px);
line-height: 1.2;
margin: 0;
font-weight: bold;
min-height: 9px;
min-width: 30px;
min-height: 1.2vh;
min-width: 4vw;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
}
.list-rotated {
width: 10px;
height: 250px;
margin-top: -90px;
margin-left: -110px;
width: 1.3vw;
height: 35vh;
margin-top: -12vh;
margin-left: -14vw;
margin-right: 0;
transform: rotate(90deg);
position: relative;
@@ -219,6 +219,7 @@
flex: 0 0 60px;
height: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
border-bottom: 1px solid #ccc;
}
@@ -226,6 +227,7 @@
display: contents;
flex-basis: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
}
.list.mobile-view:first-child {
@@ -250,6 +252,8 @@
}
.list.mobile-view .list-body {
padding: 15px 19px;
width: 100%;
min-width: 100%;
}
.list.mobile-view .list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/
@@ -258,6 +262,8 @@
min-height: 30px;
margin-top: 10px;
align-items: center;
width: 100%;
min-width: 100%;
/* Force grid layout for iPhone */
display: grid !important;
grid-template-columns: 30px 1fr auto auto !important;
@@ -338,6 +344,7 @@
flex: 0 0 60px;
height: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
border-bottom: 1px solid #ccc;
}
@@ -345,6 +352,7 @@
display: contents;
flex-basis: auto;
width: 100%;
min-width: 100%;
border-left: 0px;
}
.list:first-child {
@@ -369,6 +377,8 @@
}
.list-body {
padding: 15px 19px;
width: 100%;
min-width: 100%;
}
.list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/
@@ -377,6 +387,8 @@
min-height: 30px;
margin-top: 10px;
align-items: center;
width: 100%;
min-width: 100%;
}
.list-header .list-header-left-icon {
padding: 7px;
@@ -451,6 +463,132 @@
align-items: initial;
}
}
/* iPhone 12 Mini specific - fix icon positioning in stacked lists view */
@media screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
screen and (max-width: 375px) and (max-height: 812px), /* iPhone 12 Mini viewport */
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 375px) /* iPhone 12 Mini Retina */ {
.list.mobile-view .list-header {
/* Force grid layout for iPhone 12 Mini */
display: grid !important;
grid-template-columns: 30px 1fr auto auto !important;
gap: 10px !important;
align-items: center !important;
}
.list.mobile-view .list-header .list-header-menu-icon {
/* Remove absolute positioning for iPhone 12 Mini */
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
/* Use grid positioning */
grid-row: 1/3 !important;
grid-column: 3 !important;
padding: 14px !important;
font-size: 40px !important;
text-align: center !important;
}
.list.mobile-view .list-header .list-header-handle {
/* Remove absolute positioning for iPhone 12 Mini */
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
/* Use grid positioning */
grid-row: 1/3 !important;
grid-column: 4 !important;
padding: 14px !important;
font-size: 48px !important;
text-align: center !important;
}
.list.mobile-view .list-header .list-header-name {
grid-row: 1 !important;
grid-column: 2 !important;
align-self: end !important;
font-size: 20px !important;
font-weight: bold !important;
line-height: 1.2 !important;
padding-bottom: 2px !important;
}
.list.mobile-view .list-header .cardCount {
grid-row: 2 !important;
grid-column: 2 !important;
align-self: start !important;
font-size: 16px !important;
line-height: 1.2 !important;
}
.list.mobile-view .list-header .list-header-left-icon {
display: grid !important;
grid-row: 1/3 !important;
grid-column: 1 !important;
}
}
/* iPhone device JavaScript detection fallback - fix icon positioning */
.iphone-device .list.mobile-view .list-header {
/* Force grid layout for iPhone devices */
display: grid !important;
grid-template-columns: 30px 1fr auto auto !important;
gap: 10px !important;
align-items: center !important;
}
.iphone-device .list.mobile-view .list-header .list-header-menu-icon {
/* Remove absolute positioning for iPhone devices */
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
/* Use grid positioning */
grid-row: 1/3 !important;
grid-column: 3 !important;
padding: 14px !important;
font-size: 40px !important;
text-align: center !important;
}
.iphone-device .list.mobile-view .list-header .list-header-handle {
/* Remove absolute positioning for iPhone devices */
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
/* Use grid positioning */
grid-row: 1/3 !important;
grid-column: 4 !important;
padding: 14px !important;
font-size: 48px !important;
text-align: center !important;
}
.iphone-device .list.mobile-view .list-header .list-header-name {
grid-row: 1 !important;
grid-column: 2 !important;
align-self: end !important;
font-size: 20px !important;
font-weight: bold !important;
line-height: 1.2 !important;
padding-bottom: 2px !important;
}
.iphone-device .list.mobile-view .list-header .cardCount {
grid-row: 2 !important;
grid-column: 2 !important;
align-self: start !important;
font-size: 16px !important;
line-height: 1.2 !important;
}
.iphone-device .list.mobile-view .list-header .list-header-left-icon {
display: grid !important;
grid-row: 1/3 !important;
grid-column: 1 !important;
}
.link-board-wrapper {
display: flex;
align-items: baseline;
+18 -18
View File
@@ -1,27 +1,27 @@
.my-cards-board-wrapper {
border-radius: 0 0 4px 4px;
min-width: 400px;
margin-bottom: 2rem;
border-radius: 0 0 0.5vw 0.5vw;
min-width: min(400px, 52vw);
margin-bottom: 2.5vh;
margin-right: auto;
margin-left: auto;
border-width: 2px;
border-width: 0.3vw;
border-style: solid;
border-color: #a2a2a2;
}
.my-cards-board-title {
font-size: 1.4rem;
font-size: clamp(1.2rem, 3vw, 1.6rem);
font-weight: bold;
padding: 0.5rem;
padding: 0.7vh 0.7vw;
background-color: #808080;
color: #fff;
}
.my-cards-swimlane-title {
font-size: 1.1rem;
font-size: clamp(1rem, 2.5vw, 1.3rem);
font-weight: bold;
padding: 0.5rem;
padding-bottom: 0.4rem;
padding: 0.7vh 0.7vw;
padding-bottom: 0.5vh;
margin-top: 0;
margin-bottom: 0.5rem;
margin-bottom: 0.7vh;
text-align: center;
}
.swimlane-default-color {
@@ -29,23 +29,23 @@
}
.my-cards-list-title {
font-weight: bold;
font-size: 1.1rem;
font-size: clamp(1rem, 2.5vw, 1.3rem);
text-align: center;
margin-bottom: 0.7rem;
margin-bottom: 0.9vh;
}
.my-cards-list-wrapper {
margin: 1rem;
border-radius: 5px;
margin: 1.3vh 1.3vw;
border-radius: 0.7vw;
display: inline-grid;
min-width: 250px;
max-width: 350px;
min-width: min(250px, 32vw);
max-width: min(350px, 45vw);
}
.my-cards-card-wrapper {
margin-top: 0;
margin-bottom: 10px;
margin-bottom: 1.3vh;
}
.my-cards-dueat-list-wrapper {
max-width: 500px;
max-width: min(500px, 65vw);
margin-right: auto;
margin-left: auto;
}
+5 -5
View File
@@ -1,15 +1,15 @@
.broken-cards-card-wrapper {
margin-top: 0;
margin-bottom: 10px;
border-width: 3px !important;
margin-bottom: 1.3vh;
border-width: 0.4vw !important;
border-color: #808080 !important;
border-style: solid;
border-radius: 5px;
padding: 1.5rem;
border-radius: 0.7vw;
padding: 2vh 2vw;
background-color: #fff;
}
.broken-cards-wrapper {
max-width: 500px;
max-width: min(500px, 65vw);
margin-right: auto;
margin-left: auto;
}
+1 -1
View File
@@ -1,5 +1,5 @@
.due-cards-dueat-list-wrapper {
max-width: 500px;
max-width: min(500px, 65vw);
margin-right: auto;
margin-left: auto;
}
+3 -6
View File
@@ -325,6 +325,7 @@ BlazeComponent.extendComponent({
}).register('editor');
import DOMPurify from 'dompurify';
import { sanitizeHTML } from '/imports/lib/secureDOMPurify';
// Additional safeAttrValue function to allow for other specific protocols
// See https://github.com/leizongmin/js-xss/issues/52#issuecomment-241354114
@@ -371,9 +372,7 @@ Blaze.Template.registerHelper(
let content = Blaze.toHTML(view.templateContentBlock);
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard)
return HTML.Raw(
DOMPurify.sanitize(content, { ALLOW_UNKNOWN_PROTOCOLS: true }),
);
return HTML.Raw(sanitizeHTML(content));
const knowedUsers = _.union(currentBoard.members.map(member => {
const u = ReactiveCache.getUser(member.userId);
if (u) {
@@ -417,9 +416,7 @@ Blaze.Template.registerHelper(
content = content.replace(fullMention, Blaze.toHTML(link));
}
return HTML.Raw(
DOMPurify.sanitize(content, { ALLOW_UNKNOWN_PROTOCOLS: true }),
);
return HTML.Raw(sanitizeHTML(content));
}),
);
+864 -6
View File
@@ -99,7 +99,36 @@
display: flex;
z-index: 1000;
padding: 10px 0px;
align-items: center;
flex-wrap: wrap; /* Allow wrapping on mobile */
min-height: 28px; /* Allow height to grow */
}
#header-quick-access .home-icon {
display: flex;
align-items: center;
margin-right: 1rem;
flex-shrink: 0;
}
#header-quick-access .home-icon a {
display: flex;
align-items: center;
text-decoration: none;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s ease;
}
#header-quick-access .home-icon a:hover {
background-color: rgba(255, 255, 255, 0.1);
}
#header-quick-access .home-icon .fa-home {
font-size: 16px;
margin-right: 4px;
}
#header-quick-access .allBoards {
font-size: 14px;
padding: 4px 15px;
@@ -196,6 +225,197 @@
margin: 4px 8px 0 0;
float: left;
}
/* Zoom Controls in Header */
#header-quick-access .zoom-controls {
display: flex;
align-items: center;
gap: 0.5vw;
background: rgba(255, 255, 255, 0.9);
padding: 0.5vh 1vw;
border-radius: 0.5vw;
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
margin: 0 1vw;
float: left;
}
#header-quick-access .zoom-controls .board-header-btn {
padding: 0.5vh 0.8vw !important;
border-radius: 0.3vw !important;
background: #fff !important;
border: 1px solid #000 !important;
transition: all 0.2s ease !important;
color: #000 !important;
height: auto !important;
line-height: normal !important;
margin: 0 !important;
float: none !important;
overflow: visible !important;
text-decoration: none !important;
display: flex !important;
align-items: center !important;
gap: 0.3vw !important;
}
#header-quick-access .zoom-controls .board-header-btn i {
color: #000 !important;
float: none !important;
display: inline !important;
line-height: normal !important;
margin: 0 !important;
}
#header-quick-access .zoom-controls .board-header-btn:hover {
background: #000 !important;
border-color: #000 !important;
color: #fff !important;
}
#header-quick-access .zoom-controls .board-header-btn:hover i {
color: #fff !important;
}
#header-quick-access .zoom-controls .zoom-level {
font-weight: bold;
color: #333;
min-width: 3vw;
text-align: center;
font-size: clamp(12px, 2vw, 14px);
cursor: pointer;
padding: 0.3vh 0.5vw;
border-radius: 0.3vw;
transition: all 0.2s ease;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
#header-quick-access .zoom-controls .zoom-level:hover {
background: #f0f0f0;
color: #000;
}
#header-quick-access .zoom-controls .zoom-display {
display: inline-block;
}
#header-quick-access .zoom-controls .zoom-input {
background: #fff;
color: #000;
border: 1px solid #ccc;
border-radius: 0.3vw;
padding: 0.3vh 0.5vw;
font-weight: bold;
text-align: center;
width: 100%;
min-width: 3vw;
font-size: clamp(12px, 2vw, 14px);
}
/* Make zoom input wider on all mobile screens */
@media screen and (max-width: 800px) {
#header-quick-access .zoom-controls .zoom-input {
min-width: 50px !important; /* Wider on mobile */
width: 50px !important; /* Fixed width to show all numbers */
font-size: 14px !important; /* Slightly larger text */
}
}
#header-quick-access .zoom-controls .zoom-input:focus {
outline: 2px solid #005fcc;
outline-offset: 1px;
}
/* Mobile Mode Toggle in Header */
#header-quick-access .mobile-mode-toggle {
display: flex;
align-items: center;
margin: 0 1vw;
float: left;
}
#header-quick-access .mobile-mode-toggle .board-header-btn {
padding: 0.5vh 0.8vw !important;
border-radius: 0.3vw !important;
background: #fff !important;
border: 1px solid #000 !important;
transition: all 0.2s ease !important;
color: #000 !important;
height: auto !important;
line-height: normal !important;
margin: 0 !important;
float: none !important;
overflow: visible !important;
text-decoration: none !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
position: relative !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn i {
color: #666 !important;
float: none !important;
display: inline !important;
line-height: normal !important;
margin: 0 !important;
transition: all 0.2s ease !important;
font-size: clamp(14px, 2.8vw, 18px) !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn i.active {
color: #000 !important;
font-weight: bold !important;
transform: scale(1.1) !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn:hover {
background: #000 !important;
border-color: #000 !important;
color: #fff !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn:hover i {
color: #ccc !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn:hover i.active {
color: #fff !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn.mobile-active {
background: #fff !important;
border-color: #000 !important;
color: #000 !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn.mobile-active i.mobile-icon {
color: #000 !important;
font-weight: bold !important;
transform: scale(1.1) !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn.mobile-active i.desktop-icon {
color: #666 !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn.desktop-active {
background: #fff !important;
border-color: #000 !important;
color: #000 !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn.desktop-active i.mobile-icon {
color: #666 !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn.desktop-active i.desktop-icon {
color: #000 !important;
font-weight: bold !important;
transform: scale(1.1) !important;
}
#header-quick-access #header-user-bar .header-user-bar-name i.fa-chevron-down {
margin-right: 4px;
}
@@ -227,6 +447,650 @@
width: 100%;
z-index: 30;
}
/* Mobile home icon styling */
#header-quick-access .home-icon {
margin-right: 0.5rem;
}
#header-quick-access .home-icon .fa-home {
font-size: 16px;
margin-right: 4px;
}
#header-quick-access .home-icon a {
padding: 4px 8px;
font-size: 12px;
}
/* Ensure All Boards text is visible on mobile */
#header-quick-access .home-icon.allBoards {
display: flex;
align-items: center;
}
/* Adjust for very small screens */
@media screen and (max-width: 480px) {
#header-quick-access .home-icon a {
font-size: 11px;
padding: 3px 6px;
}
#header-quick-access .home-icon .fa-home {
font-size: 14px;
margin-right: 3px;
}
}
/* Mobile - make all text and icons 2x bigger above #content by default */
@media screen and (max-width: 800px),
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
#header-quick-access {
height: auto !important; /* Allow height to grow */
min-height: 48px !important; /* Minimum height for mobile */
flex-wrap: wrap !important; /* Force wrapping */
align-items: flex-start !important; /* Align to top when wrapping */
padding: 8px 0px !important; /* Adjust padding for mobile */
}
#header-quick-access {
font-size: 2em !important; /* 2x bigger base font size */
}
#header-quick-access * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
#header-quick-access .fa,
#header-quick-access .icon {
font-size: 2em !important; /* 2x bigger icons */
}
#header-quick-access .home-icon a {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .home-icon .fa-home {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .zoom-controls {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .zoom-controls .zoom-level {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .zoom-controls .zoom-input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .mobile-mode-toggle .board-header-btn {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .mobile-mode-toggle .board-header-btn i {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* Mobile header wrapping and spacing */
#header-quick-access .home-icon {
flex-shrink: 0 !important;
margin-right: 0.5rem !important;
margin-bottom: 4px !important;
}
#header-quick-access .zoom-controls {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
#header-quick-access .mobile-mode-toggle {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
#header-quick-access #notifications {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
#header-quick-access #header-user-bar {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
#header-quick-access ul.header-quick-access-list {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
width: auto !important;
}
}
/* Mobile All Boards page - make logo row elements 2x bigger */
@media screen and (max-width: 800px),
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
.wrapper ~ #header-quick-access,
body:not(.board-view) #header-quick-access {
font-size: 2em !important; /* 2x bigger base font size for logo row */
}
/* iPhone 12 Mini specific - 3x bigger for All Boards page */
@media screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
screen and (max-width: 375px) and (max-height: 812px), /* iPhone 12 Mini viewport */
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 375px) /* iPhone 12 Mini Retina */ {
.wrapper ~ #header-quick-access,
body:not(.board-view) #header-quick-access {
font-size: 3em !important; /* 3x bigger base font size for iPhone 12 Mini All Boards page */
}
}
.wrapper ~ #header-quick-access *,
body:not(.board-view) #header-quick-access * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
.wrapper ~ #header-quick-access .fa,
.wrapper ~ #header-quick-access .icon,
body:not(.board-view) #header-quick-access .fa,
body:not(.board-view) #header-quick-access .icon {
font-size: 2em !important; /* 2x bigger icons in logo row */
}
.wrapper ~ #header-quick-access .home-icon a,
body:not(.board-view) #header-quick-access .home-icon a {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access .home-icon .fa-home,
body:not(.board-view) #header-quick-access .home-icon .fa-home {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access .zoom-controls,
body:not(.board-view) #header-quick-access .zoom-controls {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access .zoom-controls .zoom-level,
body:not(.board-view) #header-quick-access .zoom-controls .zoom-level {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access .zoom-controls .zoom-input,
body:not(.board-view) #header-quick-access .zoom-controls .zoom-input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn,
body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn i,
body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn i {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access #notifications,
body:not(.board-view) #header-quick-access #notifications {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access #notifications .fa,
body:not(.board-view) #header-quick-access #notifications .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access #header-user-bar,
body:not(.board-view) #header-quick-access #header-user-bar {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.wrapper ~ #header-quick-access #header-user-bar .fa,
body:not(.board-view) #header-quick-access #header-user-bar .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
}
/* iPhone 12 Mini specific - make header elements 3x bigger */
@media screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
screen and (max-width: 375px) and (max-height: 812px), /* iPhone 12 Mini viewport */
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 375px), /* iPhone 12 Mini Retina */
screen and (max-width: 375px) and (orientation: portrait), /* iPhone 12 Mini portrait */
screen and (max-width: 375px) and (orientation: landscape) /* iPhone 12 Mini landscape */ {
#header-quick-access {
font-size: 3em !important; /* 3x bigger base font size for iPhone 12 Mini */
height: auto !important; /* Allow height to grow */
min-height: 84px !important; /* Much taller minimum height for iPhone 12 Mini */
flex-wrap: wrap !important; /* Force wrapping */
align-items: flex-start !important; /* Align to top when wrapping */
padding: 18px 0px !important; /* More padding for iPhone 12 Mini */
}
#header-quick-access * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
#header-quick-access .fa,
#header-quick-access .icon {
font-size: 3em !important; /* 3x bigger icons for iPhone 12 Mini */
}
#header-quick-access .home-icon a {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .home-icon .fa-home {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .zoom-controls {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .zoom-controls .zoom-level {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .zoom-controls .zoom-input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .mobile-mode-toggle .board-header-btn {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access .mobile-mode-toggle .board-header-btn i {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access #notifications {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access #notifications .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access #header-user-bar {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access #header-user-bar .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* iPhone 12 Mini header wrapping and spacing */
#header-quick-access .home-icon {
flex-shrink: 0 !important;
margin-right: 0.5rem !important;
margin-bottom: 6px !important;
}
#header-quick-access .zoom-controls {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
#header-quick-access .mobile-mode-toggle {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
#header-quick-access #notifications {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
#header-quick-access #header-user-bar {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
#header-quick-access ul.header-quick-access-list {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
width: auto !important;
}
}
/* iPhone 12 Mini and very small screens - make header elements much larger */
@media screen and (max-width: 400px) and (max-height: 900px),
screen and (max-device-width: 400px) and (max-device-height: 900px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 400px),
screen and (max-width: 400px) and (orientation: portrait),
screen and (max-width: 400px) and (orientation: landscape),
screen and (max-width: 430px) and (max-height: 950px), /* iPhone 12 Mini range */
screen and (max-width: 450px) and (max-height: 1000px), /* iPhone range */
screen and (-webkit-min-device-pixel-ratio: 3) and (max-width: 450px), /* Retina displays */
screen and (device-width: 375px) and (device-height: 812px), /* iPhone 12 Mini exact */
screen and (device-width: 390px) and (device-height: 844px), /* iPhone 12/13 */
screen and (device-width: 428px) and (device-height: 926px) /* iPhone 12 Pro Max */ {
#header-quick-access {
height: 40px !important; /* Taller header */
padding: 12px 0px !important;
}
#header-quick-access .home-icon a {
font-size: 16px !important; /* Much larger text */
padding: 8px 12px !important;
}
#header-quick-access .home-icon .fa-home {
font-size: 20px !important; /* Much larger icon */
margin-right: 6px !important;
}
#header-quick-access .home-icon {
margin-right: 1rem !important;
}
/* Make zoom controls larger */
#header-quick-access .zoom-controls {
padding: 0.8vh 1.5vw !important;
margin: 0 1.5vw !important;
}
#header-quick-access .zoom-controls .zoom-level {
font-size: 16px !important; /* Larger zoom text */
padding: 0.5vh 0.8vw !important;
min-width: 4vw !important;
}
#header-quick-access .zoom-controls .zoom-input {
font-size: 16px !important; /* Larger input text */
padding: 0.5vh 0.8vw !important;
min-width: 6vw !important; /* Much wider for mobile */
width: 60px !important; /* Fixed width to show all numbers */
}
/* Make mobile mode toggle larger */
#header-quick-access .mobile-mode-toggle .board-header-btn {
padding: 0.8vh 1.2vw !important;
font-size: 16px !important;
}
#header-quick-access .mobile-mode-toggle .board-header-btn i {
font-size: 18px !important;
}
}
/* Fallback for iPhone devices using JavaScript detection */
.iphone-device #header-quick-access {
font-size: 2em !important; /* 2x bigger base font size */
height: auto !important; /* Allow height to grow */
min-height: 48px !important; /* Minimum height for mobile */
flex-wrap: wrap !important; /* Force wrapping */
align-items: flex-start !important; /* Align to top when wrapping */
padding: 8px 0px !important; /* Adjust padding for mobile */
}
.iphone-device #header-quick-access * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
.iphone-device #header-quick-access .fa,
.iphone-device #header-quick-access .icon {
font-size: 2em !important; /* 2x bigger icons */
}
.iphone-device #header-quick-access .home-icon a {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .home-icon .fa-home {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .zoom-controls {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .zoom-controls .zoom-level {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .zoom-controls .zoom-input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn i {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* iPhone device header wrapping and spacing */
.iphone-device #header-quick-access .home-icon {
flex-shrink: 0 !important;
margin-right: 0.5rem !important;
margin-bottom: 4px !important;
}
.iphone-device #header-quick-access .zoom-controls {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
.iphone-device #header-quick-access .mobile-mode-toggle {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
.iphone-device #header-quick-access #notifications {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
.iphone-device #header-quick-access #header-user-bar {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
}
.iphone-device #header-quick-access ul.header-quick-access-list {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 4px !important;
width: auto !important;
}
/* iPhone 12 Mini specific - JavaScript detection fallback */
.iphone-device #header-quick-access {
font-size: 3em !important; /* 3x bigger base font size for iPhone 12 Mini */
height: auto !important; /* Allow height to grow */
min-height: 84px !important; /* Much taller minimum height for iPhone 12 Mini */
flex-wrap: wrap !important; /* Force wrapping */
align-items: flex-start !important; /* Align to top when wrapping */
padding: 18px 0px !important; /* More padding for iPhone 12 Mini */
}
.iphone-device #header-quick-access * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
.iphone-device #header-quick-access .fa,
.iphone-device #header-quick-access .icon {
font-size: 3em !important; /* 3x bigger icons for iPhone 12 Mini */
}
.iphone-device #header-quick-access .home-icon a {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .home-icon .fa-home {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .zoom-controls {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .zoom-controls .zoom-level {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .zoom-controls .zoom-input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access .mobile-mode-toggle .board-header-btn i {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access #notifications {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access #notifications .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access #header-user-bar {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device #header-quick-access #header-user-bar .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* iPhone 12 Mini header wrapping and spacing - JavaScript fallback */
.iphone-device #header-quick-access .home-icon {
flex-shrink: 0 !important;
margin-right: 0.5rem !important;
margin-bottom: 6px !important;
}
.iphone-device #header-quick-access .zoom-controls {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
.iphone-device #header-quick-access .mobile-mode-toggle {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
.iphone-device #header-quick-access #notifications {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
.iphone-device #header-quick-access #header-user-bar {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
}
.iphone-device #header-quick-access ul.header-quick-access-list {
flex-shrink: 0 !important;
margin: 0 0.5rem !important;
margin-bottom: 6px !important;
width: auto !important;
}
/* iPhone 12 Mini All Boards page - make logo row elements 3x bigger */
.iphone-device .wrapper ~ #header-quick-access,
.iphone-device body:not(.board-view) #header-quick-access {
font-size: 3em !important; /* 3x bigger base font size for logo row */
}
.iphone-device .wrapper ~ #header-quick-access *,
.iphone-device body:not(.board-view) #header-quick-access * {
font-size: inherit !important; /* Inherit the 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access .fa,
.iphone-device .wrapper ~ #header-quick-access .icon,
.iphone-device body:not(.board-view) #header-quick-access .fa,
.iphone-device body:not(.board-view) #header-quick-access .icon {
font-size: 2em !important; /* 2x bigger icons in logo row */
}
.iphone-device .wrapper ~ #header-quick-access .home-icon a,
.iphone-device body:not(.board-view) #header-quick-access .home-icon a {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access .home-icon .fa-home,
.iphone-device body:not(.board-view) #header-quick-access .home-icon .fa-home {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access .zoom-controls,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-level,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-level {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access .zoom-controls .zoom-input,
.iphone-device body:not(.board-view) #header-quick-access .zoom-controls .zoom-input {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn,
.iphone-device body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access .mobile-mode-toggle .board-header-btn i,
.iphone-device body:not(.board-view) #header-quick-access .mobile-mode-toggle .board-header-btn i {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access #notifications,
.iphone-device body:not(.board-view) #header-quick-access #notifications {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access #notifications .fa,
.iphone-device body:not(.board-view) #header-quick-access #notifications .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access #header-user-bar,
.iphone-device body:not(.board-view) #header-quick-access #header-user-bar {
font-size: 1em !important; /* Use inherited 2x scaling */
}
.iphone-device .wrapper ~ #header-quick-access #header-user-bar .fa,
.iphone-device body:not(.board-view) #header-quick-access #header-user-bar .fa {
font-size: 1em !important; /* Use inherited 2x scaling */
}
#header-quick-access ul {
width: calc(100% - 60px);
margin-right: 10px;
@@ -237,12 +1101,6 @@
#header-quick-access ul li a {
height: 100%;
}
#header-quick-access span .fa-home {
font-size: 26px;
margin-top: -2px;
margin-right: 10px;
margin-left: 10px;
}
#header-quick-access #header-new-board-icon {
display: none;
}
+28 -21
View File
@@ -6,10 +6,31 @@ template(name="header")
The first link goes to the boards page.
if currentUser
#header-quick-access(class=currentBoard.colorClass)
// Home icon - always at left side of logo
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
span.fa.fa-home
| {{_ 'all-boards'}}
// Logo - always visible in desktop mode
unless currentSetting.hideLogo
if currentSetting.customTopLeftCornerLogoImageUrl
if currentSetting.customTopLeftCornerLogoLinkUrl
a(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
unless currentSetting.customTopLeftCornerLogoLinkUrl
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
unless currentSetting.customTopLeftCornerLogoImageUrl
div#headerIsSettingDatabaseCallDone
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
// Zoom controls - always visible
.zoom-controls
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
span.zoom-display {{zoomLevel}}%
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
if isMiniScreen
span
a(href="{{pathFor 'home'}}")
span.fa.fa-home
ul.header-quick-access-list
if currentList
each currentBoard.lists
@@ -31,24 +52,6 @@ template(name="header")
// i.fa.fa-ban
#header-new-board-icon
else
//-
On sandstorm, the logo shouldn't be clickable, because we only have one
page/document on it, and we don't want to see the home page containing
the list of all boards.
unless currentSetting.hideLogo
if currentSetting.customTopLeftCornerLogoImageUrl
if currentSetting.customTopLeftCornerLogoLinkUrl
a(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
unless currentSetting.customTopLeftCornerLogoLinkUrl
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
unless currentSetting.customTopLeftCornerLogoImageUrl
div#headerIsSettingDatabaseCallDone
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
span.allBoards
a(href="{{pathFor 'home'}}")
span.fa.fa-home
| {{_ 'all-boards'}}
ul.header-quick-access-list
//li
// a(href="{{pathFor 'public'}}")
@@ -75,6 +78,10 @@ template(name="header")
//a#header-new-board-icon.js-create-board
// i.fa.fa-plus(title="Create a new board")
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.fa.fa-mobile.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-desktop.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
+notifications
if currentSetting.customHelpLinkUrl
+56
View File
@@ -44,10 +44,66 @@ Template.header.helpers({
const announcements = Announcements.findOne();
return announcements && announcements.body;
},
zoomLevel() {
const sessionZoom = Session.get('wekan-zoom-level');
if (sessionZoom !== undefined) {
return Math.round(sessionZoom * 100);
}
return Math.round(Utils.getZoomLevel() * 100);
},
mobileMode() {
const sessionMode = Session.get('wekan-mobile-mode');
if (sessionMode !== undefined) {
return sessionMode;
}
return Utils.getMobileMode();
},
});
Template.header.events({
'click .js-create-board': Popup.open('headerBarCreateBoard'),
'click .js-zoom-level-click'(evt) {
const $zoomDisplay = $(evt.currentTarget).find('.zoom-display');
const $zoomInput = $(evt.currentTarget).find('.zoom-input');
// Hide display, show input
$zoomDisplay.hide();
$zoomInput.show().focus().select();
},
'keypress .js-zoom-input'(evt) {
if (evt.which === 13) { // Enter key
const newZoomPercent = parseInt(evt.target.value);
if (!isNaN(newZoomPercent) && newZoomPercent >= 50 && newZoomPercent <= 300) {
const newZoom = newZoomPercent / 100;
Utils.setZoomLevel(newZoom);
// Hide input, show display
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
const $zoomInput = $(evt.target);
$zoomInput.hide();
$zoomDisplay.show();
} else {
alert('Please enter a zoom level between 50% and 300%');
evt.target.focus().select();
}
}
},
'blur .js-zoom-input'(evt) {
// When input loses focus, hide it and show display
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
const $zoomInput = $(evt.target);
$zoomInput.hide();
$zoomDisplay.show();
},
'click .js-mobile-mode-toggle'() {
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'click .js-close-announcement'() {
$('.announcement').hide();
},
+53 -53
View File
@@ -52,8 +52,8 @@ input,
select,
textarea,
button {
font: 14px Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 18px;
font: clamp(12px, 2.5vw, 16px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 1.3;
color: #4d4d4d;
}
html {
@@ -85,8 +85,8 @@ body {
margin-top: 30vh;
}
#content > .wrapper {
margin-top: 10px;
padding: 15px;
margin-top: 1vh;
padding: 2vh 2vw;
}
#modal {
position: absolute;
@@ -99,56 +99,56 @@ body {
overflow-y: auto;
}
#modal .modal-content {
width: 500px;
min-height: 160px;
margin: 42px auto;
padding: 12px;
border-radius: 4px;
width: min(500px, 90vw);
min-height: 20vh;
margin: 5vh auto;
padding: 2vh 2vw;
border-radius: 0.5vw;
background: #dedede;
z-index: 110;
}
#modal .modal-content h2 {
margin-bottom: 25px;
margin-bottom: 3vh;
}
#modal .modal-content .modal-close-btn {
display: block;
float: right;
font-size: 24px;
font-size: clamp(18px, 4vw, 24px);
}
#modal .modal-content-wide {
width: 800px;
width: min(800px, 95vw);
min-height: 0px;
margin: 42px auto;
padding: 12px;
border-radius: 4px;
margin: 5vh auto;
padding: 2vh 2vw;
border-radius: 0.5vw;
background: #dedede;
z-index: 110;
}
#modal .modal-content-wide h2 {
margin-bottom: 25px;
margin-bottom: 3vh;
}
#modal .modal-content-wide .modal-close-btn {
display: block;
float: right;
font-size: 24px;
font-size: clamp(18px, 4vw, 24px);
}
h1 {
font-size: 22px;
font-size: clamp(18px, 4vw, 24px);
line-height: 1.2em;
margin: 0 0 10px;
margin: 0 0 1vh;
}
h2 {
font-size: 18px;
font-size: clamp(16px, 3.5vw, 20px);
line-height: 1.2em;
margin: 0 0 9px;
margin: 0 0 0.8vh;
}
h3,
h4,
h5,
h6 {
font-size: 16px;
font-size: clamp(14px, 3vw, 18px);
line-height: 1.25em;
margin: 0 0 6px;
margin: 0 0 0.6vh;
}
.quiet,
.quiet a {
@@ -160,14 +160,14 @@ h6 {
}
.no-items-message {
color: #666;
margin: 30px 0;
margin: 4vh 0;
text-align: center;
}
.warning {
background: #f0ecdb;
border-radius: 3px;
border-radius: 0.4vw;
color: #aa8f09;
padding: 6px 8px;
padding: 1vh 1.2vw;
}
.warning a {
color: #aa8f09;
@@ -194,7 +194,7 @@ strong {
p {
-webkit-user-select: text;
user-select: text;
}
p a {
@@ -203,12 +203,12 @@ p a {
}
table,
p {
margin-bottom: 8px;
margin-bottom: 1vh;
}
pre {
margin: 15px 0;
margin: 2vh 0;
white-space: pre;
max-height: 516px;
max-height: 60vh;
}
pre,
code,
@@ -217,10 +217,10 @@ tt {
line-height: 1.3em;
}
blockquote {
margin: 8px 0 8px 8px;
margin: 1vh 0 1vh 1vw;
border-left: 1px solid #ccc;
color: #666;
padding: 0 0 0 8px;
padding: 0 0 0 1vw;
}
hr {
height: 1px;
@@ -229,7 +229,7 @@ hr {
width: 100%;
background: #dbdbdb;
color: #dbdbdb;
margin: 15px 0;
margin: 2vh 0;
padding: 0;
}
table,
@@ -241,7 +241,7 @@ th {
}
td,
th {
padding: 5px;
padding: 0.8vh 0.8vw;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
@@ -257,20 +257,20 @@ tbody {
}
dl,
dt {
margin-bottom: 8px;
margin-bottom: 1vh;
}
dd {
margin: 0 0 16px 24px;
margin: 0 0 2vh 3vw;
}
kbd {
padding: 1px 3px;
margin: 3px;
padding: 0.2vh 0.5vw;
margin: 0.5vh;
font-weight: bold;
background: #fafafa;
border-radius: 3px;
border-radius: 0.4vw;
border: 1px solid #e6e6e6;
color: unset;
box-shadow: 0px 2px 3px rgba(0,0,0,0.15);
box-shadow: 0px 0.3vh 0.5vh rgba(0,0,0,0.15);
}
.clear {
clear: both;
@@ -310,7 +310,7 @@ kbd {
padding-bottom: 0;
}
.wrapper {
width: calc(100% - 20px);
width: calc(100% - 2vw);
margin: 0 auto;
}
.relative {
@@ -401,23 +401,23 @@ a:not(.disabled).is-active i.fa {
}
.big-message {
display: block;
margin: 75px auto;
margin: 10vh auto;
text-align: center;
max-width: 600px;
max-width: min(600px, 90vw);
}
.big-message h1 {
font-size: 26px;
margin-bottom: 24px;
font-size: clamp(20px, 5vw, 28px);
margin-bottom: 3vh;
}
.big-message p {
font-size: 18px;
line-height: 22px;
font-size: clamp(16px, 3.5vw, 20px);
line-height: 1.3;
}
.gutter {
margin-left: 38px;
margin-left: 5vw;
}
.viewer {
min-height: 18px;
min-height: 2.5vh;
display: block;
word-wrap: break-word;
}
@@ -427,13 +427,13 @@ a:not(.disabled).is-active i.fa {
}
.viewer ol {
list-style-type: decimal;
padding-left: 20px;
padding-bottom: 10px;
padding-left: 3vw;
padding-bottom: 1.5vh;
}
.viewer ul {
list-style-type: initial;
padding-left: 20px;
padding-bottom: 10px;
padding-left: 3vw;
padding-bottom: 1.5vh;
}
.viewer em {
font-style: italic;
-8
View File
@@ -52,14 +52,6 @@ template(name="userFormsLayout")
else
// ARIA live region for error messages
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")
// Add autocomplete attribute to login input for WCAG compliance
script.
document.addEventListener('DOMContentLoaded', function() {
var loginInput = document.querySelector('input[type="text"], input[type="email"]');
if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
loginInput.setAttribute('autocomplete', 'username email');
}
});
+Template.dynamic(template=content)
if currentSetting.displayAuthenticationMethod
+connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
+6
View File
@@ -86,6 +86,12 @@ Template.userFormsLayout.onRendered(() => {
validator,
);
EscapeActions.executeAll();
// Add autocomplete attribute to login input for WCAG compliance
const loginInput = document.querySelector('input[type="text"], input[type="email"]');
if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
loginInput.setAttribute('autocomplete', 'username email');
}
});
});
+18 -18
View File
@@ -1,27 +1,27 @@
.my-cards-board-wrapper {
border-radius: 0 0 4px 4px;
min-width: 400px;
margin-bottom: 2rem;
border-radius: 0 0 0.5vw 0.5vw;
min-width: min(400px, 52vw);
margin-bottom: 2.5vh;
margin-right: auto;
margin-left: auto;
border-width: 2px;
border-width: 0.3vw;
border-style: solid;
border-color: #a2a2a2;
}
.my-cards-board-title {
font-size: 1.4rem;
font-size: clamp(1.2rem, 3vw, 1.6rem);
font-weight: bold;
padding: 0.5rem;
padding: 0.7vh 0.7vw;
background-color: #808080;
color: #fff;
}
.my-cards-swimlane-title {
font-size: 1.1rem;
font-size: clamp(1rem, 2.5vw, 1.3rem);
font-weight: bold;
padding: 0.5rem;
padding-bottom: 0.4rem;
padding: 0.7vh 0.7vw;
padding-bottom: 0.5vh;
margin-top: 0;
margin-bottom: 0.5rem;
margin-bottom: 0.7vh;
text-align: center;
}
.swimlane-default-color {
@@ -29,23 +29,23 @@
}
.my-cards-list-title {
font-weight: bold;
font-size: 1.1rem;
font-size: clamp(1rem, 2.5vw, 1.3rem);
text-align: center;
margin-bottom: 0.7rem;
margin-bottom: 0.9vh;
}
.my-cards-list-wrapper {
margin: 1rem;
border-radius: 5px;
margin: 1.3vh 1.3vw;
border-radius: 0.7vw;
display: inline-grid;
min-width: 250px;
max-width: 350px;
min-width: min(250px, 32vw);
max-width: min(350px, 45vw);
}
.my-cards-card-wrapper {
margin-top: 0;
margin-bottom: 10px;
margin-bottom: 1.3vh;
}
.my-cards-dueat-list-wrapper {
max-width: 500px;
max-width: min(500px, 65vw);
margin-right: auto;
margin-left: auto;
}
+22 -22
View File
@@ -1,16 +1,16 @@
.pop-over {
background: #fff;
border-radius: 3px;
border-radius: 0.4vw;
border: 1px solid #dbdbdb;
border-bottom-color: #c2c2c2;
box-shadow: 0 1px 6px rgba(0,0,0,0.3);
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
position: absolute;
width: 300px;
width: min(300px, 40vw);
z-index: 99999;
margin-top: 5px;
margin-top: 0.7vh;
}
.pop-over hr {
margin: 4px 0px;
margin: 0.5vh 0px;
}
.pop-over p,
.pop-over textarea,
@@ -22,27 +22,27 @@
}
.pop-over select {
width: 100%;
margin-bottom: 14px;
margin-bottom: 1.8vh;
}
.pop-over textarea {
height: 72px;
height: 9vh;
}
.pop-over form a span {
padding: 0 0.5rem;
padding: 0 0.7vw;
}
.pop-over .header {
height: 36px;
height: 4.5vh;
position: relative;
margin-bottom: 8px;
margin-bottom: 1vh;
background: #f7f7f7;
border-bottom: 1px solid #dcdcdc;
color: #666;
}
.pop-over .header .header-title {
display: block;
line-height: 32px;
padding-top: 4px;
margin: 0 10px;
line-height: 4vh;
padding-top: 0.5vh;
margin: 0 1.3vw;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
@@ -51,18 +51,18 @@
.pop-over .header .back-btn {
float: left;
overflow: hidden;
width: 30px;
width: 4vw;
transition: width 0.2s;
}
.pop-over .header .back-btn i.fa {
margin: 10px;
margin-top: 12px;
margin: 1.3vw;
margin-top: 1.5vh;
}
.pop-over .header .back-btn.is-hidden {
width: 0;
}
.pop-over .header .close-btn {
padding: 10px 10px 10px 4px;
padding: 1.3vh 1.3vw 1.3vh 0.5vw;
position: absolute;
top: 0;
right: 0;
@@ -76,23 +76,23 @@
}
.pop-over .content-container {
width: 5000px;
max-height: 550px;
max-height: 70vh;
transition: transform 0.2s;
}
.pop-over .content-container .content {
width: 280px;
padding: 0 10px 10px;
width: min(280px, 37vw);
padding: 0 1.3vw 1.3vh;
float: left;
}
.pop-over .content-container .content.no-height {
height: 20px;
height: 2.5vh;
}
.pop-over .quiet {
/* padding: 6px 6px 4px;*/
}
.pop-over.search-over {
background: #f0f0f0;
min-height: 114px;
min-height: 14vh;
}
.pop-over.search-over .header {
display: none;
@@ -4,7 +4,7 @@
#notifications-drawer .notification {
display: flex;
float: none;
padding: 15px 8px 8px;
padding: 2vh 1vw 1vh;
color: #000;
border-bottom: 1px solid #dbdbdb;
}
@@ -12,18 +12,18 @@
display: none;
}
#notifications-drawer .notification .read-status {
width: 30px;
padding: 0px 10px 0px 0px;
width: 4vw;
padding: 0 1.3vw 0 0;
}
#notifications-drawer .notification .read-status input {
width: 24px;
height: 24px;
width: 3vw;
height: 3vw;
}
#notifications-drawer .notification .read-status .activity-type {
margin: 16px 0 0;
width: 17px;
height: 17px;
font-size: 17px;
margin: 2vh 0 0;
width: 2.2vw;
height: 2.2vw;
font-size: clamp(14px, 2.5vw, 17px);
display: block;
color: #bbb;
}
@@ -32,20 +32,20 @@
padding: 0px;
}
#notifications-drawer .notification .details .activity a.member svg {
padding: 3px;
padding: 0.4vw;
}
#notifications-drawer .notification .details .activity .activity-desc {
margin: 0px 0px 0px 5px;
margin: 0 0 0 0.7vw;
}
#notifications-drawer .notification .details .activity .activity-comment {
display: block;
width: 100%;
border-radius: 3px;
border-radius: 0.4vw;
background: #fff;
text-decoration: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.2);
margin-top: 0.7vh;
padding: 0.7vh 0.7vw;
}
#notifications-drawer .notification .details .activity .activity-meta {
display: block;
+5 -5
View File
@@ -1,8 +1,8 @@
.rules-list {
overflow: hidden;
overflow-y: scroll;
max-height: 400px;
padding-right: 5px;
max-height: 50vh;
padding-right: 0.7vw;
}
.rules-lists-item {
display: block;
@@ -33,14 +33,14 @@
.rules-add {
display: block;
overflow: auto;
margin-top: 15px;
margin-bottom: 5px;
margin-top: 2vh;
margin-bottom: 0.7vh;
}
.rules-add input {
display: inline-block;
float: right;
margin: auto;
margin-right: 10px;
margin-right: 1.3vw;
}
.rules-add button {
display: inline-block;
+1 -1
View File
@@ -112,7 +112,7 @@ class AdminReport extends BlazeComponent {
}
resultsCount() {
return this.collection.find().count();
return this.collection.find().countDocuments();
}
fileSize(size) {
@@ -0,0 +1,200 @@
template(name="attachmentSettings")
.setting-content.attachment-settings-content
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-body
.side-menu
ul
li
a.js-attachment-storage-settings(data-id="storage-settings")
i.fa.fa-cog
| {{_ 'attachment-storage-settings'}}
li
a.js-attachment-migration(data-id="attachment-migration")
i.fa.fa-arrow-right
| {{_ 'attachment-migration'}}
li
a.js-attachment-monitoring(data-id="attachment-monitoring")
i.fa.fa-chart-line
| {{_ 'attachment-monitoring'}}
.main-body
if loading.get
+spinner
else if showStorageSettings.get
+storageSettings
else if showMigration.get
+attachmentMigration
else if showMonitoring.get
+attachmentMonitoring
template(name="storageSettings")
.storage-settings
h3 {{_ 'attachment-storage-configuration'}}
.storage-config-section
h4 {{_ 'filesystem-storage'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
.storage-config-section
h4 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
.storage-config-section
h4 {{_ 's3-minio-storage'}}
.form-group
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.storage-actions
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
template(name="attachmentMigration")
.attachment-migration
h3 {{_ 'attachment-migration'}}
.migration-controls
.form-group
label {{_ 'migration-batch-size'}}
input.wekan-form-control#migration-batch-size(type="number" value="{{migrationBatchSize}}" min="1" max="100")
small.form-text.text-muted {{_ 'migration-batch-size-description'}}
.form-group
label {{_ 'migration-delay-ms'}}
input.wekan-form-control#migration-delay-ms(type="number" value="{{migrationDelayMs}}" min="100" max="10000")
small.form-text.text-muted {{_ 'migration-delay-ms-description'}}
.form-group
label {{_ 'migration-cpu-threshold'}}
input.wekan-form-control#migration-cpu-threshold(type="number" value="{{migrationCpuThreshold}}" min="10" max="90")
small.form-text.text-muted {{_ 'migration-cpu-threshold-description'}}
.migration-actions
.migration-buttons
button.js-migrate-all-to-filesystem.btn.btn-primary {{_ 'migrate-all-to-filesystem'}}
button.js-migrate-all-to-gridfs.btn.btn-primary {{_ 'migrate-all-to-gridfs'}}
button.js-migrate-all-to-s3.btn.btn-primary {{_ 'migrate-all-to-s3'}}
.migration-controls
button.js-pause-migration.btn.btn-warning {{_ 'pause-migration'}}
button.js-resume-migration.btn.btn-success {{_ 'resume-migration'}}
button.js-stop-migration.btn.btn-danger {{_ 'stop-migration'}}
.migration-progress
h4 {{_ 'migration-progress'}}
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.migration-stats
.stat-item
span.label {{_ 'total-attachments'}}:
span.value {{totalAttachments}}
.stat-item
span.label {{_ 'migrated-attachments'}}:
span.value {{migratedAttachments}}
.stat-item
span.label {{_ 'remaining-attachments'}}:
span.value {{remainingAttachments}}
.stat-item
span.label {{_ 'migration-status'}}:
span.value {{migrationStatus}}
.migration-log
h4 {{_ 'migration-log'}}
.log-container
pre#migration-log-content {{migrationLog}}
template(name="attachmentMonitoring")
.attachment-monitoring
h3 {{_ 'attachment-monitoring'}}
.monitoring-stats
.stats-grid
.stat-card
h5 {{_ 'total-attachments'}}
.stat-value {{totalAttachments}}
.stat-card
h5 {{_ 'filesystem-attachments'}}
.stat-value {{filesystemAttachments}}
.stat-card
h5 {{_ 'gridfs-attachments'}}
.stat-value {{gridfsAttachments}}
.stat-card
h5 {{_ 's3-attachments'}}
.stat-value {{s3Attachments}}
.stat-card
h5 {{_ 'total-size'}}
.stat-value {{totalSize}}
.stat-card
h5 {{_ 'filesystem-size'}}
.stat-value {{filesystemSize}}
.stat-card
h5 {{_ 'gridfs-size'}}
.stat-value {{gridfsSize}}
.stat-card
h5 {{_ 's3-size'}}
.stat-value {{s3Size}}
.monitoring-charts
h4 {{_ 'storage-distribution'}}
.chart-container
canvas#storage-distribution-chart
.monitoring-actions
button.js-refresh-monitoring.btn.btn-secondary {{_ 'refresh-monitoring'}}
button.js-export-monitoring.btn.btn-primary {{_ 'export-monitoring'}}
@@ -0,0 +1,464 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { Chart } from 'chart.js';
// Global reactive variables for attachment settings
const attachmentSettings = {
loading: new ReactiveVar(false),
showStorageSettings: new ReactiveVar(false),
showMigration: new ReactiveVar(false),
showMonitoring: new ReactiveVar(false),
// Storage configuration
filesystemPath: new ReactiveVar(''),
attachmentsPath: new ReactiveVar(''),
avatarsPath: new ReactiveVar(''),
gridfsEnabled: new ReactiveVar(false),
s3Enabled: new ReactiveVar(false),
s3Endpoint: new ReactiveVar(''),
s3Bucket: new ReactiveVar(''),
s3Region: new ReactiveVar(''),
s3SslEnabled: new ReactiveVar(false),
s3Port: new ReactiveVar(443),
// Migration settings
migrationBatchSize: new ReactiveVar(10),
migrationDelayMs: new ReactiveVar(1000),
migrationCpuThreshold: new ReactiveVar(70),
migrationProgress: new ReactiveVar(0),
migrationStatus: new ReactiveVar('idle'),
migrationLog: new ReactiveVar(''),
// Monitoring data
totalAttachments: new ReactiveVar(0),
filesystemAttachments: new ReactiveVar(0),
gridfsAttachments: new ReactiveVar(0),
s3Attachments: new ReactiveVar(0),
totalSize: new ReactiveVar(0),
filesystemSize: new ReactiveVar(0),
gridfsSize: new ReactiveVar(0),
s3Size: new ReactiveVar(0),
// Migration state
isMigrationRunning: new ReactiveVar(false),
isMigrationPaused: new ReactiveVar(false),
migrationQueue: new ReactiveVar([]),
currentMigration: new ReactiveVar(null)
};
// Main attachment settings component
BlazeComponent.extendComponent({
onCreated() {
this.loading = attachmentSettings.loading;
this.showStorageSettings = attachmentSettings.showStorageSettings;
this.showMigration = attachmentSettings.showMigration;
this.showMonitoring = attachmentSettings.showMonitoring;
// Load initial data
this.loadStorageConfiguration();
this.loadMigrationSettings();
this.loadMonitoringData();
},
events() {
return [
{
'click a.js-attachment-storage-settings': this.switchToStorageSettings,
'click a.js-attachment-migration': this.switchToMigration,
'click a.js-attachment-monitoring': this.switchToMonitoring,
}
];
},
switchToStorageSettings(event) {
this.switchMenu(event, 'storage-settings');
this.showStorageSettings.set(true);
this.showMigration.set(false);
this.showMonitoring.set(false);
},
switchToMigration(event) {
this.switchMenu(event, 'attachment-migration');
this.showStorageSettings.set(false);
this.showMigration.set(true);
this.showMonitoring.set(false);
},
switchToMonitoring(event) {
this.switchMenu(event, 'attachment-monitoring');
this.showStorageSettings.set(false);
this.showMigration.set(false);
this.showMonitoring.set(true);
},
switchMenu(event, targetId) {
const target = $(event.target);
if (!target.hasClass('active')) {
this.loading.set(true);
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
// Load data based on target
if (targetId === 'storage-settings') {
this.loadStorageConfiguration();
} else if (targetId === 'attachment-migration') {
this.loadMigrationSettings();
} else if (targetId === 'attachment-monitoring') {
this.loadMonitoringData();
}
this.loading.set(false);
}
},
loadStorageConfiguration() {
Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
if (!error && result) {
attachmentSettings.filesystemPath.set(result.filesystemPath || '');
attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
attachmentSettings.avatarsPath.set(result.avatarsPath || '');
attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
attachmentSettings.s3Enabled.set(result.s3Enabled || false);
attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
attachmentSettings.s3Bucket.set(result.s3Bucket || '');
attachmentSettings.s3Region.set(result.s3Region || '');
attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
attachmentSettings.s3Port.set(result.s3Port || 443);
}
});
},
loadMigrationSettings() {
Meteor.call('getAttachmentMigrationSettings', (error, result) => {
if (!error && result) {
attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
attachmentSettings.migrationStatus.set(result.status || 'idle');
attachmentSettings.migrationProgress.set(result.progress || 0);
}
});
},
loadMonitoringData() {
Meteor.call('getAttachmentMonitoringData', (error, result) => {
if (!error && result) {
attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
attachmentSettings.totalSize.set(result.totalSize || 0);
attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
attachmentSettings.s3Size.set(result.s3Size || 0);
}
});
}
}).register('attachmentSettings');
// Storage settings component
BlazeComponent.extendComponent({
onCreated() {
this.filesystemPath = attachmentSettings.filesystemPath;
this.attachmentsPath = attachmentSettings.attachmentsPath;
this.avatarsPath = attachmentSettings.avatarsPath;
this.gridfsEnabled = attachmentSettings.gridfsEnabled;
this.s3Enabled = attachmentSettings.s3Enabled;
this.s3Endpoint = attachmentSettings.s3Endpoint;
this.s3Bucket = attachmentSettings.s3Bucket;
this.s3Region = attachmentSettings.s3Region;
this.s3SslEnabled = attachmentSettings.s3SslEnabled;
this.s3Port = attachmentSettings.s3Port;
},
events() {
return [
{
'click button.js-test-s3-connection': this.testS3Connection,
'click button.js-save-s3-settings': this.saveS3Settings,
'change input#s3-secret-key': this.updateS3SecretKey
}
];
},
testS3Connection() {
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-connection-success'));
}
});
},
saveS3Settings() {
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-settings-saved'));
$('#s3-secret-key').val(''); // Clear the password field
}
});
},
updateS3SecretKey(event) {
// This method can be used to validate the secret key format
const secretKey = event.target.value;
// Add validation logic here if needed
}
}).register('storageSettings');
// Migration component
BlazeComponent.extendComponent({
onCreated() {
this.migrationBatchSize = attachmentSettings.migrationBatchSize;
this.migrationDelayMs = attachmentSettings.migrationDelayMs;
this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
this.migrationProgress = attachmentSettings.migrationProgress;
this.migrationStatus = attachmentSettings.migrationStatus;
this.migrationLog = attachmentSettings.migrationLog;
this.isMigrationRunning = attachmentSettings.isMigrationRunning;
this.isMigrationPaused = attachmentSettings.isMigrationPaused;
// Subscribe to migration updates
this.subscription = Meteor.subscribe('attachmentMigrationStatus');
// Set up reactive updates
this.autorun(() => {
const status = attachmentSettings.migrationStatus.get();
if (status === 'running') {
this.isMigrationRunning.set(true);
} else {
this.isMigrationRunning.set(false);
}
});
},
onDestroyed() {
if (this.subscription) {
this.subscription.stop();
}
},
events() {
return [
{
'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
'click button.js-pause-migration': this.pauseMigration,
'click button.js-resume-migration': this.resumeMigration,
'click button.js-stop-migration': this.stopMigration,
'change input#migration-batch-size': this.updateBatchSize,
'change input#migration-delay-ms': this.updateDelayMs,
'change input#migration-cpu-threshold': this.updateCpuThreshold
}
];
},
startMigration(targetStorage) {
const batchSize = parseInt($('#migration-batch-size').val()) || 10;
const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
Meteor.call('startAttachmentMigration', {
targetStorage,
batchSize,
delayMs,
cpuThreshold
}, (error, result) => {
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
}
});
},
pauseMigration() {
Meteor.call('pauseAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-paused'));
}
});
},
resumeMigration() {
Meteor.call('resumeAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-resumed'));
}
});
},
stopMigration() {
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
Meteor.call('stopAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-stopped'));
}
});
}
},
updateBatchSize(event) {
const value = parseInt(event.target.value);
if (value >= 1 && value <= 100) {
attachmentSettings.migrationBatchSize.set(value);
}
},
updateDelayMs(event) {
const value = parseInt(event.target.value);
if (value >= 100 && value <= 10000) {
attachmentSettings.migrationDelayMs.set(value);
}
},
updateCpuThreshold(event) {
const value = parseInt(event.target.value);
if (value >= 10 && value <= 90) {
attachmentSettings.migrationCpuThreshold.set(value);
}
},
addToLog(message) {
const timestamp = new Date().toISOString();
const currentLog = attachmentSettings.migrationLog.get();
const newLog = `[${timestamp}] ${message}\n${currentLog}`;
attachmentSettings.migrationLog.set(newLog);
}
}).register('attachmentMigration');
// Monitoring component
BlazeComponent.extendComponent({
onCreated() {
this.totalAttachments = attachmentSettings.totalAttachments;
this.filesystemAttachments = attachmentSettings.filesystemAttachments;
this.gridfsAttachments = attachmentSettings.gridfsAttachments;
this.s3Attachments = attachmentSettings.s3Attachments;
this.totalSize = attachmentSettings.totalSize;
this.filesystemSize = attachmentSettings.filesystemSize;
this.gridfsSize = attachmentSettings.gridfsSize;
this.s3Size = attachmentSettings.s3Size;
// Subscribe to monitoring updates
this.subscription = Meteor.subscribe('attachmentMonitoringData');
// Set up chart
this.autorun(() => {
this.updateChart();
});
},
onDestroyed() {
if (this.subscription) {
this.subscription.stop();
}
},
events() {
return [
{
'click button.js-refresh-monitoring': this.refreshMonitoring,
'click button.js-export-monitoring': this.exportMonitoring
}
];
},
refreshMonitoring() {
Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
if (error) {
alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
}
});
},
exportMonitoring() {
Meteor.call('exportAttachmentMonitoringData', (error, result) => {
if (error) {
alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
} else {
// Download the exported data
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'wekan-attachment-monitoring.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
},
updateChart() {
const ctx = document.getElementById('storage-distribution-chart');
if (!ctx) return;
const filesystemCount = this.filesystemAttachments.get();
const gridfsCount = this.gridfsAttachments.get();
const s3Count = this.s3Attachments.get();
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: [
TAPi18n.__('filesystem-storage'),
TAPi18n.__('gridfs-storage'),
TAPi18n.__('s3-storage')
],
datasets: [{
data: [filesystemCount, gridfsCount, s3Count],
backgroundColor: [
'#28a745',
'#007bff',
'#ffc107'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
}).register('attachmentMonitoring');
// Export the attachment settings for use in other components
export { attachmentSettings };
+33 -17
View File
@@ -203,26 +203,35 @@ BlazeComponent.extendComponent({
this.loading.set(w);
},
orgList() {
const limitOrgs = this.page.get() * orgsPerPage;
const orgs = ReactiveCache.getOrgs(this.findOrgsOptions.get(), {
sort: { orgDisplayName: 1 },
limit: limitOrgs,
fields: { _id: true },
});
// Count only the items currently loaded to browser, not total from database
this.numberOrgs.set(orgs.length);
return orgs;
},
teamList() {
const limitTeams = this.page.get() * teamsPerPage;
const teams = ReactiveCache.getTeams(this.findTeamsOptions.get(), {
sort: { teamDisplayName: 1 },
limit: limitTeams,
fields: { _id: true },
});
// Count only the items currently loaded to browser, not total from database
this.numberTeams.set(teams.length);
return teams;
},
peopleList() {
const limitUsers = this.page.get() * usersPerPage;
const users = ReactiveCache.getUsers(this.findUsersOptions.get(), {
sort: { username: 1 },
limit: limitUsers,
fields: { _id: true },
});
// Count only the items currently loaded to browser, not total from database
this.numberPeople.set(users.length);
return users;
},
@@ -1269,23 +1278,30 @@ Template.settingsUserPopup.events({
},
'click #deleteButton'(event) {
event.preventDefault();
Users.remove(this.userId);
/*
// Delete user is enabled, but you should remove user from all boards
// before deleting user, because there is possibility of leaving empty user avatars
// to boards. You can remove non-existing user ids manually from database,
// if that happens.
//. See:
// - wekan/client/components/settings/peopleBody.jade deleteButton
// - wekan/client/components/settings/peopleBody.js deleteButton
// - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
// that does now remove member from board, card members and assignees correctly,
// but that should be used to remove user from all boards similarly
// - wekan/models/users.js Delete is not enabled
//
//
*/
Popup.back();
// Use secure server method instead of direct client-side removal
Meteor.call('removeUser', this.userId, (error, result) => {
if (error) {
if (process.env.DEBUG === 'true') {
console.error('Error removing user:', error);
}
// Show error message to user
if (error.error === 'not-authorized') {
alert('You are not authorized to delete this user.');
} else if (error.error === 'user-not-found') {
alert('User not found.');
} else if (error.error === 'not-authorized' && error.reason === 'Cannot delete the last administrator') {
alert('Cannot delete the last administrator.');
} else {
alert('Error deleting user: ' + error.reason);
}
} else {
if (process.env.DEBUG === 'true') {
console.log('User deleted successfully:', result);
}
Popup.back();
}
});
},
});
+10 -10
View File
@@ -17,34 +17,34 @@
position: absolute;
}
.setting-content .content-title {
font-size: 20px;
font-size: clamp(16px, 3.5vw, 22px);
}
.setting-content .content-body {
display: flex;
padding-top: 15px;
padding-top: 2vh;
height: 100%;
gap: 10px;
gap: 1.3vw;
}
.setting-content .content-body .side-menu {
background-color: #f7f7f7;
border: 1px solid #f0f0f0;
border-radius: 4px;
width: 250px;
box-shadow: inset -1px -1px 3px rgba(0,0,0,0.05);
border-radius: 0.5vw;
width: min(250px, 32vw);
box-shadow: inset -0.2vh -0.2vh 0.4vh rgba(0,0,0,0.05);
}
.setting-content .content-body .side-menu ul li {
margin: 0.1rem 0.2rem;
margin: 0.2vh 0.3vw;
}
.setting-content .content-body .side-menu ul li.active {
background: #fff;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
}
.setting-content .content-body .side-menu ul li:hover {
background: #fff;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
}
.setting-content .content-body .side-menu ul li a {
padding: 1rem 0 1rem 1rem;
padding: 1.3vh 0 1.3vh 1.3vw;
width: 95%;
}
.setting-content .content-body .side-menu ul li a span {
@@ -42,6 +42,10 @@ template(name="setting")
a.js-setting-menu(data-id="webhook-setting")
i.fa.fa-globe
| {{_ 'global-webhook'}}
li
a.js-setting-menu(data-id="attachment-settings")
i.fa.fa-paperclip
| {{_ 'attachment-settings'}}
.main-body
if loading.get
+spinner
@@ -62,6 +66,8 @@ template(name="setting")
+layoutSettings
else if webhookSetting.get
+webhookSettings
else if attachmentSettings.get
+attachmentSettings
template(name="webhookSettings")
span
@@ -16,6 +16,7 @@ BlazeComponent.extendComponent({
this.accessibilitySetting = new ReactiveVar(false);
this.layoutSetting = new ReactiveVar(false);
this.webhookSetting = new ReactiveVar(false);
this.attachmentSettings = new ReactiveVar(false);
Meteor.subscribe('setting');
Meteor.subscribe('mailServer');
@@ -113,6 +114,7 @@ BlazeComponent.extendComponent({
this.accessibilitySetting.set('accessibility-setting' === targetID);
this.layoutSetting.set('layout-setting' === targetID);
this.webhookSetting.set('webhook-setting' === targetID);
this.attachmentSettings.set('attachment-settings' === targetID);
this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
}
},
+7
View File
@@ -284,6 +284,12 @@ template(name="boardCardSettingsPopup")
span
i.fa.fa-tags
| {{_ 'labels'}}
div.check-div
a.flex.js-field-has-card-show-lists-on-minicard(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsShowListsOnMinicard}}is-checked{{/if}}")
span
i.fa.fa-list
| {{_ 'card-show-lists-on-minicard'}}
div.check-div
a.flex.js-field-has-card-number(class="{{#if allowsCardNumber}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCardNumber}}is-checked{{/if}}")
@@ -652,6 +658,7 @@ template(name="addMemberPopup")
p.quiet {{_ 'no-results'}}
button.js-email-invite.primary.full {{_ 'email-invite'}}
template(name="changePermissionsPopup")
ul.pop-over-list
li
+23 -1
View File
@@ -996,10 +996,15 @@ BlazeComponent.extendComponent({
return this.currentBoard.allowsShowLists;
},
allowsLabels() {
return this.currentBoard.allowsLabels;
},
allowsShowListsOnMinicard() {
return this.currentBoard.allowsShowListsOnMinicard;
},
allowsChecklists() {
return this.currentBoard.allowsChecklists;
},
@@ -1282,13 +1287,29 @@ BlazeComponent.extendComponent({
this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
$(`.js-field-has-labels ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAssignee,
this.currentBoard.allowsLabels,
);
$('.js-field-has-labels').toggleClass(
CKCLS,
this.currentBoard.allowsLabels,
);
},
'click .js-field-has-card-show-lists-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsShowListsOnMinicard = !this.currentBoard
.allowsShowListsOnMinicard;
this.currentBoard.setAllowsShowListsOnMinicard(
this.currentBoard.allowsShowListsOnMinicard,
);
$(`.js-field-has-card-show-lists-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsShowListsOnMinicard,
);
$('.js-field-has-card-show-lists-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsShowListsOnMinicard,
);
},
'click .js-field-has-description-title'(evt) {
evt.preventDefault();
this.currentBoard.allowsDescriptionTitle = !this.currentBoard
@@ -1948,3 +1969,4 @@ Template.changePermissionsPopup.helpers({
);
},
});
+17 -9
View File
@@ -13,15 +13,15 @@
font-size: 50%;
color: #a6a6a6;
position: absolute;
top: 5px;
left: 100px;
top: 0.7vh;
left: 13vw;
}
.swimlane-header-menu .swimlane-header-collapse-up {
font-size: 50%;
color: #a6a6a6;
position: absolute;
bottom: 5px;
left: 100px;
bottom: 0.7vh;
left: 13vw;
}
.swimlane-header-menu .swimlane-header-uncollapse-up {
font-size: 50%;
@@ -35,10 +35,10 @@
background-color: rgba(0,0,0,0.2);
border-color: transparent;
box-shadow: none;
height: 100px;
height: 15vh;
}
.swimlane.ui-sortable-helper {
box-shadow: -2px 2px 8px rgba(0,0,0,0.3), 0 0 1px rgba(0,0,0,0.5);
box-shadow: -0.3vw 0.3vh 1vh rgba(0,0,0,0.3), 0 0 0.1vw rgba(0,0,0,0.5);
transform: rotate(2deg);
cursor: grabbing;
}
@@ -50,12 +50,17 @@
flex-direction: row;
flex: 1 0 100%;
background-color: #ccc;
width: 100%;
min-width: 100%;
position: relative;
overflow: hidden;
min-height: 33px;
}
.swimlane .swimlane-header-wrap .swimlane-header {
font-size: 14px;
padding: 5px 5px;
font-weight: bold;
min-height: 9px;
min-height: 33px;
width: 100%;
overflow: hidden;
-o-text-overflow: ellipsis;
@@ -86,9 +91,12 @@
position: absolute;
padding: 7px;
top: 50%;
transform: translateY(-50%);
left: 230px;
font-size: 18px;
font-size: clamp(16px, 3vw, 20px);
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
}
.swimlane .swimlane-header-wrap .swimlane-header-miniscreen-handle {
position: absolute;
+64 -7
View File
@@ -63,7 +63,7 @@ function initSortable(boardComponent, $listsDom) {
};
$listsDom.sortable({
connectWith: '.board-canvas',
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
@@ -82,10 +82,31 @@ function initSortable(boardComponent, $listsDom) {
const nextListDom = ui.item.next('.js-list').get(0);
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
$listsDom.sortable('cancel');
const listDomElement = ui.item.get(0);
const list = Blaze.getData(listDomElement);
// Detect if the list was dropped in a different swimlane
const targetSwimlaneDom = ui.item.closest('.js-swimlane');
let targetSwimlaneId = null;
if (targetSwimlaneDom.length > 0) {
// List was dropped in a swimlane
targetSwimlaneId = targetSwimlaneDom.attr('id').replace('swimlane-', '');
} else {
// List was dropped in lists view (not swimlanes view)
// In this case, assign to the default swimlane
const currentBoard = ReactiveCache.getBoard(Session.get('currentBoard'));
if (currentBoard) {
const defaultSwimlane = currentBoard.getDefaultSwimline();
if (defaultSwimlane) {
targetSwimlaneId = defaultSwimlane._id;
}
}
}
// Get the original swimlane ID of the list (handle backward compatibility)
const originalSwimlaneId = list.getEffectiveSwimlaneId ? list.getEffectiveSwimlaneId() : (list.swimlaneId || null);
/*
Reverted incomplete change list width,
removed from below Lists.update:
@@ -95,10 +116,44 @@ function initSortable(boardComponent, $listsDom) {
height: list._id.height(),
*/
// Prepare update object
const updateData = {
sort: sortIndex.base,
};
// Check if the list was dropped in a different swimlane
const isDifferentSwimlane = targetSwimlaneId && targetSwimlaneId !== originalSwimlaneId;
// If the list was dropped in a different swimlane, update the swimlaneId
if (isDifferentSwimlane) {
updateData.swimlaneId = targetSwimlaneId;
if (process.env.DEBUG === 'true') {
console.log(`Moving list "${list.title}" from swimlane ${originalSwimlaneId} to swimlane ${targetSwimlaneId}`);
}
// Move all cards in the list to the new swimlane
const cardsInList = ReactiveCache.getCards({
listId: list._id,
archived: false
});
cardsInList.forEach(card => {
card.move(list.boardId, targetSwimlaneId, list._id);
});
if (process.env.DEBUG === 'true') {
console.log(`Moved ${cardsInList.length} cards to swimlane ${targetSwimlaneId}`);
}
// Don't cancel the sortable when moving to a different swimlane
// The DOM move should be allowed to complete
} else {
// If staying in the same swimlane, cancel the sortable to prevent DOM manipulation issues
$listsDom.sortable('cancel');
}
Lists.update(list._id, {
$set: {
sort: sortIndex.base,
},
$set: updateData,
});
boardComponent.setIsDragging(false);
@@ -109,10 +164,12 @@ function initSortable(boardComponent, $listsDom) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$listsDom.sortable({
handle: '.js-list-handle',
connectWith: '.js-swimlane, .js-lists',
});
} else {
$listsDom.sortable({
handle: '.js-list-header',
connectWith: '.js-swimlane, .js-lists',
});
}
@@ -228,7 +285,7 @@ BlazeComponent.extendComponent({
const user = ReactiveCache.getCurrentUser();
const swimlane = Template.currentData();
const height = user.getSwimlaneHeight(swimlane.boardId, swimlane._id);
return height == -1 ? "auto" : (height + "px");
return height == -1 ? "auto" : (height + 5 + "px");
},
}).register('swimlane');
@@ -281,7 +338,7 @@ BlazeComponent.extendComponent({
boardId: Session.get('currentBoard'),
sort: sortIndex,
type: this.isListTemplatesSwimlane ? 'template-list' : 'list',
swimlaneId: this.currentBoard.isTemplatesBoard() ? this.currentSwimlane._id : '',
swimlaneId: this.currentSwimlane._id, // Always set swimlaneId for per-swimlane list titles
});
titleInput.value = '';
+1 -1
View File
@@ -23,7 +23,7 @@ Template.userAvatar.helpers({
/*
presenceStatusClassName() {
const user = ReactiveCache.getUser(this.userId);
const userPresence = presences.findOne({ userId: this.userId });
const userPresence = Presences.findOne({ userId: this.userId });
if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending';
else if (!userPresence) return 'disconnected';
else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
+13 -13
View File
@@ -1,25 +1,25 @@
.auth-layout .at-form-landing-logo {
width: 249px;
width: min(249px, 32vw);
margin: auto;
margin-top: 50px;
margin-bottom: 20px;
margin-top: 6vh;
margin-bottom: 2.5vh;
}
.auth-layout .auth-dialog {
width: 275px;
padding: 25px;
width: min(275px, 36vw);
padding: 3vh 3vw;
margin: auto;
margin-bottom: 20px;
margin-bottom: 2.5vh;
background: #fff;
border-radius: 3px;
border-radius: 0.4vw;
border: 1px solid #dbdbdb;
border-bottom-color: #c2c2c2;
box-shadow: 0 1px 6px rgba(0,0,0,0.3);
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
}
.auth-layout .auth-dialog .at-form .at-link {
color: #17683a;
}
.auth-layout .auth-dialog .at-form label {
margin-bottom: 3px;
margin-bottom: 0.4vh;
}
.auth-layout .auth-dialog .at-form input {
width: 100%;
@@ -31,9 +31,9 @@
}
.auth-layout .auth-dialog .at-form .at-title {
background: #f7f7f7;
margin: -25px;
padding: 15px 25px 5px;
margin-bottom: 20px;
margin: -3vh -3vw;
padding: 2vh 3vw 0.7vh;
margin-bottom: 2.5vh;
border-bottom: 1px solid #dcdcdc;
color: #4d4d4d;
font-weight: bold;
@@ -42,7 +42,7 @@
.auth-layout .auth-dialog .at-form .at-signin-link,
.auth-layout .auth-dialog .at-form .at-forgotPwd {
font-size: 0.9em;
margin-top: 15px;
margin-top: 2vh;
color: #4d4d4d;
}
.auth-layout .auth-dialog .at-form .at-signup-link .at-signUp,
+15 -2
View File
@@ -241,8 +241,21 @@ Template.editProfilePopup.events({
},
'click #deleteButton': Popup.afterConfirm('userDelete', function() {
Popup.back();
Users.remove(Meteor.userId());
AccountsTemplates.logout();
// Use secure server method for self-deletion
Meteor.call('removeUser', Meteor.userId(), (error, result) => {
if (error) {
if (process.env.DEBUG === 'true') {
console.error('Error removing user:', error);
}
alert('Error deleting account: ' + error.reason);
} else {
if (process.env.DEBUG === 'true') {
console.log('User deleted successfully:', result);
}
AccountsTemplates.logout();
}
});
}),
});
+121
View File
@@ -0,0 +1,121 @@
import DOMPurify from 'dompurify';
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
export function getSecureDOMPurifyConfig() {
return {
// Block dangerous elements that can cause XSS and CSS injection
FORBID_TAGS: [
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
'meta', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'textarea',
'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset',
'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'
],
// Block dangerous attributes that can cause XSS and CSS injection
FORBID_ATTR: [
'xlink:href', 'href', 'onload', 'onerror', 'onclick', 'onmouseover',
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress',
'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove',
'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend',
'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough',
'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata',
'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload',
'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage',
'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'
],
// Allow only safe image formats and protocols
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
// Remove dangerous protocols
ALLOW_UNKNOWN_PROTOCOLS: false,
// Sanitize URLs to prevent malicious content loading
SANITIZE_DOM: true,
// Remove dangerous elements completely
KEEP_CONTENT: false,
// Additional security measures
ADD_ATTR: [],
// Block data URIs that could contain malicious content
ALLOW_DATA_ATTR: false,
// Custom hook to further sanitize content
HOOKS: {
uponSanitizeElement: function(node, data) {
// Block any remaining dangerous elements
const dangerousTags = ['svg', 'style', 'script', 'link', 'meta', 'iframe', 'object', 'embed', 'applet'];
if (node.tagName && dangerousTags.includes(node.tagName.toLowerCase())) {
if (process.env.DEBUG === 'true') {
console.warn('Blocked potentially dangerous element:', node.tagName);
}
return false;
}
// Block img tags with SVG data URIs
if (node.tagName && node.tagName.toLowerCase() === 'img') {
const src = node.getAttribute('src');
if (src && (src.startsWith('data:image/svg') || src.endsWith('.svg'))) {
if (process.env.DEBUG === 'true') {
console.warn('Blocked potentially malicious SVG image:', src);
}
return false;
}
}
// Block elements with dangerous attributes
const dangerousAttrs = ['style', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur'];
for (const attr of dangerousAttrs) {
if (node.hasAttribute && node.hasAttribute(attr)) {
if (process.env.DEBUG === 'true') {
console.warn('Blocked element with dangerous attribute:', node.tagName, attr);
}
return false;
}
}
return true;
},
uponSanitizeAttribute: function(node, data) {
// Block style attributes completely
if (data.attrName === 'style') {
if (process.env.DEBUG === 'true') {
console.warn('Blocked style attribute');
}
return false;
}
// Block class and id attributes that might be used for CSS injection
if (data.attrName === 'class' || data.attrName === 'id') {
if (process.env.DEBUG === 'true') {
console.warn('Blocked class/id attribute:', data.attrName, data.attrValue);
}
return false;
}
// Block data attributes
if (data.attrName && data.attrName.startsWith('data-')) {
if (process.env.DEBUG === 'true') {
console.warn('Blocked data attribute:', data.attrName);
}
return false;
}
return true;
}
}
};
}
// Convenience function for secure sanitization
export function sanitizeHTML(html) {
return DOMPurify.sanitize(html, getSecureDOMPurifyConfig());
}
// Convenience function for sanitizing text (no HTML)
export function sanitizeText(text) {
return DOMPurify.sanitize(text, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
KEEP_CONTENT: true
});
}
+177
View File
@@ -0,0 +1,177 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
/**
* Global upload progress manager for drag-and-drop file uploads
* Tracks upload progress across all cards and provides reactive data
*/
class UploadProgressManager {
constructor() {
// Map of cardId -> array of upload objects
this.cardUploads = new ReactiveVar(new Map());
// Map of uploadId -> upload object for easy lookup
this.uploadMap = new ReactiveVar(new Map());
}
/**
* Add a new upload to track
* @param {string} cardId - The card ID
* @param {Object} uploader - The uploader object from Attachments.insert
* @param {File} file - The file being uploaded
* @returns {string} uploadId - Unique identifier for this upload
*/
addUpload(cardId, uploader, file) {
const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const upload = {
id: uploadId,
cardId: cardId,
file: file,
uploader: uploader,
progress: new ReactiveVar(0),
status: new ReactiveVar('uploading'), // 'uploading', 'completed', 'error'
error: new ReactiveVar(null),
startTime: Date.now(),
endTime: null
};
// Update card uploads
const currentCardUploads = this.cardUploads.get();
const cardUploads = currentCardUploads.get(cardId) || [];
cardUploads.push(upload);
currentCardUploads.set(cardId, cardUploads);
this.cardUploads.set(currentCardUploads);
// Update upload map
const currentUploadMap = this.uploadMap.get();
currentUploadMap.set(uploadId, upload);
this.uploadMap.set(currentUploadMap);
// Set up uploader event listeners
uploader.on('progress', (progress) => {
upload.progress.set(progress);
});
uploader.on('uploaded', (error, fileRef) => {
upload.status.set(error ? 'error' : 'completed');
upload.endTime = Date.now();
upload.error.set(error);
if (process.env.DEBUG === 'true') {
console.log(`Upload ${uploadId} completed:`, error ? 'error' : 'success');
}
// Remove from tracking after a delay to show completion
setTimeout(() => {
this.removeUpload(uploadId);
}, 2000);
});
uploader.on('error', (error) => {
upload.status.set('error');
upload.endTime = Date.now();
upload.error.set(error);
if (process.env.DEBUG === 'true') {
console.log(`Upload ${uploadId} failed:`, error);
}
// Remove from tracking after a delay to show error
setTimeout(() => {
this.removeUpload(uploadId);
}, 3000);
});
if (process.env.DEBUG === 'true') {
console.log(`Added upload ${uploadId} for card ${cardId}: ${file.name}`);
}
return uploadId;
}
/**
* Remove an upload from tracking
* @param {string} uploadId - The upload ID to remove
*/
removeUpload(uploadId) {
const upload = this.uploadMap.get().get(uploadId);
if (!upload) return;
const cardId = upload.cardId;
// Remove from card uploads
const currentCardUploads = this.cardUploads.get();
const cardUploads = currentCardUploads.get(cardId) || [];
const filteredCardUploads = cardUploads.filter(u => u.id !== uploadId);
if (filteredCardUploads.length === 0) {
currentCardUploads.delete(cardId);
} else {
currentCardUploads.set(cardId, filteredCardUploads);
}
this.cardUploads.set(currentCardUploads);
// Remove from upload map
const currentUploadMap = this.uploadMap.get();
currentUploadMap.delete(uploadId);
this.uploadMap.set(currentUploadMap);
if (process.env.DEBUG === 'true') {
console.log(`Removed upload ${uploadId} from tracking`);
}
}
/**
* Get all uploads for a specific card
* @param {string} cardId - The card ID
* @returns {Array} Array of upload objects
*/
getUploadsForCard(cardId) {
return this.cardUploads.get().get(cardId) || [];
}
/**
* Get upload count for a specific card
* @param {string} cardId - The card ID
* @returns {number} Number of active uploads
*/
getUploadCountForCard(cardId) {
return this.getUploadsForCard(cardId).length;
}
/**
* Check if a card has any active uploads
* @param {string} cardId - The card ID
* @returns {boolean} True if card has active uploads
*/
hasActiveUploads(cardId) {
return this.getUploadCountForCard(cardId) > 0;
}
/**
* Get all uploads across all cards
* @returns {Array} Array of all upload objects
*/
getAllUploads() {
const allUploads = [];
this.cardUploads.get().forEach(cardUploads => {
allUploads.push(...cardUploads);
});
return allUploads;
}
/**
* Clear all uploads (useful for cleanup)
*/
clearAllUploads() {
this.cardUploads.set(new Map());
this.uploadMap.set(new Map());
}
}
// Create global instance
const uploadProgressManager = new UploadProgressManager();
export default uploadProgressManager;
+142 -1
View File
@@ -51,6 +51,146 @@ Utils = {
const ret = ReactiveCache.getCard(cardId);
return ret;
},
// Zoom and mobile mode utilities
getZoomLevel() {
const user = ReactiveCache.getCurrentUser();
if (user && user.profile && user.profile.zoomLevel !== undefined) {
return user.profile.zoomLevel;
}
// For non-logged-in users, check localStorage
const stored = localStorage.getItem('wekan-zoom-level');
return stored ? parseFloat(stored) : 1.0;
},
setZoomLevel(level) {
const user = ReactiveCache.getCurrentUser();
if (user) {
// Update user profile
user.setZoomLevel(level);
} else {
// Store in localStorage for non-logged-in users
localStorage.setItem('wekan-zoom-level', level.toString());
}
Utils.applyZoomLevel(level);
// Trigger reactive updates for UI components
Session.set('wekan-zoom-level', level);
},
getMobileMode() {
const user = ReactiveCache.getCurrentUser();
if (user && user.profile && user.profile.mobileMode !== undefined) {
return user.profile.mobileMode;
}
// For non-logged-in users, check localStorage
const stored = localStorage.getItem('wekan-mobile-mode');
return stored ? stored === 'true' : false;
},
setMobileMode(enabled) {
const user = ReactiveCache.getCurrentUser();
if (user) {
// Update user profile
user.setMobileMode(enabled);
} else {
// Store in localStorage for non-logged-in users
localStorage.setItem('wekan-mobile-mode', enabled.toString());
}
Utils.applyMobileMode(enabled);
// Trigger reactive updates for UI components
Session.set('wekan-mobile-mode', enabled);
},
applyZoomLevel(level) {
const boardWrapper = document.querySelector('.board-wrapper');
const body = document.body;
const isMobileMode = body.classList.contains('mobile-mode');
if (boardWrapper) {
if (isMobileMode) {
// On mobile mode, only apply zoom to text and icons, not the entire layout
// Remove any existing transform from board-wrapper
boardWrapper.style.transform = '';
boardWrapper.style.transformOrigin = '';
// Apply zoom to text and icon elements instead
const textElements = boardWrapper.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div, .minicard, .list-header-name, .board-header-btn, .fa, .icon');
textElements.forEach(element => {
element.style.transform = `scale(${level})`;
element.style.transformOrigin = 'center';
});
// Reset board-canvas height
const boardCanvas = document.querySelector('.board-canvas');
if (boardCanvas) {
boardCanvas.style.height = '';
}
} else {
// Desktop mode: apply zoom to entire board-wrapper as before
boardWrapper.style.transform = `scale(${level})`;
boardWrapper.style.transformOrigin = 'top left';
// If zoom is 50% or lower, make board wrapper full width like content
if (level <= 0.5) {
boardWrapper.style.width = '100%';
boardWrapper.style.maxWidth = '100%';
boardWrapper.style.margin = '0';
} else {
// Reset to normal width for higher zoom levels
boardWrapper.style.width = '';
boardWrapper.style.maxWidth = '';
boardWrapper.style.margin = '';
}
// Adjust container height to prevent scroll issues
const boardCanvas = document.querySelector('.board-canvas');
if (boardCanvas) {
boardCanvas.style.height = `${100 / level}%`;
// For high zoom levels (200%+), enable both horizontal and vertical scrolling
if (level >= 2.0) {
boardCanvas.style.overflowX = 'auto';
boardCanvas.style.overflowY = 'auto';
// Ensure the content area can scroll both horizontally and vertically
const content = document.querySelector('#content');
if (content) {
content.style.overflowX = 'auto';
content.style.overflowY = 'auto';
}
} else {
// Reset overflow for normal zoom levels
boardCanvas.style.overflowX = '';
boardCanvas.style.overflowY = '';
const content = document.querySelector('#content');
if (content) {
content.style.overflowX = '';
content.style.overflowY = '';
}
}
}
}
}
},
applyMobileMode(enabled) {
const body = document.body;
if (enabled) {
body.classList.add('mobile-mode');
body.classList.remove('desktop-mode');
} else {
body.classList.add('desktop-mode');
body.classList.remove('mobile-mode');
}
},
initializeUserSettings() {
// Apply saved settings on page load
const zoomLevel = Utils.getZoomLevel();
const mobileMode = Utils.getMobileMode();
Utils.applyZoomLevel(zoomLevel);
Utils.applyMobileMode(mobileMode);
},
getCurrentList() {
const listId = this.getCurrentListId();
let ret = null;
@@ -376,7 +516,8 @@ Utils = {
// returns if mini screen or desktop drag handles
isTouchScreenOrShowDesktopDragHandles() {
return this.isTouchScreen();
// Always enable drag handles for mobile screens (touch devices)
return this.isTouchScreen() || this.isMiniScreen();
//return this.isTouchScreen() || this.isShowDesktopDragHandles();
//return this.isShowDesktopDragHandles();
},
+31
View File
@@ -163,9 +163,12 @@ services:
# ap-southeast-1,ap-northeast-1,sa-east-1
#
#- S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
#- S3_SECRET_FILE=/run/secrets/s3_secret
#-----------------------------------------------------------------
# ==== MONGO_URL ====
- MONGO_URL=mongodb://wekandb:27017/wekan
#- MONGO_URL=mongodb://username:password@wekandb:27017/wekan
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
# Change ROOT_URL to your real Wekan URL, for example:
@@ -194,6 +197,7 @@ services:
#- MAIL_SERVICE=Outlook365
#- MAIL_SERVICE_USER=firstname.lastname@hotmail.com
#- MAIL_SERVICE_PASSWORD=SecretPassword
#- MAIL_SERVICE_PASSWORD_FILE=/run/secrets/mail_service_password
#---------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap, this is done by default at Dockerfile:
@@ -397,6 +401,7 @@ services:
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
# Secret key generated during app registration:
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
#- OAUTH2_SERVER_URL=https://login.microsoftonline.com/
#- OAUTH2_AUTH_ENDPOINT=/oauth2/v2.0/authorize
#- OAUTH2_USERINFO_ENDPOINT=https://graph.microsoft.com/oidc/userinfo
@@ -423,6 +428,7 @@ services:
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
# Secret key generated during app registration:
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
#- OAUTH2_SERVER_URL=https://your-nextcloud.tld
#- OAUTH2_AUTH_ENDPOINT=/index.php/apps/oauth2/authorize
#- OAUTH2_USERINFO_ENDPOINT=/ocs/v2.php/cloud/user?format=json
@@ -447,6 +453,7 @@ services:
#- OAUTH2_USERINFO_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/userinfo
#- OAUTH2_TOKEN_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/token
#- OAUTH2_SECRET=<keycloak client secret>
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
#- OAUTH2_ID_MAP=sub
#- OAUTH2_USERNAME_MAP=preferred_username
#- OAUTH2_EMAIL_MAP=email
@@ -464,6 +471,7 @@ services:
#- OAUTH2_CLIENT_ID=abcde12345
# OAuth2 Secret.
#- OAUTH2_SECRET=54321abcde
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
# OAuth2 Server URL.
#- OAUTH2_SERVER_URL=https://chat.example.com
# OAuth2 Authorization Endpoint.
@@ -570,6 +578,7 @@ services:
#
# The password for the search user
#- LDAP_AUTHENTIFICATION_PASSWORD=pwd
#- LDAP_AUTHENTIFICATION_PASSWORD_FILE=/run/secrets/ldap_auth_password
#
# Enable logs for the module
#- LDAP_LOG_ENABLED=true
@@ -725,6 +734,12 @@ services:
volumes:
- /etc/localtime:/etc/localtime:ro
- wekan-files:/data:rw
secrets:
- ldap_auth_password
- oauth2_secret
- mail_service_password
- mongo_password
- s3_secret
#---------------------------------------------------------------------------------
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
@@ -786,3 +801,19 @@ volumes:
networks:
wekan-tier:
driver: bridge
# Docker Compose Secrets
# Create secret files on the host system before running docker-compose up
# Example: echo "your_password_here" > ldap_auth_password.txt
# Then use: docker-compose up -d
secrets:
ldap_auth_password:
file: ./secrets/ldap_auth_password.txt
oauth2_secret:
file: ./secrets/oauth2_secret.txt
mail_service_password:
file: ./secrets/mail_service_password.txt
mongo_password:
file: ./secrets/mongo_password.txt
s3_secret:
file: ./secrets/s3_secret.txt
+31
View File
@@ -167,9 +167,12 @@ services:
# ap-southeast-1,ap-northeast-1,sa-east-1
#
#- S3='{"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "xxx"}}'
#- S3_SECRET_FILE=/run/secrets/s3_secret
#-----------------------------------------------------------------
# ==== MONGO_URL ====
- MONGO_URL=mongodb://wekandb:27017/wekan
#- MONGO_URL=mongodb://username:password@wekandb:27017/wekan
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
#---------------------------------------------------------------
# ==== ROOT_URL SETTING ====
# Change ROOT_URL to your real Wekan URL, for example:
@@ -198,6 +201,7 @@ services:
#- MAIL_SERVICE=Outlook365
#- MAIL_SERVICE_USER=firstname.lastname@hotmail.com
#- MAIL_SERVICE_PASSWORD=SecretPassword
#- MAIL_SERVICE_PASSWORD_FILE=/run/secrets/mail_service_password
#---------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap, this is done by default at Dockerfile:
@@ -399,6 +403,7 @@ services:
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
# Secret key generated during app registration:
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
#- OAUTH2_SERVER_URL=https://login.microsoftonline.com/
#- OAUTH2_AUTH_ENDPOINT=/oauth2/v2.0/authorize
#- OAUTH2_USERINFO_ENDPOINT=https://graph.microsoft.com/oidc/userinfo
@@ -425,6 +430,7 @@ services:
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
# Secret key generated during app registration:
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
#- OAUTH2_SERVER_URL=https://your-nextcloud.tld
#- OAUTH2_AUTH_ENDPOINT=/index.php/apps/oauth2/authorize
#- OAUTH2_USERINFO_ENDPOINT=/ocs/v2.php/cloud/user?format=json
@@ -449,6 +455,7 @@ services:
#- OAUTH2_USERINFO_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/userinfo
#- OAUTH2_TOKEN_ENDPOINT=/realms/<keycloak realm>/protocol/openid-connect/token
#- OAUTH2_SECRET=<keycloak client secret>
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
#-----------------------------------------------------------------
# ==== OAUTH2 DOORKEEPER ====
# https://github.com/wekan/wekan/issues/1874
@@ -462,6 +469,7 @@ services:
#- OAUTH2_CLIENT_ID=abcde12345
# OAuth2 Secret.
#- OAUTH2_SECRET=54321abcde
#- OAUTH2_SECRET_FILE=/run/secrets/oauth2_secret
# OAuth2 Server URL.
#- OAUTH2_SERVER_URL=https://chat.example.com
# OAuth2 Authorization Endpoint.
@@ -568,6 +576,7 @@ services:
#
# The password for the search user
#- LDAP_AUTHENTIFICATION_PASSWORD=pwd
#- LDAP_AUTHENTIFICATION_PASSWORD_FILE=/run/secrets/ldap_auth_password
#
# Enable logs for the module
#- LDAP_LOG_ENABLED=true
@@ -723,6 +732,12 @@ services:
volumes:
- /etc/localtime:/etc/localtime:ro
- wekan-files:/data:rw
secrets:
- ldap_auth_password
- oauth2_secret
- mail_service_password
- mongo_password
- s3_secret
#---------------------------------------------------------------------------------
# ==== OPTIONAL: SHARE DATABASE TO OFFICE LAN AND REMOTE VPN ====
@@ -784,3 +799,19 @@ volumes:
networks:
wekan-tier:
driver: bridge
# Docker Compose Secrets
# Create secret files on the host system before running docker-compose up
# Example: echo "your_password_here" > ldap_auth_password.txt
# Then use: docker-compose up -d
secrets:
ldap_auth_password:
file: ./secrets/ldap_auth_password.txt
oauth2_secret:
file: ./secrets/oauth2_secret.txt
mail_service_password:
file: ./secrets/mail_service_password.txt
mongo_password:
file: ./secrets/mongo_password.txt
s3_secret:
file: ./secrets/s3_secret.txt
@@ -0,0 +1,318 @@
# MongoDB Compatibility Guide
## Overview
This guide documents MongoDB compatibility issues and fixes for Wekan across MongoDB versions 3.0 through 8.0, ensuring proper operation with Meteor.js 2.14.
## Current Status
- **Meteor.js Version**: 2.14
- **MongoDB Package**: mongo@1.16.8
- **Supported MongoDB Versions**: 3.0 - 8.0
- **Compatibility Status**: ✅ Fixed
## Compatibility Issues Fixed
### 1. Deprecated `.count()` Method
**Issue**: The `.count()` method is deprecated in newer MongoDB versions.
**Fixed Files**:
- `models/users.js` - Line 1839
- `server/migrations.js` - Line 209
- `server/publications/cards.js` - Lines 709, 711, 713
- `client/components/settings/adminReports.js` - Line 115
**Fix Applied**:
```javascript
// Before (deprecated)
const count = collection.find().count();
// After (compatible)
const count = collection.find().countDocuments();
```
### 2. MongoDB 8.0 Null/Undefined Equality
**Issue**: MongoDB 8.0 changed behavior where equality matches to `null` no longer match `undefined` values.
**Fixed Files**:
- `models/customFields.js` - Custom field initialization
- `models/cards.js` - Card custom field initialization
**Fix Applied**:
```javascript
// Before (MongoDB 8.0 incompatible)
{ field: null }
// After (MongoDB 8.0 compatible)
{ field: { $in: [null, undefined] } }
```
## Direct Operations Analysis
### Purpose of Direct Operations
Direct operations (`.direct.insert()`, `.direct.update()`, `.direct.remove()`) are used intentionally in Wekan to:
1. **Bypass Meteor Security**: For system operations that need to bypass validation
2. **Migration Scripts**: For data migration operations
3. **Import/Export**: For bulk data operations
4. **System Initialization**: For setting up initial data
### Files Using Direct Operations
**Models**:
- `models/wekanCreator.js` - Wekan board creation
- `models/trelloCreator.js` - Trello import
- `models/cards.js` - Card operations
- `models/boards.js` - Board operations
- `models/lists.js` - List operations
- `models/swimlanes.js` - Swimlane operations
- `models/customFields.js` - Custom field operations
- `models/checklistItems.js` - Checklist operations
- `models/integrations.js` - Integration operations
- `models/csvCreator.js` - CSV export operations
**Server**:
- `server/migrations.js` - Database migrations
- `server/notifications/outgoing.js` - Notification operations
### Security Considerations
Direct operations bypass Meteor's security model, so they should only be used when:
- ✅ System-level operations (migrations, imports)
- ✅ Bulk operations that need performance
- ✅ Operations that need to bypass validation
- ❌ User-initiated operations (use regular methods)
- ❌ Operations that need security validation
## Raw Collection Usage
### Purpose of Raw Collections
Raw collections (`.rawCollection()`) are used for:
1. **Advanced Aggregation**: Complex aggregation pipelines
2. **Database Commands**: Direct database commands
3. **Index Management**: Creating/dropping indexes
4. **Migration Operations**: Complex data transformations
### Files Using Raw Collections
- `models/server/metrics.js` - Metrics aggregation
- `server/migrations.js` - Migration operations
### Security Considerations
Raw collections bypass all Meteor security, so they should only be used when:
- ✅ Server-side only operations
- ✅ System administration tasks
- ✅ Complex aggregation pipelines
- ❌ Client-side operations
- ❌ User data operations without validation
## Version-Specific Compatibility
### MongoDB 3.0 - 3.6
- ✅ Full compatibility
- ✅ Uses mongodb3legacy driver
- ✅ All operations supported
### MongoDB 4.0 - 4.4
- ✅ Full compatibility
- ✅ Uses mongodb4legacy driver
- ✅ All operations supported
### MongoDB 5.0
- ✅ Full compatibility
- ✅ Uses mongodb5legacy driver
- ✅ All operations supported
### MongoDB 6.0
- ✅ Full compatibility
- ✅ Uses mongodb6legacy driver
- ✅ All operations supported
### MongoDB 7.0
- ✅ Full compatibility
- ✅ Uses mongodb7legacy driver
- ✅ All operations supported
### MongoDB 8.0
- ✅ Full compatibility (after fixes)
- ✅ Uses mongodb8legacy driver
- ✅ Null/undefined equality fixed
## Testing Recommendations
### Test Matrix
| MongoDB Version | Driver | Status | Test Priority |
|----------------|--------|--------|---------------|
| 3.0 | mongodb3legacy | ✅ Compatible | High |
| 3.2 | mongodb3legacy | ✅ Compatible | High |
| 3.4 | mongodb3legacy | ✅ Compatible | High |
| 3.6 | mongodb3legacy | ✅ Compatible | High |
| 4.0 | mongodb4legacy | ✅ Compatible | High |
| 4.2 | mongodb4legacy | ✅ Compatible | High |
| 4.4 | mongodb4legacy | ✅ Compatible | High |
| 5.0 | mongodb5legacy | ✅ Compatible | Medium |
| 6.0 | mongodb6legacy | ✅ Compatible | Medium |
| 7.0 | mongodb7legacy | ✅ Compatible | Medium |
| 8.0 | mongodb8legacy | ✅ Compatible | High |
### Test Scenarios
1. **Basic Operations**:
- Create/Read/Update/Delete operations
- Collection queries and filters
- Index operations
2. **Advanced Operations**:
- Aggregation pipelines
- Complex queries with null/undefined
- Direct operations
- Raw collection operations
3. **Migration Operations**:
- Database migrations
- Data import/export
- Schema changes
4. **Performance Testing**:
- Large dataset operations
- Concurrent operations
- Memory usage
## Monitoring and Debugging
### MongoDB Driver System
The MongoDB driver system provides:
- Automatic version detection
- Driver selection based on MongoDB version
- Connection monitoring
- Error tracking
### Debug Methods
```javascript
// Get connection statistics
Meteor.call('mongodb-driver-stats', (error, result) => {
console.log('Connection Stats:', result);
});
// Test connection
Meteor.call('mongodb-driver-test-connection', (error, result) => {
console.log('Connection Test:', result);
});
// Get supported versions
Meteor.call('mongodb-driver-supported-versions', (error, result) => {
console.log('Supported Versions:', result);
});
```
### Real-time Monitoring
```javascript
// Subscribe to monitoring
Meteor.subscribe('mongodb-driver-monitor');
// Access data in template
Template.yourTemplate.helpers({
driverStats() {
return MongoDBDriverMonitor.findOne('stats');
}
});
```
## Best Practices
### Query Optimization
1. **Use Indexes**: Ensure proper indexes for frequently queried fields
2. **Limit Results**: Use `.limit()` and `.skip()` for pagination
3. **Project Fields**: Use projection to limit returned fields
4. **Aggregation**: Use aggregation pipelines for complex operations
### Security
1. **Validate Input**: Always validate user input
2. **Use Regular Methods**: Prefer regular methods over direct operations
3. **Server-side Only**: Keep sensitive operations server-side
4. **Audit Logging**: Log important operations
### Performance
1. **Connection Pooling**: Use connection pooling for better performance
2. **Batch Operations**: Use batch operations for bulk data
3. **Caching**: Implement caching for frequently accessed data
4. **Monitoring**: Monitor query performance and optimize as needed
## Troubleshooting
### Common Issues
1. **Connection Failures**:
- Check MONGO_URL configuration
- Verify MongoDB server is running
- Check network connectivity
2. **Query Errors**:
- Verify query syntax
- Check field names and types
- Validate MongoDB version compatibility
3. **Performance Issues**:
- Check indexes
- Optimize queries
- Monitor connection pool usage
### Debug Commands
```bash
# Check MongoDB version
mongo --eval "db.version()"
# Check connection status
mongo --eval "db.runCommand({connectionStatus: 1})"
# Check indexes
mongo --eval "db.collection.getIndexes()"
# Check query performance
mongo --eval "db.collection.find().explain('executionStats')"
```
## Migration Guide
### From Older MongoDB Versions
1. **Backup Data**: Always backup before migration
2. **Test Compatibility**: Test with target MongoDB version
3. **Update Drivers**: Use appropriate driver for MongoDB version
4. **Run Migrations**: Execute any necessary data migrations
5. **Validate**: Verify all functionality works correctly
### To Newer MongoDB Versions
1. **Check Compatibility**: Verify all queries are compatible
2. **Update Queries**: Fix any deprecated method usage
3. **Test Thoroughly**: Test all functionality
4. **Monitor Performance**: Watch for performance changes
5. **Update Documentation**: Update any version-specific documentation
## Support
For MongoDB compatibility issues:
1. Check this guide for known issues and solutions
2. Review MongoDB release notes for version-specific changes
3. Test with the MongoDB driver system
4. Use the monitoring tools to diagnose issues
5. Consult the Wekan community for additional help
## License
This MongoDB Compatibility Guide is part of Wekan and is licensed under the MIT License.
+281
View File
@@ -0,0 +1,281 @@
# MongoDB Driver System
## Overview
The MongoDB Driver System provides automatic MongoDB version detection and driver selection to support MongoDB versions 3.0 through 8.0 with compatible Node.js drivers. This system eliminates the need for manual driver configuration and provides seamless compatibility across all supported MongoDB versions.
## Features
- **Automatic Version Detection**: Detects MongoDB server version from wire protocol errors
- **Dynamic Driver Selection**: Automatically selects the appropriate Node.js driver based on detected version
- **Fallback Mechanism**: Tries multiple drivers if the first attempt fails
- **Connection Retry Logic**: Retries connections with different drivers on failure
- **Real-time Monitoring**: Provides connection statistics and monitoring capabilities
- **Meteor Integration**: Seamlessly integrates with Meteor's MongoDB connection system
## Supported MongoDB Versions
| MongoDB Version | Node.js Driver | Driver Version | Package Name |
|----------------|----------------|----------------|--------------|
| 3.0 - 3.6 | mongodb@3.7.4 | 3.7.4 | mongodb3legacy |
| 4.0 - 4.4 | mongodb@4.17.2 | 4.17.2 | mongodb4legacy |
| 5.0 | mongodb@5.9.2 | 5.9.2 | mongodb5legacy |
| 6.0 | mongodb@6.3.0 | 6.3.0 | mongodb6legacy |
| 7.0 | mongodb@7.0.1 | 7.0.1 | mongodb7legacy |
| 8.0 | mongodb@6.9.0 | 6.9.0 | mongodb8legacy |
## Architecture
### Core Components
1. **MongoDBDriverManager** (`models/lib/mongodbDriverManager.js`)
- Manages driver compatibility matrix
- Detects MongoDB version from wire protocol errors
- Selects appropriate driver based on detected version
- Tracks connection attempts and statistics
2. **MongoDBConnectionManager** (`models/lib/mongodbConnectionManager.js`)
- Handles MongoDB connections with automatic driver selection
- Implements connection retry logic with different drivers
- Manages connection pooling and lifecycle
- Provides fallback mechanism for unsupported versions
3. **MeteorMongoIntegration** (`models/lib/meteorMongoIntegration.js`)
- Integrates with Meteor's MongoDB connection system
- Overrides Meteor's connection methods to use custom drivers
- Provides Meteor-compatible connection objects
- Enhances collections with additional methods
4. **MongoDB Driver Startup** (`server/mongodb-driver-startup.js`)
- Initializes the system on server startup
- Provides server-side methods for monitoring and debugging
- Sets up real-time monitoring publications
## Installation
The MongoDB driver system is automatically installed when you install Wekan dependencies:
```bash
npm install
```
All required driver packages are included in `package.json`:
```json
{
"dependencies": {
"mongodb3legacy": "npm:mongodb@3.7.4",
"mongodb4legacy": "npm:mongodb@4.17.2",
"mongodb5legacy": "npm:mongodb@5.9.2",
"mongodb6legacy": "npm:mongodb@6.3.0",
"mongodb7legacy": "npm:mongodb@7.0.1",
"mongodb8legacy": "npm:mongodb@6.9.0"
}
}
```
## Usage
### Automatic Operation
The system works automatically without any configuration required:
1. **Startup**: The system initializes automatically when Wekan starts
2. **Connection**: When connecting to MongoDB, the system detects the server version
3. **Driver Selection**: The appropriate driver is selected based on the detected version
4. **Fallback**: If the first driver fails, fallback drivers are tried automatically
### Manual Testing
You can test the system using the provided test script:
```bash
node test-mongodb-drivers.js
```
### Monitoring
The system provides several ways to monitor its operation:
#### Server-side Methods
```javascript
// Get connection statistics
Meteor.call('mongodb-driver-stats', (error, result) => {
console.log('Connection Stats:', result);
});
// Test connection
Meteor.call('mongodb-driver-test-connection', (error, result) => {
console.log('Connection Test:', result);
});
// Get supported versions
Meteor.call('mongodb-driver-supported-versions', (error, result) => {
console.log('Supported Versions:', result);
});
// Reset system
Meteor.call('mongodb-driver-reset', (error, result) => {
console.log('Reset Result:', result);
});
```
#### Real-time Monitoring
Subscribe to the monitoring publication:
```javascript
Meteor.subscribe('mongodb-driver-monitor');
```
Access the data in your template:
```javascript
Template.yourTemplate.helpers({
driverStats() {
return MongoDBDriverMonitor.findOne('stats');
}
});
```
## Wire Protocol Error Detection
The system detects MongoDB versions by analyzing wire protocol errors. Here are the error patterns used:
### MongoDB 3.x
- `unsupported wire protocol version`
- `wire protocol version 0/1/2/3`
- `protocol version 0/1/2/3`
### MongoDB 4.x
- `wire protocol version 4/5/6`
- `protocol version 4/5/6`
### MongoDB 5.x
- `wire protocol version 7`
- `protocol version 7`
### MongoDB 6.x
- `wire protocol version 8`
- `protocol version 8`
### MongoDB 7.x
- `wire protocol version 9`
- `protocol version 9`
### MongoDB 8.x
- `wire protocol version 10`
- `protocol version 10`
## Configuration
### Environment Variables
The system uses the standard MongoDB environment variables:
- `MONGO_URL`: MongoDB connection string
- `MONGO_OPLOG_URL`: MongoDB oplog connection string (optional)
### Connection Options
You can customize connection options by modifying the default options in `mongodbConnectionManager.js`:
```javascript
const defaultOptions = {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
// Add your custom options here
};
```
## Troubleshooting
### Common Issues
1. **Driver Not Found**
- Ensure all driver packages are installed: `npm install`
- Check that package.json contains all required drivers
2. **Connection Failures**
- Verify MONGO_URL is correctly set
- Check MongoDB server is running and accessible
- Review connection logs for specific error messages
3. **Version Detection Issues**
- Check if MongoDB server is responding
- Verify wire protocol error patterns match your MongoDB version
- Review connection attempt logs
### Debugging
Enable debug logging by setting the DEBUG environment variable:
```bash
DEBUG=mongodb-driver* node main.js
```
### Logs
The system provides detailed logging:
```
=== MongoDB Driver System Startup ===
MONGO_URL found, initializing MongoDB driver system...
Connection string: mongodb://***:***@localhost:27017/wekan
Initializing Meteor MongoDB Integration...
Testing MongoDB connection...
✅ MongoDB connection test successful
Driver: mongodb6legacy
Version: 6.0
MongoDB Driver System Statistics:
Initialized: true
Custom Connection: true
Supported Versions: 3.0, 3.2, 3.4, 3.6, 4.0, 4.2, 4.4, 5.0, 6.0, 7.0, 8.0
=== MongoDB Driver System Ready ===
```
## Performance Considerations
- **Connection Pooling**: Each driver maintains its own connection pool
- **Driver Selection**: Version detection happens only on first connection
- **Fallback Overhead**: Fallback attempts add minimal overhead
- **Memory Usage**: Multiple drivers are loaded but only one is active
## Security
- **Credential Protection**: Connection strings are logged with credentials masked
- **Access Control**: Monitoring methods require user authentication
- **Error Handling**: Sensitive information is not exposed in error messages
## Migration from Standard MongoDB
If you're migrating from standard MongoDB connections:
1. **No Code Changes Required**: The system is backward compatible
2. **Automatic Detection**: Version detection happens automatically
3. **Gradual Migration**: You can test with specific drivers before full migration
4. **Rollback**: You can disable the system and return to standard connections
## Future Enhancements
- **Additional MongoDB Versions**: Support for newer MongoDB versions as they're released
- **Performance Optimization**: Connection pooling optimizations
- **Enhanced Monitoring**: More detailed metrics and alerting
- **Configuration UI**: Web interface for driver configuration
## Support
For issues or questions:
1. Check the troubleshooting section above
2. Review the logs for specific error messages
3. Test with the provided test script
4. Use the monitoring methods to gather diagnostic information
## License
This MongoDB Driver System is part of Wekan and is licensed under the MIT License.
@@ -0,0 +1,189 @@
# MongoDB Version Management System
This document describes the MongoDB version management system that automatically detects MongoDB server versions and switches to the appropriate binary and Node.js driver.
## Overview
The system provides:
- **Automatic version detection** based on connection attempts and wire protocol errors
- **Automatic binary switching** to the correct MongoDB server version
- **Automatic driver selection** using the appropriate Node.js MongoDB driver
- **Support for MongoDB versions 3-8** with fallback mechanisms
## Current Status
### Available MongoDB Server Binaries
- ✅ **MongoDB 3.2.22** - Available at `/snap/wekan/current/migratemongo/bin/`
- ❌ **MongoDB 4.4.28** - Not available (needs to be added to snap package)
- ❌ **MongoDB 5.0.28** - Not available (needs to be added to snap package)
- ❌ **MongoDB 6.0.15** - Not available (needs to be added to snap package)
- ✅ **MongoDB 7.0.25** - Available at `/snap/wekan/current/bin/`
- ❌ **MongoDB 8.0.4** - Not available (needs to be added to snap package)
### Available MongoDB Node.js Drivers
- ✅ **MongoDB 3.7.4** - Available as `mongodb3legacy`
- ✅ **MongoDB 4.17.2** - Available as `mongodb4legacy`
- ✅ **MongoDB 5.9.2** - Available as `mongodb5legacy`
- ✅ **MongoDB 6.3.0** - Available as `mongodb6legacy`
- ✅ **MongoDB 7.0.1** - Available as `mongodb7legacy`
- ✅ **MongoDB 8.0** - Available as `mongodb8legacy`
## How It Works
### 1. Version Detection
The system attempts to connect to MongoDB using the latest available binary (MongoDB 7.x) and analyzes the response:
- **Success**: If connection succeeds, it's MongoDB 7.x
- **Wire Protocol Error**: If connection fails with wire protocol errors, it detects the version based on the protocol version number:
- Protocol 0-3: MongoDB 3.x
- Protocol 4: MongoDB 4.x
- Protocol 5: MongoDB 5.x
- Protocol 6: MongoDB 6.x
- Protocol 7: MongoDB 7.x
- Protocol 8: MongoDB 8.x
### 2. Binary Switching
Based on the detected version, the system switches to the appropriate MongoDB server binary:
```bash
# MongoDB 3.x
export PATH="/snap/wekan/current/migratemongo/bin:$PATH"
export LD_LIBRARY_PATH="/snap/wekan/current/migratemongo/lib:$LD_LIBRARY_PATH"
# MongoDB 7.x (default)
export PATH="/snap/wekan/current/bin:$PATH"
export LD_LIBRARY_PATH="/snap/wekan/current/usr/lib:$LD_LIBRARY_PATH"
```
### 3. Driver Selection
The Node.js application automatically selects the appropriate MongoDB driver based on the detected version using the `mongodbDriverManager` system.
## Files Modified
### Removed Migration Files
- ❌ `snap-src/bin/mongodb-migrate` - Removed
- ❌ `snap-src/bin/mongodb-migration-web` - Removed
- ❌ `snap-src/bin/mongodb-migration-status` - Removed
### Updated Files
- ✅ `snap-src/bin/mongodb-control` - Completely rewritten with version detection
- ✅ `snap-src/bin/mongodb-version-manager` - New utility for managing versions
### Node.js Driver System
- ✅ `models/lib/mongodbDriverManager.js` - Manages multiple MongoDB drivers
- ✅ `models/lib/mongodbConnectionManager.js` - Handles connections with version detection
- ✅ `models/lib/meteorMongoIntegration.js` - Integrates with Meteor's Mongo.Collection
- ✅ `server/mongodb-driver-startup.js` - Initializes the driver system
## Adding Missing MongoDB Server Binaries
To add MongoDB versions 4, 5, 6, and 8 to the snap package, you need to:
### 1. Download MongoDB Server Binaries
Download the appropriate MongoDB server binaries for each version:
```bash
# MongoDB 4.4.28
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.4.28.tgz
# MongoDB 5.0.28
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-5.0.28.tgz
# MongoDB 6.0.15
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-6.0.15.tgz
# MongoDB 8.0.4
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-8.0.4.tgz
```
### 2. Extract and Install in Snap Package
Extract each version to the appropriate directory in the snap package:
```bash
# Extract MongoDB 4.x
tar -xzf mongodb-linux-x86_64-4.4.28.tgz
mkdir -p /snap/wekan/current/mongodb4/bin
cp mongodb-linux-x86_64-4.4.28/bin/* /snap/wekan/current/mongodb4/bin/
# Extract MongoDB 5.x
tar -xzf mongodb-linux-x86_64-5.0.28.tgz
mkdir -p /snap/wekan/current/mongodb5/bin
cp mongodb-linux-x86_64-5.0.28/bin/* /snap/wekan/current/mongodb5/bin/
# Extract MongoDB 6.x
tar -xzf mongodb-linux-x86_64-6.0.15.tgz
mkdir -p /snap/wekan/current/mongodb6/bin
cp mongodb-linux-x86_64-6.0.15/bin/* /snap/wekan/current/mongodb6/bin/
# Extract MongoDB 8.x
tar -xzf mongodb-linux-x86_64-8.0.4.tgz
mkdir -p /snap/wekan/current/mongodb8/bin
cp mongodb-linux-x86_64-8.0.4/bin/* /snap/wekan/current/mongodb8/bin/
```
### 3. Update Snap Package Build
Modify the snap package build process to include these binaries in the final package.
## Usage
### Check Available Versions
```bash
$SNAP/bin/mongodb-version-manager versions
```
### Check Current Active Version
```bash
$SNAP/bin/mongodb-version-manager active
```
### Force Version Detection
```bash
$SNAP/bin/mongodb-version-manager detect
```
### View Detection Log
```bash
$SNAP/bin/mongodb-version-manager log
```
## Configuration
### Environment Variables
- `MONGO_URL` - MongoDB connection URL (default: `mongodb://127.0.0.1:27017/wekan`)
- `SNAP_COMMON` - Snap common directory for logs and version files
### Version Files
- `${SNAP_COMMON}/mongodb-active-version` - Caches the currently active version
- `${SNAP_COMMON}/mongodb-version-detection.log` - Logs version detection events
## Benefits
1. **No Downtime**: Automatic version detection and switching without manual intervention
2. **Backward Compatibility**: Works with existing MongoDB 3.x databases
3. **Forward Compatibility**: Ready for MongoDB 8.x when available
4. **Automatic Fallback**: Falls back to MongoDB 7.x if detection fails
5. **Comprehensive Logging**: Detailed logs for troubleshooting
6. **No Migrations**: Eliminates the need for database migrations
## Troubleshooting
### Version Detection Not Working
1. Check the detection log: `$SNAP/bin/mongodb-version-manager log`
2. Force re-detection: `$SNAP/bin/mongodb-version-manager detect`
3. Restart MongoDB service
### Wrong Version Selected
1. Clear the cached version: `rm ${SNAP_COMMON}/mongodb-active-version`
2. Restart MongoDB service
### Connection Issues
1. Verify MongoDB server is running
2. Check MONGO_URL environment variable
3. Review MongoDB server logs
## Future Enhancements
1. **Add Missing Binaries**: Include MongoDB 4, 5, 6, and 8 server binaries in snap package
2. **Health Checks**: Add health monitoring for different MongoDB versions
3. **Performance Metrics**: Track performance across different versions
4. **Auto-Upgrade**: Automatically upgrade to newer versions when available
@@ -0,0 +1,306 @@
# Enhanced Attachment Migration System
## Overview
The Enhanced Attachment Migration System provides a comprehensive solution for managing attachment storage across multiple backends (filesystem, MongoDB GridFS, S3/MinIO) with CPU throttling, real-time monitoring, and secure configuration management.
## Features
### 1. Multi-Backend Storage Support
- **Filesystem Storage**: Local file system storage using `WRITABLE_PATH`
- **MongoDB GridFS**: Database-based file storage
- **S3/MinIO**: Cloud and object storage compatibility
### 2. CPU Throttling
- **Automatic CPU Monitoring**: Real-time CPU usage tracking
- **Configurable Thresholds**: Set CPU usage limits (10-90%)
- **Automatic Pausing**: Migration pauses when CPU threshold is exceeded
- **Resume Capability**: Continue migration when CPU usage drops
### 3. Batch Processing
- **Configurable Batch Size**: Process 1-100 attachments per batch
- **Adjustable Delays**: Set delays between batches (100-10000ms)
- **Progress Tracking**: Real-time progress monitoring
- **Queue Management**: Intelligent migration queue handling
### 4. Security Features
- **Password Protection**: S3 secret keys are never displayed
- **Admin-Only Access**: All operations require admin privileges
- **Secure Configuration**: Environment-based configuration management
- **Audit Logging**: Comprehensive migration logging
### 5. Real-Time Monitoring
- **Storage Statistics**: Live attachment counts and sizes
- **Visual Charts**: Storage distribution visualization
- **Migration Status**: Real-time migration progress
- **System Metrics**: CPU, memory, and performance monitoring
## Admin Panel Interface
### Storage Settings
- **Filesystem Configuration**: View and configure filesystem paths
- **GridFS Status**: Monitor MongoDB GridFS availability
- **S3/MinIO Configuration**: Secure S3/MinIO setup and testing
- **Connection Testing**: Validate storage backend connections
### Migration Controls
- **Batch Configuration**: Set batch size, delays, and CPU thresholds
- **Migration Actions**: Start, pause, resume, and stop migrations
- **Progress Monitoring**: Real-time progress bars and statistics
- **Log Viewing**: Live migration logs with timestamps
### Monitoring Dashboard
- **Storage Distribution**: Visual breakdown of attachment storage
- **Size Analytics**: Total and per-storage size statistics
- **Performance Metrics**: System resource usage
- **Export Capabilities**: Download monitoring data
## Configuration
### Environment Variables
#### Filesystem Storage
```bash
# Base writable path for all file storage
WRITABLE_PATH=/data
# Attachments will be stored at: ${WRITABLE_PATH}/attachments
# Avatars will be stored at: ${WRITABLE_PATH}/avatars
```
#### S3/MinIO Storage
```bash
# S3 configuration (JSON format)
S3='{"s3":{"key":"access-key","secret":"secret-key","bucket":"bucket-name","endPoint":"s3.amazonaws.com","port":443,"sslEnabled":true,"region":"us-east-1"}}'
# Alternative: S3 secret file (Docker secrets)
S3_SECRET_FILE=/run/secrets/s3_secret
```
### Migration Settings
#### Default Configuration
- **Batch Size**: 10 attachments per batch
- **Delay**: 1000ms between batches
- **CPU Threshold**: 70% maximum CPU usage
- **Auto-pause**: When CPU exceeds threshold
#### Customization
All settings can be adjusted through the admin panel:
- Batch size: 1-100 attachments
- Delay: 100-10000ms
- CPU threshold: 10-90%
## Usage
### Starting a Migration
1. **Access Admin Panel**: Navigate to Settings → Attachment Settings
2. **Configure Migration**: Set batch size, delay, and CPU threshold
3. **Select Target Storage**: Choose filesystem, GridFS, or S3
4. **Start Migration**: Click the appropriate migration button
5. **Monitor Progress**: Watch real-time progress and logs
### Migration Process
1. **Queue Initialization**: All attachments are queued for migration
2. **Batch Processing**: Attachments are processed in configurable batches
3. **CPU Monitoring**: System continuously monitors CPU usage
4. **Automatic Pausing**: Migration pauses if CPU threshold is exceeded
5. **Resume Capability**: Migration resumes when CPU usage drops
6. **Progress Tracking**: Real-time progress updates and logging
### Monitoring Migration
- **Progress Bar**: Visual progress indicator
- **Statistics**: Total, migrated, and remaining attachment counts
- **Status Display**: Current migration status (running, paused, idle)
- **Log View**: Real-time migration logs with timestamps
- **Control Buttons**: Pause, resume, and stop migration
## API Reference
### Server Methods
#### Migration Control
```javascript
// Start migration
Meteor.call('startAttachmentMigration', {
targetStorage: 'filesystem', // 'filesystem', 'gridfs', 's3'
batchSize: 10,
delayMs: 1000,
cpuThreshold: 70
});
// Pause migration
Meteor.call('pauseAttachmentMigration');
// Resume migration
Meteor.call('resumeAttachmentMigration');
// Stop migration
Meteor.call('stopAttachmentMigration');
```
#### Configuration Management
```javascript
// Get storage configuration
Meteor.call('getAttachmentStorageConfiguration');
// Test S3 connection
Meteor.call('testS3Connection', { secretKey: 'new-secret-key' });
// Save S3 settings
Meteor.call('saveS3Settings', { secretKey: 'new-secret-key' });
```
#### Monitoring
```javascript
// Get monitoring data
Meteor.call('getAttachmentMonitoringData');
// Refresh monitoring data
Meteor.call('refreshAttachmentMonitoringData');
// Export monitoring data
Meteor.call('exportAttachmentMonitoringData');
```
### Publications
#### Real-Time Updates
```javascript
// Subscribe to migration status
Meteor.subscribe('attachmentMigrationStatus');
// Subscribe to monitoring data
Meteor.subscribe('attachmentMonitoringData');
```
## Performance Considerations
### CPU Throttling
- **Automatic Monitoring**: CPU usage is checked every 5 seconds
- **Threshold-Based Pausing**: Migration pauses when CPU exceeds threshold
- **Resume Logic**: Migration resumes when CPU usage drops below threshold
- **Configurable Limits**: CPU thresholds can be adjusted (10-90%)
### Batch Processing
- **Configurable Batches**: Batch size can be adjusted (1-100)
- **Delay Control**: Delays between batches prevent system overload
- **Queue Management**: Intelligent queue processing with error handling
- **Progress Tracking**: Real-time progress updates
### Memory Management
- **Streaming Processing**: Large files are processed using streams
- **Memory Monitoring**: System memory usage is tracked
- **Garbage Collection**: Automatic cleanup of processed data
- **Error Recovery**: Robust error handling and recovery
## Security
### Access Control
- **Admin-Only Access**: All operations require admin privileges
- **User Authentication**: Proper user authentication and authorization
- **Session Management**: Secure session handling
### Data Protection
- **Password Security**: S3 secret keys are never displayed in UI
- **Environment Variables**: Sensitive data stored in environment variables
- **Secure Transmission**: All data transmission is encrypted
- **Audit Logging**: Comprehensive logging of all operations
### Configuration Security
- **Read-Only Display**: Sensitive configuration is displayed as read-only
- **Password Updates**: Only new passwords can be set, not viewed
- **Connection Testing**: Secure connection testing without exposing credentials
- **Environment Isolation**: Configuration isolated from application code
## Troubleshooting
### Common Issues
#### Migration Not Starting
- **Check Permissions**: Ensure user has admin privileges
- **Verify Configuration**: Check storage backend configuration
- **Review Logs**: Check server logs for error messages
- **Test Connections**: Verify storage backend connectivity
#### High CPU Usage
- **Reduce Batch Size**: Decrease batch size to reduce CPU load
- **Increase Delays**: Add longer delays between batches
- **Lower CPU Threshold**: Reduce CPU threshold for earlier pausing
- **Monitor System**: Check system resource usage
#### Migration Pausing Frequently
- **Check CPU Threshold**: Verify CPU threshold settings
- **Monitor System Load**: Check for other high-CPU processes
- **Adjust Settings**: Increase CPU threshold or reduce batch size
- **System Optimization**: Optimize system performance
#### Storage Connection Issues
- **Verify Credentials**: Check S3/MinIO credentials
- **Test Connectivity**: Use connection test feature
- **Check Network**: Verify network connectivity
- **Review Configuration**: Validate storage configuration
### Debug Information
#### Migration Logs
- **Real-Time Logs**: View live migration logs in admin panel
- **Server Logs**: Check server console for detailed logs
- **Error Messages**: Review error messages for specific issues
- **Progress Tracking**: Monitor migration progress and statistics
#### System Monitoring
- **CPU Usage**: Monitor CPU usage during migration
- **Memory Usage**: Track memory consumption
- **Disk I/O**: Monitor disk input/output operations
- **Network Usage**: Check network bandwidth usage
## Best Practices
### Migration Planning
- **Schedule During Low Usage**: Run migrations during low-traffic periods
- **Test First**: Test migration with small batches first
- **Monitor Resources**: Keep an eye on system resources
- **Have Backup**: Ensure data backup before migration
### Performance Optimization
- **Optimize Batch Size**: Find optimal batch size for your system
- **Adjust Delays**: Set appropriate delays between batches
- **Monitor CPU**: Set realistic CPU thresholds
- **Use Monitoring**: Regularly check monitoring data
### Security Practices
- **Regular Updates**: Keep S3 credentials updated
- **Access Control**: Limit admin access to necessary users
- **Audit Logs**: Regularly review migration logs
- **Environment Security**: Secure environment variable storage
## Future Enhancements
### Planned Features
- **Incremental Migration**: Migrate only changed attachments
- **Parallel Processing**: Support for parallel migration streams
- **Advanced Scheduling**: Time-based migration scheduling
- **Compression Support**: Built-in file compression during migration
### Integration Opportunities
- **Cloud Storage**: Additional cloud storage providers
- **CDN Integration**: Content delivery network support
- **Backup Integration**: Automated backup during migration
- **Analytics**: Advanced storage analytics and reporting
## Support
For issues and questions:
1. Check this documentation
2. Review server logs
3. Use the monitoring tools
4. Consult the Wekan community
5. Report issues with detailed information
## License
This Enhanced Attachment Migration System is part of Wekan and is licensed under the MIT License.
@@ -0,0 +1,169 @@
# Attachment Backward Compatibility
This document describes the backward compatibility implementation for Wekan attachments, allowing the system to read attachments from both the old CollectionFS structure (Wekan v6.09 and earlier) and the new Meteor-Files structure (Wekan v7.x and later).
## Overview
When Wekan migrated from CollectionFS to Meteor-Files (ostrio-files), the database structure for attachments changed significantly. This backward compatibility layer ensures that:
1. Old attachments can still be accessed and downloaded
2. No database migration is required
3. Both old and new attachments can coexist
4. The UI works seamlessly with both structures
## Database Structure Changes
### Old Structure (CollectionFS)
- **CollectionFS Files**: `cfs_gridfs.attachments.files`
- **CollectionFS Records**: `cfs.attachments.filerecord`
- **File Storage**: GridFS with bucket name `cfs_gridfs.attachments`
### New Structure (Meteor-Files)
- **Files Collection**: `attachments`
- **File Storage**: Configurable (Filesystem, GridFS, or S3)
## Implementation Details
### Files Added/Modified
1. **`models/lib/attachmentBackwardCompatibility.js`**
- Main backward compatibility layer
- Handles reading from old CollectionFS structure
- Converts old data format to new format
- Provides GridFS streaming for downloads
2. **`models/attachments.js`**
- Added backward compatibility methods to Attachments collection
- Imports compatibility functions
3. **`imports/reactiveCache.js`**
- Updated to use backward compatibility layer
- Falls back to old structure when new structure has no results
4. **`server/routes/legacyAttachments.js`**
- Handles legacy attachment downloads via `/cfs/files/attachments/:id`
- Provides proper HTTP headers and streaming
5. **`server/migrations/migrateAttachments.js`**
- Migration methods for converting old attachments to new structure
- Optional migration tools for users who want to fully migrate
### Key Functions
#### `getAttachmentWithBackwardCompatibility(attachmentId)`
- Tries new structure first, falls back to old structure
- Returns attachment data in new format
- Handles both single attachment lookups
#### `getAttachmentsWithBackwardCompatibility(query)`
- Queries both old and new structures
- Combines and deduplicates results
- Used for card attachment lists
#### `getOldAttachmentData(attachmentId)`
- Reads from old CollectionFS structure
- Converts old format to new format
- Handles file type detection and metadata
#### `getOldAttachmentStream(attachmentId)`
- Creates GridFS download stream for old attachments
- Used for file downloads
## Usage
### Automatic Fallback
The system automatically falls back to the old structure when:
- An attachment is not found in the new structure
- Querying attachments for a card returns no results
### Legacy Download URLs
Old attachment download URLs (`/cfs/files/attachments/:id`) continue to work and are handled by the legacy route.
### Migration (Optional)
Users can optionally migrate their old attachments to the new structure using the migration methods:
```javascript
// Migrate a single attachment
Meteor.call('migrateAttachment', attachmentId);
// Migrate all attachments for a card
Meteor.call('migrateCardAttachments', cardId);
// Check migration status
Meteor.call('getAttachmentMigrationStatus', cardId);
```
## Performance Considerations
1. **Query Optimization**: The system queries the new structure first, only falling back to old structure when necessary
2. **Caching**: ReactiveCache handles caching for both old and new attachments
3. **Streaming**: Large files are streamed efficiently using GridFS streams
## Error Handling
- Graceful fallback when old structure is not available
- Proper error logging for debugging
- HTTP error codes for download failures
- Permission checks for both old and new attachments
## Security
- Permission checks are maintained for both old and new attachments
- Board access rules apply to legacy attachments
- File type validation is preserved
## Testing
To test the backward compatibility:
1. Ensure you have old Wekan v6.09 data with attachments
2. Upgrade to Wekan v7.x
3. Verify that old attachments are visible in the UI
4. Test downloading old attachments
5. Verify that new attachments work normally
## Troubleshooting
### Common Issues
1. **Old attachments not showing**
- Check that old CollectionFS collections exist in database
- Verify GridFS bucket is accessible
- Check server logs for errors
2. **Download failures**
- Verify GridFS connection
- Check file permissions
- Ensure legacy route is loaded
3. **Performance issues**
- Consider migrating old attachments to new structure
- Check database indexes
- Monitor query performance
### Debug Mode
Enable debug logging by setting:
```javascript
console.log('Legacy attachments route loaded');
```
This will help identify if the backward compatibility layer is properly loaded.
## Future Considerations
- The backward compatibility layer can be removed in future versions
- Users should be encouraged to migrate old attachments
- Consider adding migration tools to the admin interface
- Monitor usage of old vs new structures
## Migration Path
For users who want to fully migrate to the new structure:
1. Use the migration methods to convert old attachments
2. Verify all attachments are working
3. Remove old CollectionFS collections (optional)
4. Update any hardcoded URLs to use new structure
The backward compatibility layer ensures that migration is optional and can be done gradually.
+2 -2
View File
@@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-7.97-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v7.97/wekan-7.97-amd64-windows.zip)
1. [wekan-8.01-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64-windows.zip)
2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe)
@@ -22,7 +22,7 @@ Right click and download files 1-4:
6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-7.97-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-8.01-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+16 -1
View File
@@ -4,5 +4,20 @@ import { TAPi18n } from './tap';
Blaze.registerHelper('_', (...args) => {
const { hash } = args.pop();
const [key] = args.splice(0, 1);
return TAPi18n.__(key, { ...hash, sprintf: args });
// If TAPi18n is not initialized yet, return the key as fallback
if (!TAPi18n.i18n) {
return key;
}
const translation = TAPi18n.__(key, { ...hash, sprintf: args });
// If translation is the same as key (meaning not found), return a formatted version
if (translation === key) {
return key.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
}
return translation;
});
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "عرض اللوحات",
"boards": "لوحات",
"board-view": "عرض اللوحات",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "التقويم",
"board-view-swimlanes": "خطوط السباحة",
"board-view-collapse": "انهيار",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "رفع صورة شخصية",
"uploaded-avatar": "تم رفع الصورة الشخصية",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Изглед на таблото",
"boards": "Табла",
"board-view": "Изглед на таблото",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Календар",
"board-view-swimlanes": "Коридори",
"board-view-collapse": "Събери",
@@ -623,6 +631,9 @@
"upload": "Качване",
"upload-avatar": "Качване на аватар",
"uploaded-avatar": "Качихте аватар",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Visió del tauler",
"boards": "Taulers",
"board-view": "Visió del tauler",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendari",
"board-view-swimlanes": "Carrils",
"board-view-collapse": "Contraure",
@@ -623,6 +631,9 @@
"upload": "Puja",
"upload-avatar": "Actualitza avatar",
"uploaded-avatar": "Avatar actualitzat",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "URL personalitzat de la imatge del logotip de la cantonada superior esquerra",
"custom-top-left-corner-logo-link-url": "URL de l'enllaç del logotip de la cantonada superior esquerra personalitzada",
"custom-top-left-corner-logo-height": "Alçada personalitzada del logotip de la cantonada superior esquerra. Per defecte: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Náhled tabla",
"boards": "Tabla",
"board-view": "Náhled tabla",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Kalendář",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Sbalit",
@@ -623,6 +631,9 @@
"upload": "Nahrát",
"upload-avatar": "Nahrát avatar",
"uploaded-avatar": "Avatar nahrán",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "URL obrázku vlastního loga v levém horním rohu",
"custom-top-left-corner-logo-link-url": "URL odkazu vlastního loga v levém horním rohu",
"custom-top-left-corner-logo-height": "Výška vlastního loga v horním levém rohu. Defaultně: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Náhled tabla",
"boards": "Tabla",
"board-view": "Náhled tabla",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Kalendář",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Sbalit",
@@ -623,6 +631,9 @@
"upload": "Nahrát",
"upload-avatar": "Nahrát avatar",
"uploaded-avatar": "Avatar nahrán",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "URL obrázku vlastního loga v levém horním rohu",
"custom-top-left-corner-logo-link-url": "URL odkazu vlastního loga v levém horním rohu",
"custom-top-left-corner-logo-height": "Výška vlastního loga v horním levém rohu. Defaultně: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Tavlevisning",
"boards": "Tavler",
"board-view": "Tavlevisning",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Kalender",
"board-view-swimlanes": "Svømmebaner",
"board-view-collapse": "Sammenfold",
@@ -623,6 +631,9 @@
"upload": "Overfør",
"upload-avatar": "Overfør en avatar",
"uploaded-avatar": "Overførte en avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Boardansicht",
"boards": "Boards",
"board-view": "Boardansicht",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Kalender",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Einklappen",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Profilbild hochladen",
"uploaded-avatar": "Profilbild hochgeladen",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Boardansicht",
"boards": "Boards",
"board-view": "Boardansicht",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Kalender",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Einklappen",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Profilbild hochladen",
"uploaded-avatar": "Profilbild hochgeladen",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Boardansicht",
"boards": "Boards",
"board-view": "Boardansicht",
"desktop-mode": "Desktop Modus",
"mobile-mode": "Mobil Modus",
"mobile-desktop-toggle": "Wechseln zwischen Mobil und Desktop Modus",
"zoom-in": "Zoom vergrößern",
"zoom-out": "Zoom verkleinern",
"click-to-change-zoom": "Klicken um die Zoom Stufe zu ändern",
"zoom-level": "Zoom Stufe",
"enter-zoom-level": "Eingabe Zoom Stufe (50-300%):",
"board-view-cal": "Kalender",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Einklappen",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Profilbild hochladen",
"uploaded-avatar": "Profilbild hochgeladen",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
+28 -17
View File
@@ -125,7 +125,7 @@
"archive": "Ins Archiv verschieben",
"archive-all": "Alles ins Archiv verschieben",
"archive-board": "Board ins Archiv verschieben",
"archive-board-confirm": "Are you sure you want to archive this board?",
"archive-board-confirm": "Wollen Sie das Board wirklich archivieren?",
"archive-card": "Karte ins Archiv verschieben",
"archive-list": "Liste ins Archiv verschieben",
"archive-swimlane": "Swimlane ins Archiv verschieben",
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Boardansicht",
"boards": "Boards",
"board-view": "Boardansicht",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Vergrößern",
"zoom-out": "Verkleinern",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Kalender",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Einklappen",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Profilbild hochladen",
"uploaded-avatar": "Profilbild hochgeladen",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Benutzerdefiniertes Logo oben links Bild URL",
"custom-top-left-corner-logo-link-url": "Benutzerdefiniertes Logo oben links Link URL",
"custom-top-left-corner-logo-height": "Benutzerdefiniertes Logo oben links Höhe. Voreinstellung: 27",
@@ -897,7 +908,7 @@
"oidc-button-text": "Benutzerdefinierter Text der OIDC-Schaltfläche",
"default-authentication-method": "Standardauthentifizierungsverfahren",
"duplicate-board": "Board duplizieren",
"duplicate-board-confirm": "Are you sure you want to duplicate this board?",
"duplicate-board-confirm": "Wollen Sie das Board wirklich duplizieren?",
"org-number": "Die Anzahl der Organisationen ist:",
"team-number": "Die Anzahl der Teams ist:",
"people-number": "Die Anzahl der Personen ist:",
@@ -1268,37 +1279,37 @@
"hideAllChecklistItems": "Verberge alle Checklisteneinträge",
"support": "Unterstützung",
"supportPopup-title": "Unterstützung",
"accessibility": "Accessibility",
"accessibility": "Bedienungshilfe",
"accessibility-page-enabled": "Barrierefreie Seite freigeschaltet",
"accessibility-info-not-added-yet": "Accessibility info has not been added yet",
"accessibility-title": "Accessibility title",
"accessibility-info-not-added-yet": "Es wurde noch keine Information zur Bedienungshilfe hinzugefügt",
"accessibility-title": "Bedienungshilfe Titel",
"accessibility-content": "Barrierefreier Eintrag",
"accounts-lockout-settings": "Brute Force Protection Settings",
"accounts-lockout-info": "These settings control how login attempts are protected against brute force attacks.",
"accounts-lockout-known-users": "Settings for known users (correct username, wrong password)",
"accounts-lockout-unknown-users": "Settings for unknown users (non-existent username)",
"accounts-lockout-failures-before": "Failures before lockout",
"accounts-lockout-period": "Lockout period (seconds)",
"accounts-lockout-failures-before": "Fehler vor einer Sperrung",
"accounts-lockout-period": "Dauer der Sperrung (Sekunden)",
"accounts-lockout-failure-window": "Failure window (seconds)",
"accounts-lockout-settings-updated": "Brute force protection settings have been updated",
"accounts-lockout-locked-users": "Locked Users",
"accounts-lockout-locked-users-info": "Users currently locked out due to too many failed login attempts",
"accounts-lockout-no-locked-users": "There are currently no locked users",
"accounts-lockout-failed-attempts": "Failed Attempts",
"accounts-lockout-remaining-time": "Remaining Time",
"accounts-lockout-user-unlocked": "User has been unlocked successfully",
"accounts-lockout-confirm-unlock": "Are you sure you want to unlock this user?",
"accounts-lockout-locked-users": "Gesperrte Benutzer",
"accounts-lockout-locked-users-info": "Kürzlich gesperrte Benutzer aufgrund von zu vielen fehlerhaften Logins",
"accounts-lockout-no-locked-users": "Es gibt aktuell keine gesperrten Benutzer",
"accounts-lockout-failed-attempts": "Fehlversuche",
"accounts-lockout-remaining-time": "Verbleibende Zeit",
"accounts-lockout-user-unlocked": "Benutzer wurde erfolgreich entsperrt",
"accounts-lockout-confirm-unlock": "Wollen Sie den Benutzer wirklich entsperren?",
"accounts-lockout-confirm-unlock-all": "Are you sure you want to unlock all locked users?",
"accounts-lockout-show-locked-users": "Show locked users only",
"accounts-lockout-user-locked": "User is locked",
"accounts-lockout-click-to-unlock": "Click to unlock this user",
"accounts-lockout-user-locked": "Benutzer ist gesperrt",
"accounts-lockout-click-to-unlock": "Klicken Sie, um den Benutzer zu entsperren",
"accounts-lockout-status": "Status",
"admin-people-filter-show": "Show:",
"admin-people-filter-all": "alle Benutzer",
"admin-people-filter-locked": "Locked Users Only",
"admin-people-filter-active": "Aktiv",
"admin-people-filter-inactive": "Not Active",
"admin-people-active-status": "Active Status",
"admin-people-active-status": "Aktiv Status",
"admin-people-user-active": "User is active - click to deactivate",
"admin-people-user-inactive": "User is inactive - click to activate",
"accounts-lockout-all-users-unlocked": "All locked users have been unlocked",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Προβολή Πίνακα",
"boards": "Πίνακες",
"board-view": "Προβολή Πίνακα",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Ημερολόγιο",
"board-view-swimlanes": "Λωρίδες",
"board-view-collapse": "Σύμπτυξη",
@@ -623,6 +631,9 @@
"upload": "Μεταφόρτωση",
"upload-avatar": "Μεταφόρτωση ενός avatar",
"uploaded-avatar": "Μεταφόρτωσε ένα avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Προβολή Πίνακα",
"boards": "Πίνακες",
"board-view": "Προβολή Πίνακα",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Ημερολόγιο",
"board-view-swimlanes": "Λωρίδες",
"board-view-collapse": "Σύμπτυξη",
@@ -623,6 +631,9 @@
"upload": "Μεταφόρτωση",
"upload-avatar": "Μεταφόρτωση ενός avatar",
"uploaded-avatar": "Μεταφόρτωσε ένα avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
+11
View File
@@ -177,6 +177,14 @@
"boardChangeViewPopup-title": "Board View",
"boards": "Boards",
"board-view": "Board View",
"desktop-mode": "Desktop Mode",
"mobile-mode": "Mobile Mode",
"mobile-desktop-toggle": "Toggle between Mobile and Desktop Mode",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"click-to-change-zoom": "Click to change zoom level",
"zoom-level": "Zoom Level",
"enter-zoom-level": "Enter zoom level (50-300%):",
"board-view-cal": "Calendar",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
@@ -623,6 +631,9 @@
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"uploading-files": "Uploading files",
"upload-failed": "Upload failed",
"upload-completed": "Upload completed",
"custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
"custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
"custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",
Loaded 100 of 273 files, more files were not shown because too many files have changed in this diff. Show more