Compare commits

..

No commits in common. "main" and "v8.14" have entirely different histories.
main ... v8.14

556 changed files with 18798 additions and 61998 deletions

View file

@ -97,8 +97,9 @@
"Avatar": true,
"Avatars": true,
"BlazeComponent": false,
"BlazeLayout": false,
"CollectionHooks": false,
"DocHead": false,
"ESSearchResults": false,
"FastRender": false,
"FlowRouter": false,

1
.github/FUNDING.yml vendored
View file

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

56
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,56 @@
## Issue
UPGRADE: https://wekan.fi/upgrade/
Pull requests welcome to fix any broken links at docs directory, and organizing docs/Features and their screenshots to subdirectories of each feature.
Please report these issues elsewhere:
- SECURITY ISSUES, PGP EMAIL: https://github.com/wekan/wekan/blob/main/SECURITY.md
- UCS: https://github.com/wekan/univention/issues
If WeKan Snap is slow, try this: https://github.com/wekan/wekan/wiki/Cron
Please search existing Open and Closed issues, most questions have already been answered.
If you can not login for any reason: https://github.com/wekan/wekan/wiki/Forgot-Password
Email settings, only SMTP MAIL_URL and MAIL_FROM are in use:
https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
### Server Setup Information
Please anonymize info, and do not any of your Wekan board URLs, passwords,
API tokens etc to this public issue.
* Did you test in newest Wekan?:
* Did you configure root-url correctly so Wekan cards open correctly (see https://github.com/wekan/wekan/wiki/Settings)?
* Operating System:
* Deployment Method (Snap/Docker/Sandstorm/bundle/source):
* Http frontend if any (Caddy, Nginx, Apache, see config examples from Wekan GitHub wiki first):
* Node.js Version:
* MongoDB Version:
* What webbrowser version are you using (Wekan should work on all modern browsers that support Javascript)?
### Problem description
Add a recorded animated gif (e.g. with https://github.com/phw/peek) about
how it works currently, and screenshot mockups how it should work.
#### Reproduction Steps
#### Logs
Check Right Click / Inspect / Console in you browser - generally Chromium
based browsers show more detailed info than Firefox based browsers.
Please anonymize logs.
Snap: sudo snap logs wekan.wekan
Docker: sudo docker logs wekan-app
If logs are very long, attach them in .zip file

View file

@ -1,69 +0,0 @@
name: 🐛 Bug Report
description: Report a bug to help improve WeKan
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug! Please ensure you are using the [latest version of WeKan](https://wekan.fi/install) and [Upgrade](https://wekan.fi/upgrade) before submitting.
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is. Mention versions of WeKan, Node.js, database name and version, frontend webserver version like Caddy etc.
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform / Installation Method
options:
- Snap Stable
- Snap Candidate
- Docker
- Sandstorm
- Source (Meteor)
- Windows
- Mac
- Other
validations:
required: true
- type: dropdown
id: CPU
attributes:
label: CPU
options:
- amd64
- arm64
- s390x
- Other
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: How can we recreate this issue?
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Logs
description: |
- Please paste any relevant anonymized server logs or browser console errors here.
- Snap: sudo snap logs wekan.wekan
- Docker: sudo docker logs wekan-app
- If logs are very long, attach them in .zip file
render: shell
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context, anonymized screenshots or GIF video about the bug, and screenshot mockups about how it should work.

View file

@ -1,31 +0,0 @@
name: ✨ Feature Request
description: Suggest a new feature for WeKan
labels: ["Feature:Request"]
body:
- type: textarea
id: feature-description
attributes:
label: Problem Statement
description: Is your feature request related to a problem? Please describe.
placeholder: I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any alternative solutions or features you've considered.
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context, like anonymized screenshot mockups about how it should work.

View file

@ -1,14 +0,0 @@
name: 🛡️ Security Issue
description: Report a security vulnerability
labels: ["security", "critical"]
body:
- type: markdown
attributes:
value: |
## ⚠️ IMPORTANT: Please do not report security vulnerabilities via public issues.
To protect the WeKan community, we ask that you report security bugs privately. This allows us to fix the issue before it can be exploited by malicious actors.
### How to report:
Please read our **[Security Policy (SECURITY.md)](https://github.com/wekan/wekan/blob/main/SECURITY.md)** for the official reporting process and contact information.

View file

@ -1,23 +0,0 @@
name: 🗳️ Univention (UCS) Issue
description: Problems specifically related to the WeKan app on Univention Corporate Server
labels: ["UCS"]
body:
- type: markdown
attributes:
value: |
## 🛑 Is this a UCS-specific issue?
If your issue is related to the **Univention Corporate Server (UCS) integration**, packaging, or installation via the Univention App Center, it should be reported in the dedicated Univention repository.
### ➡️ [Report UCS Issues Here](https://github.com/wekan/univention/issues)
---
**Why?**
Reporting there ensures that the maintainers specifically focused on the UCS environment see your request.
If you are certain this is a **core WeKan bug** that affects all platforms (Docker, Snap, etc.), please go back and use the standard [Bug Report](https://github.com/wekan/wekan/issues/new?template=bug-report.yml) template.
- type: textarea
id: ucs-details
attributes:
label: Brief Description (Optional)
description: If you still wish to post here, please provide a brief summary of why this is a core Wekan issue and not a UCS-specific integration bug.

View file

@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View file

@ -32,13 +32,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@ -48,7 +48,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

View file

@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Build the Docker image
run: docker build . --file Dockerfile --tag wekan:$(date +%s)

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0

View file

@ -18,7 +18,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: checkout
# uses: actions/checkout@v6
# uses: actions/checkout@v5
#
# - name: setup node
# uses: actions/setup-node@v1
@ -42,7 +42,7 @@ jobs:
# needs: [lintcode]
# steps:
# - name: checkout
# uses: actions/checkout@v6
# uses: actions/checkout@v5
#
# - name: setup node
# uses: actions/setup-node@v1
@ -65,7 +65,7 @@ jobs:
# needs: [lintcode,lintstyle]
# steps:
# - name: checkout
# uses: actions/checkout@v6
# uses: actions/checkout@v5
#
# - name: setup node
# uses: actions/setup-node@v1
@ -90,12 +90,12 @@ jobs:
# CHECKOUTS
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
# CACHING
- name: Install Meteor
id: cache-meteor-install
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ~/.meteor
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
@ -104,7 +104,7 @@ jobs:
- name: Cache NPM dependencies
id: cache-meteor-npm
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ~/.npm
key: v1-npm-${{ hashFiles('package-lock.json') }}
@ -113,7 +113,7 @@ jobs:
- name: Cache Meteor build
id: cache-meteor-build
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: |
.meteor/local/resolver-result-cache.json
@ -136,7 +136,7 @@ jobs:
run: sh ./test-wekan.sh -cv
- name: Upload coverage
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: coverage-folder
path: .coverage/
@ -147,10 +147,10 @@ jobs:
needs: [tests]
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Download coverage
uses: actions/download-artifact@v7
uses: actions/download-artifact@v5
with:
name: coverage-folder
path: .coverage/

View file

@ -16,13 +16,17 @@ es5-shim@4.8.0
# Collections
aldeed:collection2
reywood:publish-composite
cottz:publish-relations
dburles:collection-helpers
idmontie:migrations
easy:search
mongo@1.16.8
mquandalle:collection-mutations
# Account system
accounts-password@2.4.0
useraccounts:core
useraccounts:flow-routing
useraccounts:unstyled
simple:rest-accounts-password
wekan-ldap
@ -40,7 +44,11 @@ reactive-dict@1.3.1
session@1.2.1
tracker@1.3.3
underscore@1.0.13
arillo:flow-router-helpers
audit-argument-checks@1.0.7
kadira:dochead
mquandalle:autofocus
ongoworks:speakingurl
raix:handlebar-helpers
http@2.0.0! # force new http package
@ -50,6 +58,9 @@ http@2.0.0! # force new http package
# UI components
ostrio:i18n
reactive-var@1.0.12
mousetrap:mousetrap
mquandalle:jquery-textcomplete
mquandalle:mousetrap-bindglobal
templates:tabs
meteor-autosize
shell-server@0.5.0
@ -61,25 +72,25 @@ meteorhacks:subs-manager
meteorhacks:aggregate@1.3.0
wekan-markdown
konecty:mongo-counter
quave:synced-cron
percolate:synced-cron
ostrio:cookies
ostrio:files@2.3.0
pascoual:pdfkit
lmieulet:meteor-coverage
meteortesting:mocha@2.0.3
aldeed:simple-schema
matb33:collection-hooks
simple:json-routes
kadira:flow-router
spacebars
service-configuration@1.3.2
communitypackages:picker
minifier-css@1.6.4
blaze
kadira:blaze-layout
peerlibrary:blaze-components
ejson@1.1.3
logging@1.3.3
wekan-fullcalendar
momentjs:moment@2.29.3
wekan-fontawesome
useraccounts:flow-routing-extra
ostrio:flow-router-extra
# wekan-fontawesome

View file

@ -1 +1 @@
METEOR@2.16
METEOR@2.14

View file

@ -1,5 +1,5 @@
accounts-base@2.2.11
accounts-oauth@1.4.4
accounts-base@2.2.10
accounts-oauth@1.4.3
accounts-password@2.4.0
aldeed:collection2@2.10.0
aldeed:collection2-core@1.2.0
@ -7,6 +7,7 @@ aldeed:schema-deny@1.1.0
aldeed:schema-index@1.1.1
aldeed:simple-schema@1.5.4
allow-deny@1.1.1
arillo:flow-router-helpers@0.5.2
audit-argument-checks@1.0.7
autoupdate@1.8.0
babel-compiler@7.10.5
@ -19,25 +20,29 @@ boilerplate-generator@1.7.2
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.5.1
check@1.4.1
check@1.3.2
coffeescript@2.7.0
coffeescript-compiler@2.4.1
communitypackages:picker@1.1.1
cottz:publish-relations@2.0.8
dburles:collection-helpers@1.1.0
ddp@1.4.1
ddp-client@2.6.2
ddp-common@1.4.1
ddp-client@2.6.1
ddp-common@1.4.0
ddp-rate-limiter@1.2.1
ddp-server@2.7.1
ddp-server@2.7.0
deps@1.0.12
diff-sequence@1.1.2
dynamic-import@0.7.3
easy:search@2.2.1
easysearch:components@2.2.2
easysearch:core@2.2.2
ecmascript@0.16.8
ecmascript-runtime@0.8.1
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.3
email@2.2.6
email@2.2.5
es5-shim@4.8.0
fetch@0.1.4
geojson-utils@1.0.11
@ -46,12 +51,16 @@ html-tools@1.1.3
htmljs@1.1.1
http@2.0.0
id-map@1.1.1
idmontie:migrations@1.0.3
inter-process-messaging@0.1.1
jquery@3.0.0
kadira:blaze-layout@2.3.0
kadira:dochead@1.5.0
kadira:flow-router@2.12.1
konecty:mongo-counter@0.0.5_3
lmieulet:meteor-coverage@1.1.4
localstorage@1.2.0
logging@1.3.4
logging@1.3.3
matb33:collection-hooks@1.3.0
mdg:validation-error@0.5.1
meteor@1.11.5
@ -65,39 +74,45 @@ meteortesting:browser-tests@1.4.2
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.0.1
minifier-css@1.6.4
minifier-js@2.8.0
minifier-js@2.7.5
minifiers@1.1.8-faster-rebuild.0
minimongo@1.9.4
minimongo@1.9.3
modern-browsers@0.1.10
modules@0.20.0
modules-runtime@0.13.1
momentjs:moment@2.29.3
mongo@1.16.10
mongo@1.16.8
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
mousetrap:mousetrap@1.4.6_1
mquandalle:autofocus@1.0.0
mquandalle:collection-mutations@0.1.0
mquandalle:jade@0.4.9
mquandalle:jade-compiler@0.4.5
mquandalle:jquery-textcomplete@0.8.0_1
mquandalle:mousetrap-bindglobal@0.0.1
msavin:usercache@1.8.0
npm-mongo@4.17.2
oauth@2.2.1
oauth2@1.3.2
observe-sequence@1.0.21
ongoworks:speakingurl@1.1.0
ordered-dict@1.1.0
ostrio:cookies@2.7.2
ostrio:cstorage@4.0.1
ostrio:files@2.3.3
ostrio:flow-router-extra@3.10.1
ostrio:i18n@3.2.1
pascoual:pdfkit@1.0.7
peerlibrary:assert@0.3.0
peerlibrary:base-component@0.17.1
peerlibrary:blaze-components@0.23.0
peerlibrary:computed-field@0.10.0
peerlibrary:data-lookup@0.3.0
peerlibrary:reactive-field@0.6.0
percolate:synced-cron@1.5.2
promise@0.12.2
quave:synced-cron@2.2.1
raix:eventemitter@0.1.3
raix:handlebar-helpers@0.2.5
random@1.2.1
@ -107,9 +122,8 @@ reactive-dict@1.3.1
reactive-var@1.0.12
reload@1.3.1
retry@1.1.0
reywood:publish-composite@1.9.0
routepolicy@1.1.1
service-configuration@1.3.4
service-configuration@1.3.3
session@1.2.1
sha@1.0.9
shell-server@0.5.0
@ -130,21 +144,21 @@ templating-tools@1.2.2
tracker@1.3.3
typescript@4.9.5
ui@1.0.13
underscore@1.6.1
underscore@1.0.13
url@1.3.2
useraccounts:core@1.16.2
useraccounts:flow-routing-extra@1.1.0
useraccounts:flow-routing@1.15.0
useraccounts:unstyled@1.14.2
webapp@1.13.8
webapp@1.13.6
webapp-hashing@1.1.1
wekan-accounts-cas@0.1.0
wekan-accounts-lockout@1.1.0
wekan-accounts-lockout@1.0.0
wekan-accounts-oidc@1.0.10
wekan-accounts-sandstorm@0.8.0
wekan-fontawesome@6.4.2
wekan-fullcalendar@3.10.5
wekan-ldap@0.0.2
wekan-markdown@1.0.9
wekan-oidc@1.0.12
yasaricli:slugify@0.0.7
zodern:types@1.0.13
zimme:active-route@2.3.2
zodern:types@1.0.10

View file

@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, sl_SI: sl, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
[o:wekan:p:wekan:r:application]
file_filter = imports/i18n/data/<lang>.i18n.json

View file

@ -9,8 +9,5 @@
"TERM": "xterm-256color"
},
"terminal.integrated.shell.linux": "/bin/bash",
"terminal.integrated.shellArgs.linux": [
"-l"
],
"files.simpleDialog.enable": true
"terminal.integrated.shellArgs.linux": ["-l"]
}

View file

@ -8,548 +8,20 @@ Newest WeKan at these platforms:
- [Mac amd64, works also with Rosetta2 at Apple Silicon](https://github.com/wekan/wekan/blob/main/docs/Platforms/Propietary/Mac.md)
- https://wekan.fi/install/
- Snap Candidate amd64
- Docker amd64/arm64/s390x
- Docker amd64
- Kubernetes Docker amd64
- Bitnami MongoDB Docker images do not exist anymore. [MongoDump/MongoRestore to groundhog2k MongoDB images](https://github.com/wekan/charts/issues/45)
Fixing other platforms In Progress.
- [Node.js 14.21.4](https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4) or [Node.js 14.21.3](https://nodejs.org/dist/latest-v14.x/)
- MongoDB 6.x and 7.x, or [FerretDB2/PostgreSQL](https://github.com/wekan/wekan/blob/main/docs/Databases/FerretDB2-PostgreSQL.md)
- Node.js 14.x at https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4 and https://nodejs.org/dist/latest-v14.x/
- MongoDB 6.x and 7.x, or FerretDB/PostgreSQL https://blog.ferretdb.io/building-project-management-stack-wekan-ferretdb/
[Upgrade WeKan](https://wekan.fi/upgrade/)
WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible.
Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
WeKan 8.00-8.24 used Colorful Unicode Emoji Icons, versions before and after use mostly Font Awesome 4.7 icons.
# v8.31 2026-02-08 WeKan ® release
This release fixes the following bugs:
- [Fix Copy Card and Move Card](https://github.com/wekan/wekan/commit/f8aa487e9118264f4d96c4d0cde384bcaf05e0a0).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.30 2026-02-08 WeKan ® release
This release reverts the following new features and adds the following fixes:
- [Reverted New UI Design of WeKan v8.29 and added more fixes and performance improvements](https://github.com/wekan/wekan/commit/1b8b8d2eef5b56654026597ae445f3f20ad886b2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.29 2026-02-07 WeKan ® release
This release adds the following new features:
- New UI Design.
[Part 1](https://github.com/wekan/wekan/pull/6131),
[Part 2](https://github.com/wekan/wekan/pull/6133).
Thanks to Chostakovitch.
and fixes the following bugs:
- [Fix List widths](https://github.com/wekan/wekan/pull/6129).
Thanks to KhaoulaMaleh.
- [Fix extra space at RTL need margin](https://github.com/wekan/wekan/commit/4456bc13609b2d0e944ee71a82df200060a601b2).
Thanks to mimZD and xet7.
- [Fix No Add Card + etc](https://github.com/wekan/wekan/commit/55710835fe8879775b73c8bc921bac5febf552a2).
Thanks to mimZD and xet7.
- [Removed extra file](https://github.com/wekan/wekan/commit/0987154a7fea89b0416f48d9bffd5fa7fba9908a).
Thanks to xet7.
- [Added missing linefeeds](https://github.com/wekan/wekan/commit/0ae9865fcbad42966988225393fa66bca49cf14e).
Thanks to xet7.
- [Fix Notifications from not allowed Boards](https://github.com/wekan/wekan/commit/0a92e896f8d2cf0677891857d163ada336a45c61).
Thanks to FK-PATZ3 and xet7.
- [Fix move and copy popup duplicate view](https://github.com/wekan/wekan/commit/631c250f403172937b76ddd37bab54bc9b6dbb78).
Thanks to mimZD and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.28 2026-02-05 WeKan ® release
This release adds the following updates:
- [Bump docker/login-action from 3.6.0 to 3.7.0](https://github.com/wekan/wekan/pull/6122).
Thanks to dependabot.
- [Updated meteor-node-stubs](https://github.com/wekan/wekan/commit/6c2e2f271d6343b347224430a4eedfe54db2d838).
Thanks to Meteor developers.
and fixes the following bugs:
- [Fixed text truncation at quick-access board link bar](https://github.com/wekan/wekan/pull/6121).
Thanks to KhaoulaMaleh.
- [Improved cardDetails.css for better UI](https://github.com/wekan/wekan/pull/6124).
Thanks to AymenHassini19.
- [Fixed Jade syntax at header](https://github.com/wekan/wekan/commit/c31758960f5372e88f47e8d081404294751284c8).
Thanks to xet7.
- [Await async setDone before closing popup in copy/move dialogs](https://github.com/wekan/wekan/pull/6126).
Thanks to harryadel.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.27 2026-01-31 WeKan ® release
This release adds the following updates:
- [Updated MongoDB to 7.0.29 at Windows install docs](https://github.com/wekan/wekan/commit/b55e1bbd409f76bd0388d19d4d0a8420cee8df96).
Thanks to MongoDB developers.
and fixes the following bugs:
- [Fix async/await in copy/move card operations](https://github.com/wekan/wekan/pull/6120).
Thanks to harryadel.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.26 2026-01-31 WeKan ® release
This release adds the following updates:
- [Migrate wekan-accounts-lockout to async API for Meteor 3.0](https://github.com/wekan/wekan/pull/6113).
Thanks to harryadel.
- Added Docs: Spreadsheet vs Kanban.
[Part 1](https://github.com/wekan/wekan/commit/a0a8d0186cbc7fefe38f72244723bcff292ae2f4),
[Part 2](https://github.com/wekan/wekan/commit/37d0daee590ab48cbfa1672e4bc5efd95d341211).
Thanks to xet7.
- [Updated dependencies](https://github.com/wekan/wekan/commit/03439d1bccf82511870eed7301b621b1d495941b).
Thanks to developers of dependencies.
and fixes the following bugs:
- [Reduce visual overflow in Member Settings menu by extending container height](https://github.com/wekan/wekan/pull/6104).
Thanks to AymenHassini19.
- [Fix Card copy menu is not displayed](https://github.com/wekan/wekan/commit/0b891464b907b272e075d8aafd3ce29e704739cf).
Thanks to xet7.
- [Fix Bug: Rules view translation not is not shown correctly](https://github.com/wekan/wekan/commit/f73eab23f997efe5347aa1f06515bf355cfe7ed5).
Thanks to cactus7as and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.25 2026-01-28 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Floppybleed](https://wekan.fi/hall-of-fame/floppybleed/):
- [Fix Filebleed of Floppybleed](https://github.com/wekan/wekan/commit/a419d831a408f251c798f5410375b20afd98c04b).
Thanks to Luke Hebenstreit Twitter lheben_ and xet7.
and adds the following updates:
- [Updated code counts](https://github.com/wekan/wekan/commit/2f25f47d0ba4c7f543264cd7fe2ed117ab0ec9ee).
Thanks to xet7.
- Updated FerretDB 2 / PostgreSQL docs location.
[Part 1](https://github.com/wekan/wekan/commit/710d522e069b7521b6c2ec4f93f1491a897cf2b4),
[Part 2](https://github.com/wekan/wekan/commit/0ede9d6d93a688f24fc36c0c456e184a0aa6af8c),
[Part 3](https://github.com/wekan/wekan/commit/bf5d50e8a9fce327a16b069932fa3e13c6d81978).
Thanks to xet7.
- [Updated Dockerfile](https://github.com/wekan/wekan/commit/d298ab7486d489d353fc410232a9dcdd68501c72).
Thanks to xet7.
- Docker for Linux amd64/arm64/s390x.
[Part 1](https://github.com/wekan/wekan/commit/38711f0a29bf37d1e0a3fd9c8a9bcfb2442934b3),
[Part 2](https://github.com/wekan/wekan/commit/e72019fa55ef6142767fd83e928bf2a0a966f9e6),
[Part 3](https://github.com/wekan/wekan/commit/b2c7c7f55b5136bc91251cd57125316ec622d4a3),
[Part 4](https://github.com/wekan/wekan/commit/98e5adfba80ee935b2a1293851d88812ad707b78),
[Part 5](https://github.com/wekan/wekan/commit/60846a44959d46262672c6a3048bd76d829c03bf),
[Part 6](https://github.com/wekan/wekan/commit/7ff174cf660f43dfbb471b29d75820f527771bbd).
Thanks to xet7.
- [Most Unicode Icons back to Font Awesome 4.7 for better accessibility. Less always visible buttons, More at ☰ Menu](https://github.com/wekan/wekan/commit/7ad04f45353e1628881fec310caedf7625a34d4d).
Thanks to xet7.
- [Updated to MongoDB 7.0.29 at Snap Candidate](https://github.com/wekan/wekan/commit/ac70fe28488c09364133a65fbc80f5a819a1e4bf).
Thanks to developers of MongoDB.
- [Updated to MongoDB 7.0.29 at Helm Charts](https://github.com/wekan/charts/commit/8169739260b6f104c4d011dac5a4bf5485db8b45).
Thanks to developers of MongoDB.
and fixes the following bugs:
- [Fix autofocus](https://github.com/wekan/wekan/commit/440f553de0baf460acc891ee5864f84bb982104a).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.24 2026-01-24 WeKan ® release
This release adds the following updates:
- Secure Sandbox for VSCode at Debian 13 amd64.
[Part 1](https://github.com/wekan/wekan/commit/639ac9549f88069d8569de777c533ab4c9438088),
[Part 2](https://github.com/wekan/wekan/commit/cc8b771eb448199fa23a87955cf9fa1a504ba8d2).
Thanks to xet7.
- [Updated build scripts and docs to Meteor 2.16](https://github.com/wekan/wekan/commit/1d374db0f3ed35a0463b5f89ca2d01078e245d11).
Thanks to xet7.
- [Replace mquandalle:collection-mutations with collection helpers](https://github.com/wekan/wekan/pull/6086).
Thanks to harryadel.
- [Replace ongoworks:speakingurl with limax](https://github.com/wekan/wekan/pull/6087).
Thanks to harryadel.
- [Migrate createIndex to createIndexAsync](https://github.com/wekan/wekan/pull/6093).
Thanks to harryadel.
- [Remove idmontie:migrations](https://github.com/wekan/wekan/pull/6095).
Thanks to harryadel.
- Remove mquandalle:autofocus.
[Part 1](https://github.com/wekan/wekan/pull/6088),
[Part 2](https://github.com/wekan/wekan/pull/6096).
Thanks to harryadel.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.23 2026-01-21 WeKan ® release
This release adds the following updates:
- [Migrate from percolate:synced-cron to quave:synced-cron](https://github.com/wekan/wekan/pull/6080).
Thanks to harryadel.
- [Replace mousetrap](https://github.com/wekan/wekan/pull/6082).
Thanks to harryadel.
- [Remove kadira:dochead](https://github.com/wekan/wekan/pull/6083).
Thanks to harryadel.
- [Replace cottz:publish-relations with reywood:publish-composite](https://github.com/wekan/wekan/pull/6084).
Thanks to harryadel.
- [Bump tar from 7.5.3 to 7.5.6](https://github.com/wekan/wekan/pull/6085).
Thanks to dependabot.
- [Updated dependencies](https://github.com/wekan/wekan/commit/04bfa0e6ba278a9d6544a678d1fba3ea71841062).
Thanks to developers of dependencies.
and fixes the following bugs:
- [Fixed newly created "Default" swimlane are displayed as "key 'default (LOCALE)' returned an object instead of string"](https://github.com/wekan/wekan/commit/ce55f0d8f432922ca4c0e3d28b1fb0e826d8008f).
Thanks to brlin-tw and xet7.
- [Fix DB migration from 8.19 to 8.21 stuck forever](https://github.com/wekan/wekan/commit/a31a615da6911a2db22d4db86875b31fc951ae96).
Thanks to MaccabeeY and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.22 2026-01-20 WeKan ® release
This release fixes the following bugs:
- [Fixed Add member and @mentions](https://github.com/wekan/wekan/commit/ad511bd1378afdca7264597900a11ab6b5e09b77).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.21 2026-01-18 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
- [Security Fix 2: OrgsTeamsBleed](https://github.com/wekan/wekan/commit/cabfeed9a68e21c469bf206d8655941444b9912c).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 3: ChecklistRESTBleed](https://github.com/wekan/wekan/commit/251d49eea94834cf351bb395808f4a56fb4dbb44).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 4: MigrationsBleed2](https://github.com/wekan/wekan/commit/cc35dafef57ef6e44a514a523f9a8d891e74ad8f).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 5: PositionHistoryBleed](https://github.com/wekan/wekan/commit/55576ec17722db094835470b386162c9a662fb60).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 6: SyncLDAPBleed](https://github.com/wekan/wekan/commit/146905a459106b5d00b4f09453a6554255e6965a).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 7: AttachmentMigrationBleed](https://github.com/wekan/wekan/commit/053bf1dfb76ef230db162c64a6ed50ebedf67eee).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 8: MoveStorageBleed](https://github.com/wekan/wekan/commit/c413a7e860bc4d93fe2adcf82516228570bf382d).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 9: ListWIPBleed](https://github.com/wekan/wekan/commit/8c0b4f79d8582932528ec2fdf2a4487c86770fb9).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 10: BoardTitleRESTBleed](https://github.com/wekan/wekan/commit/545566f5663545d16174e0f2399f231aa693ab6e).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 11: CardPubSubBleed](https://github.com/wekan/wekan/commit/0f5a9c38778ca550cbab6c5093470e1e90cb837f).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 12: FixDuplicateBleed](https://github.com/wekan/wekan/commit/4ce181d17249778094f73d21515f7f863f554743).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 13: LinkedBoardActivitiesBleed](https://github.com/wekan/wekan/commit/91a936e07d2976d4246dfe834281c3aaa87f9503).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 14: RulesBleed](https://github.com/wekan/wekan/commit/a787bcddf33ca28afb13ff5ea9a4cb92dceac005).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
and adds the following new features:
- [Show password at Login and Register pages](https://github.com/wekan/wekan/commit/d30192f7f925a055e6f31723c47ad32b628ff2c0).
Thanks to xet7.
and adds the following updates:
- [Updated Docker build command](https://github.com/wekan/wekan/commit/b88b27689af8c5abf23dd7891780581a2d92001d).
Thanks to xet7.
- [Updated Windows Bundle build .bat script](https://github.com/wekan/wekan/commit/f0118d52e984628b0e06e36d7b7f90166d18fbf7).
Thanks to xet7.
- [Updated Linux arm64 bundle build script](https://github.com/wekan/wekan/commit/e2ec50730ff7fd4eb805071bb17fe0c105514f83).
Thanks to xet7.
- [Updated Linux s390x bundle build script](https://github.com/wekan/wekan/commit/980510d71ad428325645dd53297f4ce20bd12983).
Thanks to xet7.
- [Bump tar and @mapbox/node-pre-gyp](https://github.com/wekan/wekan/pull/6071).
Thanks to dependabot.
- [Upgrade to Meteor 2.16](https://github.com/wekan/wekan/pull/6072).
Thanks to harryadel.
- [Updated dependencies](https://github.com/wekan/wekan/commit/95da8966fe3bebc7c5ef2c1fc555de5fa239f8ca).
Thanks to developers of dependencies.
and fixes the following bugs:
- [Fixed "Copy card link to clipboard" icon often not working](https://github.com/wekan/wekan/commit/d337afd5d3e8ca719adcde13d2b24d983e0f9926).
Thanks to brlin-tw and xet7.
- [Fix DB migration from 8.19 to 8.20 is in a loop](https://github.com/wekan/wekan/commit/2fa490d83da858b193ca6a363e1599c5bbe55640).
Thanks to MaccabeeY and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.20 2026-01-16 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
- [Security Fix 1: MigrationsBleed](https://github.com/wekan/wekan/commit/cbb1cd78de3e40264a5e047ace0ce27f8635b4e6).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
and adds the following features:
- [Added back feature: Toggle Drag Handles. Improved positions of Add List etc buttons](https://github.com/wekan/wekan/commit/5cb712bee4cf46c6fe13d7dacf4b62298152b894).
Thanks to xet7.
and adds the following updates:
- [Updated dependencies](https://github.com/wekan/wekan/pull/6059).
Thanks to dependabot.
- [Updated dependencies and published as @wekanteam npm packages to npmjs.com](https://github.com/wekan/wekan/commit/a9a89b501a91ffcdbdd611a05029d9483c59e4db).
Thanks to xet7.
- Added FerretDB2/PostgreSQL Docs.
[Part 1](https://github.com/wekan/wekan/commit/9fb1aeb8272b011c3d0b6b2c26ff7cb498c7b37f),
[Part 2](https://github.com/wekan/wekan/commit/f198421f10dd3be9d58f64a242d12ea1ef45fee3),
[Part 3](https://github.com/wekan/wekan/commit/9431b2d53014289bebb06567f5662fdcb6dd409c),
[Part 4](https://github.com/wekan/wekan/commit/ffd37b9fd9171ca22973d6d0a62baef4a18494f5).
Thanks to juri_ at WeKan Libera.Chat IRC and xet7.
- [Added s390x firewall Docs](https://github.com/wekan/wekan/commit/ec7c0e6dc3641f43b1a110d285f6ef15c146584a).
Thanks to xet7.
- Updated GitHub issue templates.
[Part 1](https://github.com/wekan/wekan/commit/bd37b88e4d508c1f2712184a27dbbfd9df0e4c4e),
[Part 2](https://github.com/wekan/wekan/commit/cf6e6914989a7bf1d79f8b753a0a576c54ad7580),
[Part 3](https://github.com/wekan/wekan/commit/4a658dc02a770f8219669dc10bfe1077c760744f).
Thanks to xet7.
- [Migrate kadira:flow-router to ostrio:flow-router-extra](https://github.com/wekan/wekan/pull/6067), related to Meteor 3 upgrades.
Thanks to harryadel.
- [Some fixes to make WeKan working after Meteor 3 related router upgrades](https://github.com/wekan/wekan/commit/984a2dcec18fd20ebd1a5add8380d4c13d8303ba).
Thanks to xet7.
and fixes the following bugs:
- [Fix attachment download error with non-ASCII filenames](https://github.com/wekan/wekan/pull/6056).
Thanks to brlin-tw.
- [Swimlane drag button position improvements](https://github.com/wekan/wekan/commit/376a30f8a9c5cc6b5341fda7336244ee1b9983fd).
Thanks to TDSCDMA and xet7.
- [Removed extra list borders](https://github.com/wekan/wekan/commit/a4f8faa48e3fb6c617cf9c5a398bc7f85b8bae92).
Thanks to TDSCDMA and xet7.
- [Add back button texts to Filter, Search, Board View and MultiSelection](https://github.com/wekan/wekan/commit/dac7e17500de97febc7ad8f84cd1bf5edab27c52).
Thanks to audiocrush and xet7.
- [Removed extra pipe character from UI](https://github.com/wekan/wekan/commit/66e79d2df7ecf5526dbae360cf93352657db7fcf).
Thanks to xet7.
- [Changed find.sh to not search from translations, because I'm trying to find code, not translations](https://github.com/wekan/wekan/commit/58ae2b6c6848235132308611fe3083533e120f72).
Thanks to xet7.
- [Fixed Change Avatar. Improved Admin Panel: People columns order, selected tab background color. Fixed can not edit existing user at Admin Panel/People/People](https://github.com/wekan/wekan/commit/07186e12a93c56555feb3b7332d43a918abe7f20).
Thanks to xet7.
- [Fix mentions and notifications drawer](https://github.com/wekan/wekan/commit/20b5e2ab8fd37303cda8305d87d757c1cb9bdd12).
Thanks to xet7.
- Fix New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly.
[Part 1](https://github.com/wekan/wekan/commit/eabb6a239d20530f538d22f94d9cfbebeb847493).
Thanks to nazim-oss and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.19 2025-12-29 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Megableed](https://wekan.fi/hall-of-fame/megableed/):
- [Security Fix 1: IDOR in setCreateTranslation. Non-admin could change Custom Translation](https://github.com/wekan/wekan/commit/f244a43771f6ebf40218b83b9f46dba6b940d7de).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 2: Private-only board setting can be bypassed](https://github.com/wekan/wekan/commit/7ed76c180ede46ab1dac6b8ad27e9128a272c2c8).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 3: Card comment author spoofing (IDOR) via API](https://github.com/wekan/wekan/commit/67cb47173c1a152d9eaf5469740992b2dacdf62d).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 4: Cross-board card move without destination authorization](https://github.com/wekan/wekan/commit/198509e7600981400353aec6259247b3c04e043e).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 5: Read-only roles can still update cards](https://github.com/wekan/wekan/commit/181f837d8cbae96bdf9dcbd31beaa3653c2c0285).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 6: Checklist delete IDOR: checklist not verified against board/card](https://github.com/wekan/wekan/commit/08a6f084eba09487743a7c807fb4a9000fcfa9ac).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 7: Checklist create IDOR: cardId not verified against boardId](https://github.com/wekan/wekan/commit/5cd875813fdec5a3c40a0358b30a347967c85c14).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 8: Attachments publication leaks metadata without auth](https://github.com/wekan/wekan/commit/6dfa3beb2b6ab23438d0f4395b84bf0749eb4820).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 9: Attachment upload not scoped to card/board relationship](https://github.com/wekan/wekan/commit/1d16955b6d4f0a0282e89c2c1b0415c7597019b8).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
- [Security Fix 10: LDAP filter injection in LDAP auth](https://github.com/wekan/wekan/commit/0b0e16c3eae28bbf453d33a81a9c58ce7db6d5bb).
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
and adds the following new features:
- [Opened card Checklist menu: Hide finished tasks. Show Checklist at Minicard](https://github.com/wekan/wekan/commit/fbfde81bc8208b718c070a6eeba4b2e2d2ce83ba).
Thanks to C0rn3j and xet7.
and adds the following updates:
- [Helm Chart: Updated MongoDB to 7.0.28 at artifacthub.io](https://github.com/wekan/charts/commit/5e6d344e0b976ce683116b66a1fb8417590115aa).
Thanks to xet7 and titver968.
and fixes the following bugs:
- [Re-add JS closing class to unicode close announcement symbol](https://github.com/wekan/wekan/pull/6050).
Thanks to Chostakovitch.
- [Cannot re-arrange lists within swimlanes](https://github.com/wekan/wekan/pull/6052).
Thanks to Chostakovitch.
- Converted Gantt from js to Jade, and made card title to render markdown at Gantt view.
[Part 1](https://github.com/wekan/wekan/commit/2d3bef9033134c3b62cf22179bbee4b6fea81444),
[Part 2](https://github.com/wekan/wekan/commit/3af3c9a89d8a4020b6f1ccada7da2ccbec1a8562).
Thanks to xet7.
- [Fix find.sh work with spaces, for example: ./find.sh "Some text"](https://github.com/wekan/wekan/commit/db4b04d8377523440fd2c36c1633ee74d7b05146).
Thanks to xet7.
- [Fix copy move card at board and MultiSelect to have numbered target of board, card above or below. Added MultiSelect change color](https://github.com/wekan/wekan/commit/74f1dfde72b9448645552ae28ba8d989d3e823d8).
Thanks to mimZD and xet7.
- [Fix move card last selection is gone](https://github.com/wekan/wekan/commit/2d87ba18b31ab5d8dc91dce01199cf7b313bd560).
Thanks to mimZD and xet7.
- [Fix Unable to delete Checklist. Added confirm delete to Checklist and Chekclist Item](https://github.com/wekan/wekan/commit/cf62807ad5d056ce9b8045c55f7cf6c29044967b).
Thanks to C0rn3j and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.18 2025-12-28 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES:
- [Upgraded MongoDB to 7.0.28 to fix mongobleed at Snap Candidate](https://github.com/wekan/wekan/commit/e210c9973be55a4fa4e7dd15aefc24e06dbc3e7f).
Thanks to developers of MongoDB.
and adds the following new features:
- [Gantt chart view to one board view menu Swimlanes/Lists/Calendar/Gantt](https://github.com/wekan/wekan/commit/f34e4c0e363e386dbcce8e6ee8933b2d50491c58).
Thanks to xet7.
- [Number of cards per list and sum of custom number field in list head](https://github.com/wekan/wekan/commit/e569c2957ecc2b5fbf65ddcf0793b97c3ed5da81).
Thanks to xet7.
- [New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly](https://github.com/wekan/wekan/commit/c1168d181b3ff34f5ee7794a5740281c4ab5e253).
Thanks to xet7.
- [More translations. Added support page to Admin Panel / Settings / Layout](https://github.com/wekan/wekan/commit/a7400dca4503961267cc5fd6a1c8efaa78668f77).
Thanks to xet7.
- [Right top User Settings / Grey Icons. Also fixed Change Language popup](https://github.com/wekan/wekan/commit/300b653ea3416892faf2d08f5e0be3752e2041d6).
Thanks to xet7.
- [Collapse Swimlane, List, Opened Card. Opened Card window X and Y position can be moved freely from drag handle. Fix some dragging not possible. Fix iPhone Safari](https://github.com/wekan/wekan/commit/58f4884ad603e4f8c68a8819dfb1440234da70b6).
Thanks to xet7.
- Per-User and Board-level data save fixes. Per-User is collapse, width, height. Per-Board is Swimlanes, Lists, Cards etc.
[Part 1](https://github.com/wekan/wekan/commit/414b8dbf41ecf368d54aeceb6a78ccd0aa58f6a6),
[Part 2](https://github.com/wekan/wekan/commit/58e970d68508a76a1b9333941eb1696fb8fb7727).
Thanks to xet7.
and adds the following updates:
- [Update GitHub docker/metadata-action from 5.8.0 to 5.9.0](https://github.com/wekan/wekan/pull/6012).
Thanks to dependabot.
- [Updated security.md](https://github.com/wekan/wekan/commit/7ff1649d8909917cae590c68def6eecac0442f91).
Thanks to xet7.
- [Updated build script for Linux arm64 bundle](https://github.com/wekan/wekan/commit/3db1305e58168f7417023ccd8d54995026844b18).
Thanks to xet7.
- Update Backup docs about migrating to newest WeKan.
[Part 1](https://github.com/wekan/wekan/commit/e669b1b9c72278c8debbc9de74d3fa02224a66d8),
[Part 2](https://github.com/wekan/wekan/commit/19fa12bb26a0444acffd49f24123ed993c425f6a),
[Part 3](https://github.com/wekan/wekan/commit/4e346c0ab7fbfb39544063cbd0e095307b26648f),
[Part 4](https://github.com/wekan/wekan/commit/59fc756a0bda8e11b9d86961daa35bb755110a68),
[Part 5](https://github.com/wekan/wekan/commit/30541260f0f979662889bc40b4db461af1583a07),
[Part 6](https://github.com/wekan/wekan/commit/784c5c6b0c83397ab4344d1a0fa231f33ff26564),
[Part 7](https://github.com/wekan/wekan/commit/5686c92e05452a5d91c10ed436fae71103ecfb1f),
[Part 8](https://github.com/wekan/wekan/commit/b7ff370561153bbfbb07426f9bd8b4d2977b1d0c),
[Part 9](https://github.com/wekan/wekan/commit/fe4b36b85d4ac8efddb2c7148bc5d2413cd643e1),
[Part 10](https://github.com/wekan/wekan/commit/9ebdc82d46d86029df12adaafba95c0ecfc9d2c2),
[Part 11](https://github.com/wekan/wekan/commit/3ef0a3e685657eba1cc07314ac8d195f89dbef74),
[Part 12](https://github.com/wekan/wekan/commit/2cbf64da33aff2d0b77ee91e7e9ac360cd1edb99),
[Part 13](https://github.com/wekan/wekan/commit/3c578403404084ae10e4349b5570b0d50ecd8eb4),
[Part 14](https://github.com/wekan/wekan/commit/451e9f78705dbbac2ed6ce123fd5440a871b6dcc),
[Part 15](https://github.com/wekan/wekan/commit/e07e461e482f54c8ddaebc63373c93dc4aa0d956).
and fixes the following bugs:
- [Fix Broken Strikethroughs in Markdown to HTML conversion](https://github.com/wekan/wekan/pull/6009).
Thanks to brlin-tw.
- [Updated Mac docs for Applite](https://github.com/wekan/wekan/commit/400eb81206f346a973d871a8aaa55d4ac5d48753).
Thanks to xet7.
- [Fix checklist delete action (issue #6020), link-card popup defaults, and stabilize due-cards ordering](https://github.com/wekan/wekan/pull/5967).
Thanks to seve12.
- [Improve rules UI board dropdowns/loading, rule header titles, and ensure card move updates attachment metadata](https://github.com/wekan/wekan/pull/5967).
Thanks to seve12.
- [Improve imports: normalize id → _id, add default swimlane fallback, and add regression test](https://github.com/wekan/wekan/pull/5967).
Thanks to seve12.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.17 2025-11-06 WeKan ® release
This release adds the following new feature:
- [Feature: Workspaces, at All Boards page](https://github.com/wekan/wekan/commit/0afbdc95b49537e06b4f9cf98f51a669ef249384).
Thanks to xet7.
and fixes the following bugs:
- [Fix 8.16: Switching Board View fails with 403 error](https://github.com/wekan/wekan/commit/550d87ac6cb3ec946600616485afdbd242983ab4).
Thanks to xet7.
- [Moved migrations from opening board to right sidebar / Migrations](https://github.com/wekan/wekan/commit/1b25d1d5720d4f486a10d2acce37e315cf9b6057).
Thanks to xet7.
- [Fix 8.16 Lists with no items are deleted every time when board is opened. Moved migrations to right sidebar](https://github.com/wekan/wekan/commit/7713e613b431e44dc13cee72e7a1e5f031473fa6).
Thanks to xet7.
- [Remove old translations and code not in use anymore](https://github.com/wekan/wekan/commit/ba49d4d140bc0d4cfb5a96db9ab077bc85db58f1).
Thanks to xet7.
- [Fixed sidebar migrations to be per-board, not global. Clarified translations](https://github.com/wekan/wekan/commit/e4638d5fbcbe004ac393462331805cac3ba25097).
Thanks to xet7.
- [Fix star board](https://github.com/wekan/wekan/commit/8711b476be30496b96b845529b5717bb6e685c27).
Thanks to xet7.
- [Fix Card emoji issues](https://github.com/wekan/wekan/commit/e5e711c938edcca23c974c3eec97296898bcf24e).
Thanks to xet7.
- [Try to fix Edit Custom Fields button not working. Removed duplicate option from Boards Settings](https://github.com/wekan/wekan/commit/20af0a2ef55b11e7205845859ee92a929616ce91).
Thanks to xet7.
- [Fix Regression - calendar popup to set due date has gone](https://github.com/wekan/wekan/commit/581733d605b7e0494e72229c45947cff134f6dd6).
Thanks to xet7.
- [Remove not working Bookmark menu option](https://github.com/wekan/wekan/commit/c829c073cf822e48b7cd84bbfb79d42867412517).
Thanks to xet7.
- [Fix Workspaces at All Boards to have correct count of remaining etc, while starred also at Starred/Favorites](https://github.com/wekan/wekan/commit/6244657ca53a54646ec01e702851a51d89bd0d55).
Thanks to xet7.
- [Fix Worker Permissions does not allow for cards to be moved. - v8.15. Removed buttons Worker should not use](https://github.com/wekan/wekan/commit/18003900c2d497c129793d1653d4d9872a2f19da).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.16 2025-11-02 WeKan ® release
This release fixes the following CRITICAL SECURITY ISSUES of [Spacebleed](https://wekan.fi/hall-of-fame/spacebleed/):
- [Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High)](https://github.com/wekan/wekan/commit/e9a727301d7b4f1689a703503df668c0f4f4cab8).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 2: Access to boards of any Orgs/Teams, and avatar permissions](https://github.com/wekan/wekan/commit/f26d58201855e861bab1cd1fda4d62c664efdb81).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort](https://github.com/wekan/wekan/commit/ea310d7508b344512e5de0dfbc9bdfd38145c5c5).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 4: Members can forge others votes (Low). Bonus: Similar fixes to planning poker too done by xet7](https://github.com/wekan/wekan/commit/0a1a075f3153e71d9a858576f1c68d2925230d9c).
Thanks to Siam Thanat Hack (STH) and xet7.
- [Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low)](https://github.com/wekan/wekan/commit/ccd90343394f433b287733ad0a33c08e0a71f53c).
Thanks to Siam Thanat Hack (STH) and xet7.
and adds the following new features:
- [List menu / More / Delete duplicate lists that do not have any cards](https://github.com/wekan/wekan/commit/91b846e2cdee9154b045d11b4b4c1a7ae1d79016).
Thanks to xet7.
- [Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane](https://github.com/wekan/wekan/commit/034dc08269520ca31c780cce64e0150969e9228e).
Thanks to xet7.
and fixes the following bugs:
- [Fix changing swimlane color to not reload webpage](https://github.com/wekan/wekan/commit/ecf2418347cae4329deb292b534f68eb099d3f90).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.15 2025-10-23 WeKan ® release
This release fixes the following bugs:
- Fix drag lists did not work
[Part 1](https://github.com/wekan/wekan/commit/8662c96d1c8d4fa76ce7b31eb06678ad59c3ebe1),
[Part 2](https://github.com/wekan/wekan/commit/0cebd8aa4dbe0bf2418b814716744ab806b671c2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.14 2025-10-23 WeKan ® release
This release fixes the following bugs:

View file

@ -4,23 +4,28 @@ LABEL org.opencontainers.image.ref.name="ubuntu"
LABEL org.opencontainers.image.version="24.04"
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH is automatically provided by Docker Buildx
ARG TARGETARCH
# 2022-04-25:
# - gyp does not yet work with Ubuntu 22.04 ubuntu:rolling,
# so changing to 21.10. https://github.com/wekan/wekan/issues/4488
# 2021-09-18:
# - Above Ubuntu base image copied from Docker Hub ubuntu:hirsute-20210825
# to Quay to avoid Docker Hub rate limits.
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
ENV BUILD_DEPS="apt-utils gnupg gosu wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
ENV \
DEBUG=false \
NODE_VERSION=v14.21.4 \
METEOR_RELEASE=METEOR@2.16 \
METEOR_RELEASE=METEOR@2.14 \
USE_EDGE=false \
METEOR_EDGE=1.5-beta.17 \
NPM_VERSION=6.14.17 \
FIBERS_VERSION=4.0.1 \
ARCHITECTURE=linux-x64 \
SRC_PATH=./ \
WITH_API=true \
MONGO_OPLOG_URL="" \
RESULTS_PER_PAGE="" \
DEFAULT_BOARD_ID="" \
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
@ -158,69 +163,134 @@ ENV \
MONGO_PASSWORD_FILE="" \
S3_SECRET_FILE=""
# NODE_OPTIONS="--max_old_space_size=4096"
#---------------------------------------------
# == at docker-compose.yml: AUTOLOGIN WITH OIDC/OAUTH2 ====
# https://github.com/wekan/wekan/wiki/autologin
#- OIDC_REDIRECTION_ENABLED=true
#---------------------------------------------------------------------
# Copy the app to the image
#COPY ${SRC_PATH} /home/wekan/app
# Install OS
RUN <<EOR
set -o xtrace
# Create Wekan user
# Add non-root user wekan
useradd --user-group --system --home-dir /home/wekan wekan
# OS Updates
# OS dependencies
apt-get update --assume-yes
apt-get upgrade --assume-yes
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
# Multi-arch mapping logic
case "${TARGETARCH}" in
"amd64") NODE_ARCH="x64" WEKAN_ARCH="amd64" ;;
"arm64") NODE_ARCH="arm64" WEKAN_ARCH="arm64" ;;
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
*) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;;
esac
# Node.js Installation
cd /tmp
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt | shasum -a 256 -c -
tar xzf "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
# NPM configuration
npm install -g npm@${NPM_VERSION} --production
chown --recursive wekan:wekan /home/wekan/
# Temporary Tar swap for Meteor bundle
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
cp $(which tar) $(which tar)~
ln -sf $(which bsdtar) $(which tar)
# WeKan Bundle Installation
# Install NodeJS
cd /tmp
# Download nodejs
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz"
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/node-v14.21.4-linux-x64.tar.gz"
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/SHASUMS256.txt"
# Verify nodejs authenticity
#grep "node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
grep "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
rm -f "SHASUMS256.txt"
# Install Node
#tar xzf "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
tar xzf "node-v14.21.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
#rm "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" "SHASUMS256.txt"
rm "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt"
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/${NODE_VERSION} /home/wekan/.config"
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/v14.21.4 /home/wekan/.config"
# Install node dependencies
#npm install -g npm@${NPM_VERSION} --production
npm install -g npm@$6.14.17 --production
chown --recursive wekan:wekan /home/wekan/.config
# Install Meteor
cd /home/wekan
chown --recursive wekan:wekan /home/wekan
echo "Starting meteor ${METEOR_RELEASE} installation... \n"
#gosu wekan:wekan curl https://install.meteor.com/ | /bin/sh
# Specify Meteor version 2.14 to be compatible: https://github.com/wekan/wekan/pull/5816/files
#gosu wekan:wekan npm -g install meteor@2.14 --unsafe-perm
#mv /root/.meteor /home/wekan/
#chown --recursive wekan:wekan /home/wekan/.meteor
#sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js
#cd /home/wekan/.meteor
#gosu wekan:wekan /home/wekan/.meteor/meteor -- help
# Build app (Production)
#cd /home/wekan/app
mkdir -p /home/wekan/app
cd /home/wekan/app
wget "https://github.com/wekan/wekan/releases/download/v8.31/wekan-8.31-${WEKAN_ARCH}.zip"
unzip "wekan-8.31-${WEKAN_ARCH}.zip"
rm "wekan-8.31-${WEKAN_ARCH}.zip"
#mkdir -p /home/wekan/.npm
#chown --recursive wekan:wekan /home/wekan/.npm
#chmod u+w *.json
#gosu wekan:wekan meteor npm install --production
#gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build
#cd /home/wekan/app_build/bundle/programs/server/
#chmod u+w *.json
#gosu wekan:wekan meteor npm install --production
#cd node_modules/fibers
#node build.js
#cd ../..
# 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/v8.14/wekan-8.14-amd64.zip"
unzip wekan-8.14-amd64.zip
rm wekan-8.14-amd64.zip
mv /home/wekan/app/bundle /build
# Restore original tar
# Put back the original tar
mv $(which tar)~ $(which tar)
# Cleanup
apt-get remove --purge --assume-yes ${BUILD_DEPS}
#npm uninstall -g api2html
apt-get autoremove --assume-yes
apt-get clean --assume-yes
rm -Rf /tmp/*
rm -Rf /var/lib/apt/lists/*
rm -Rf /var/cache/apt
rm -Rf /var/lib/apt/lists
rm -Rf /home/wekan/app_build
rm -Rf /home/wekan/app
rm -Rf /home/wekan/.meteor
mkdir -p /data
chown wekan:wekan --recursive /data
mkdir /data
chown wekan --recursive /data
EOR
USER wekan
ENV PORT=8080
EXPOSE $PORT
STOPSIGNAL SIGKILL
WORKDIR /build
CMD ["bash", "-c", "ulimit -s 65500; exec node main.js"]
STOPSIGNAL SIGKILL
WORKDIR /home/wekan/app
#---------------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap:
# NODE_OPTIONS="--max_old_space_size=4096"
# Add more stack:
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
#---------------------------------------------------------------------
#
# CMD ["node", "/build/main.js"]
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 /build/main.js"]
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /build/main.js"]
CMD ["bash", "-c", "ulimit -s 65500; exec node /build/main.js"]

View file

@ -1,20 +1,12 @@
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
## Responsible Security Disclosure
Security is very important to us. If you discover any issue regarding security, please disclose
the information responsibly by sending an email from Protonmail to security@wekan.fi
that is Protomail email address, or by using this PGP key
[security-at-wekan.fi.asc](security-at-wekan.fi.asc) to security@wekan.fi
and not by creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
- Wait for new WeKan release that fixes security issue
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
## Bonus Points
- If you include code for fixing security issue
## Losing Points
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
- If you forget to include vulnerability details.
- If you send info about security issue to somewhere else than security@wekan.fi
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
## How should reports be formatted?
@ -34,7 +26,7 @@ CWSS (optional): %cwss
Anyone who reports a unique security issue in scope and does not disclose it to
a third party before we have patched and updated may be upon their approval
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
added to the Wekan Hall of Fame.
## Which domains are in scope?
@ -71,6 +63,11 @@ and by by companies that have 30k users.
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
- For On Premise:
- https://caddyserver.com/docs/automatic-https#local-https
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
- https://github.com/wekan/wekan/wiki/Azure
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
## XSS
@ -175,57 +172,6 @@ Meteor.startup(() => {
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
- https://wekan.github.io/hall-of-fame/filebleed/
### Attachments: Forced download to prevent stored XSS
- To prevent browser-side execution of uploaded content under the app origin, all attachment downloads are served with safe headers:
- `Content-Type: application/octet-stream`
- `Content-Disposition: attachment`
- `X-Content-Type-Options: nosniff`
- A restrictive `Content-Security-Policy` with `sandbox`
- This means attachments are downloaded instead of rendered inline by default. This mitigates HTML/JS/SVG based stored XSS vectors.
- Avatars and inline images remain supported but SVG uploads are blocked and never rendered inline.
## Users: Client update restrictions
- Client-side updates to user documents are limited to safe fields only:
- `username`
- `profile.*`
- Sensitive fields are blocked from any client updates and can only be modified by server methods with authorization:
- `orgs`, `teams`, `roles`, `isAdmin`, `createdThroughApi`, `loginDisabled`, `authenticationMethod`, `services.*`, `emails.*`, `sessionData.*`
- Attempts to update forbidden fields from the client are denied.
- Admin operations like managing org/team membership or toggling flags must use server methods that check permissions.
## Voting: integrity and authorization
- Client updates to card `vote` fields are blocked to prevent forged votes and inconsistent policy enforcement.
- Voting is performed via a server method that enforces:
- Authentication and board membership, or an explicit per-card flag allowing non-members to vote.
- Only the caller's own userId is added/removed from `vote.positive`/`vote.negative`.
- This prevents members from fabricating other users' votes and ensures non-members cannot vote unless explicitly allowed.
## Planning Poker: integrity and authorization
- Client updates to card `poker` fields are blocked. All poker actions go through server methods that enforce:
- Authentication and board membership for configuration and results.
- For casting a poker vote, either board membership or an explicit per-card flag allowing non-members to participate.
- Only the caller's own userId is added/removed from the selected estimation bucket (e.g., one, two, five, etc.).
- Methods cover setting/unsetting poker question/end, casting votes, replaying, and setting final estimation.
## Attachment API: authentication and DoS prevention
- The attachment API (`/api/attachment/*`) requires proper authentication using `X-User-Id` and `X-Auth-Token` headers.
- Authentication validates tokens by hashing with `Accounts._hashLoginToken` and matching against stored login tokens, preventing identity spoofing.
- Request handlers implement:
- 30-second timeout to prevent hanging connections.
- Request body size limits (50MB for uploads, 10MB for metadata operations).
- Proper error handling and guaranteed response completion.
- Request error event handlers to clean up failed connections.
- This prevents:
- DoS attacks via concurrent unauthenticated or malformed requests.
- Identity spoofing by using arbitrary bearer tokens or user IDs.
- Resource exhaustion from hanging connections or excessive payloads.
- Access control: all attachment operations verify board membership before allowing access.
## Brute force login protection
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
@ -272,4 +218,9 @@ Typical already known or "no impact" bugs such as:
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
Wekan is Open Source with MIT license, and free to use also for commercial use.
We welcome all fixes to improve security by email to security@wekan.fi
We welcome all fixes to improve security by email to security@wekan.team
## Bonus Points
If your Responsible Security Disclosure includes code for fixing security issue,
you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).

View file

@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v8.31.0"
appVersion: "v8.14.0"
files:
userUploads:
- README.md

View file

@ -10,73 +10,8 @@ import '/client/lib/boardConverter';
import '/client/components/boardConversionProgress';
// Import migration manager and progress UI
import '/client/lib/attachmentMigrationManager';
import '/client/components/settings/migrationProgress';
import '/client/lib/migrationManager';
import '/client/components/migrationProgress';
// Import cron settings
import '/client/components/settings/cronSettings';
// Mirror Meteor login token into a cookie for server-side file route auth
// This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
Meteor.startup(() => {
const COOKIE_NAME = 'meteor_login_token';
const cookieAttrs = () => {
const attrs = ['Path=/', 'SameSite=Lax'];
try {
if (window.location && window.location.protocol === 'https:') {
attrs.push('Secure');
}
} catch (_) {}
return attrs.join('; ');
};
const setCookie = (name, value) => {
if (!value) return;
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ${cookieAttrs()}`;
};
const clearCookie = (name) => {
document.cookie = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; ${cookieAttrs()}`;
};
const syncCookie = () => {
try {
const token = Accounts && typeof Accounts._storedLoginToken === 'function' ? Accounts._storedLoginToken() : null;
if (token) setCookie(COOKIE_NAME, token); else clearCookie(COOKIE_NAME);
} catch (e) {
// ignore
}
};
// Initial sync on startup
syncCookie();
// Keep cookie in sync on login/logout
if (Accounts && typeof Accounts.onLogin === 'function') Accounts.onLogin(syncCookie);
if (Accounts && typeof Accounts.onLogout === 'function') Accounts.onLogout(syncCookie);
// Sync across tabs/windows when localStorage changes
window.addEventListener('storage', (ev) => {
if (ev && typeof ev.key === 'string' && ev.key.indexOf('Meteor.loginToken') !== -1) {
syncCookie();
}
});
});
// Subscribe to per-user small publications
Meteor.startup(() => {
Tracker.autorun(() => {
if (Meteor.userId()) {
Meteor.subscribe('userGreyIcons');
Meteor.subscribe('userDesktopDragHandles');
}
});
// Initialize mobile mode on startup for iOS devices
// This ensures mobile mode is applied correctly on page load
Tracker.afterFlush(() => {
if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
Utils.initializeUserSettings();
}
});
});

View file

@ -199,5 +199,4 @@ template(name="activity")
else if(currentData.timeValue)
| {{_ activity.activityType currentData.timeValue}}
if($neq mode 'none')
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}

View file

@ -108,12 +108,15 @@
text-decoration: none;
height: 24px;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
display: inline-block;
font-size: clamp(14px, 2vw, 18px);
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
font-size: 17px;
font-weight: 500;
line-height: 1;
margin-left: 4px;
margin-left: 2px;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
font-size: 8px;
margin-top: -7px;
margin-left: 1px;
}
.comments .comment .comment-desc .reactions .reaction {
cursor: pointer;

View file

@ -25,8 +25,7 @@ template(name="comment")
= text
.edit-controls
button.primary(type="submit") {{_ 'edit'}}
a.js-close-inlined-form(title="{{_ 'close' }}")
i.fa.fa-times-thin
.fa.fa-times-thin.js-close-inlined-form
else
.comment-text
+viewer
@ -55,11 +54,9 @@ template(name="commentReactions")
span.reaction-codepoint !{reaction.reactionCodepoint}
span.reaction-count #{reaction.userIds.length}
if (currentUser.isBoardMember)
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
span(title="{{_ 'reaction' }}") 😀
span(title="{{_ 'add' }}")
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
i.fa.fa-smile-o
i.fa.fa-plus
template(name="addReactionPopup")
.reactions-popup

View file

@ -57,9 +57,8 @@ BlazeComponent.extendComponent({
BlazeComponent.extendComponent({
getComments() {
const data = this.data();
if (!data || typeof data.comments !== 'function') return [];
return data.comments();
const ret = this.data().comments();
return ret;
},
}).register("comments");

View file

@ -170,14 +170,14 @@
width: 95%;
margin: 20px;
}
.board-conversion-header,
.board-conversion-content,
.board-conversion-footer {
padding-left: 16px;
padding-right: 16px;
}
.board-conversion-header h3 {
font-size: 18px;
}

View file

@ -3,25 +3,25 @@ template(name="boardConversionProgress")
.board-conversion-modal
.board-conversion-header
h3
i.fa.fa-cog
| ⚙️
| {{_ 'converting-board'}}
p {{_ 'converting-board-description'}}
.board-conversion-content
.conversion-progress
.progress-bar
.progress-fill(style="width: {{conversionProgress}}%")
.progress-text {{conversionProgress}}%
.conversion-status
i.fa.fa-cog
| ⚙️
| {{conversionStatus}}
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
i.fa.fa-clock-o
| ⏰
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
.board-conversion-footer
.conversion-info
i.fa.fa-info-circle
|
| {{_ 'conversion-info-text'}}

View file

@ -1,6 +1,6 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import {
import {
boardConverter,
isConverting,
conversionProgress,
@ -12,15 +12,15 @@ Template.boardConversionProgress.helpers({
isConverting() {
return isConverting.get();
},
conversionProgress() {
return conversionProgress.get();
},
conversionStatus() {
return conversionStatus.get();
},
conversionEstimatedTime() {
return conversionEstimatedTime.get();
}

View file

@ -1,7 +1,6 @@
template(name="archivedBoards")
h2
span(title="{{_ 'archived-boards'}}")
i.fa.fa-archive
i.fa.fa-archive
| {{_ 'archived-boards'}}
ul.archived-lists
@ -9,7 +8,7 @@ template(name="archivedBoards")
li.archived-lists-item
div.board-header-btns
button.board-header-btn.js-delete-board
i.fa.fa-trash
i.fa.fa-trash-o
| {{_ 'delete-board'}}
button.board-header-btn.js-restore-board
i.fa.fa-undo

View file

@ -1,5 +1,4 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
BlazeComponent.extendComponent({
onCreated() {
@ -23,7 +22,7 @@ BlazeComponent.extendComponent({
events() {
return [
{
async 'click .js-restore-board'() {
'click .js-restore-board'() {
// TODO : Make isSandstorm variable global
const isSandstorm =
Meteor.settings &&
@ -31,13 +30,13 @@ BlazeComponent.extendComponent({
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await currentBoard.archive();
currentBoard.archive();
}
const board = this.currentData();
await board.restore();
board.restore();
Utils.goBoardId(board._id);
},
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
Popup.back();
const isSandstorm =
Meteor.settings &&
@ -45,9 +44,9 @@ BlazeComponent.extendComponent({
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await Boards.removeAsync(currentBoard._id);
Boards.remove(currentBoard._id);
}
await Boards.removeAsync(this._id);
Boards.remove(this._id);
FlowRouter.go('home');
}),
},

View file

@ -231,30 +231,6 @@
font-size: 1em !important; /* Keep original icon size */
}
/* Mobile iPhone: scale card details text and icons to 2x */
body.mobile-mode.iphone-device .card-details {
font-size: 2em !important;
}
body.mobile-mode.iphone-device .card-details .fa,
body.mobile-mode.iphone-device .card-details .icon,
body.mobile-mode.iphone-device .card-details i,
body.mobile-mode.iphone-device .card-details .emoji-icon,
body.mobile-mode.iphone-device .card-details a,
body.mobile-mode.iphone-device .card-details p,
body.mobile-mode.iphone-device .card-details span,
body.mobile-mode.iphone-device .card-details div,
body.mobile-mode.iphone-device .card-details button,
body.mobile-mode.iphone-device .card-details input,
body.mobile-mode.iphone-device .card-details select,
body.mobile-mode.iphone-device .card-details textarea {
font-size: inherit !important;
}
/* Section titles slightly larger than content but not as big as card title */
body.mobile-mode.iphone-device .card-details .card-details-item-title {
font-size: 1.1em !important;
font-weight: bold;
}
/* Ensure scrollbars are positioned correctly */
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
width: 12px;
@ -287,106 +263,63 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
animation: fadeIn 0.2s;
z-index: 16;
}
/* Fix for mobile Safari: ensure overlay stays behind card details */
@media screen and (max-width: 800px) {
.board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
/* In desktop mode on small screens, still keep overlay behind card */
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
}
/* In mobile mode, lower the overlay z-index to stay behind card details */
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
/* iPhone in desktop mode: remove overlay to avoid blocking card */
body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
display: none !important;
pointer-events: none !important;
}
/* Desktop mode: hide overlay to allow multiple cards and board interaction */
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
display: none !important;
pointer-events: none !important;
}
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
display: none;
}
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-wrapper.mobile-view {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper.mobile-view .board-canvas {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
border-bottom: 1px solid #ccc;
display: block !important;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
overflow-x: hidden !important;
overflow-x: hidden;
overflow-y: auto;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100%;
min-width: 100%;
}
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.board-wrapper {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper .board-canvas {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
min-width: 100% !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc;
display: block !important;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
overflow-x: hidden !important;
overflow-x: hidden;
overflow-y: auto;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100%;
min-width: 100%;
}
}
.calendar-event-green {

View file

@ -1,6 +1,8 @@
template(name="board")
if isConverting.get
if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress
else if isBoardReady.get
if currentBoard
@ -22,7 +24,7 @@ template(name="boardBody")
// Debug information (remove in production)
if debugBoardState
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
@ -47,8 +49,6 @@ template(name="boardBody")
+listsGroup(currentBoard)
else if isViewCalendar
+calendarView
else if isViewGantt
+ganttView
else
// Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes
@ -56,10 +56,6 @@ template(name="boardBody")
+swimlane(this)
else
+listsGroup(currentBoard)
//- Render multiple open cards in desktop mode
unless isMiniScreen
each openCards
+cardDetails(this cardIndex=@index)
+sidebar
template(name="calendarView")
@ -67,4 +63,4 @@ template(name="calendarView")
.calendar-view.swimlane
if currentCard
+cardDetails(currentCard)
+fullcalendar(calendarOptions)
+fullcalendar(calendarOptions)

View file

@ -1,10 +1,9 @@
import { ReactiveCache } from '/imports/reactiveCache';
import '../gantt/gantt.js';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import { boardConverter } from '/client/lib/boardConverter';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import { migrationManager } from '/client/lib/migrationManager';
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
import Swimlanes from '/models/swimlanes';
import Lists from '/models/lists';
@ -16,6 +15,7 @@ BlazeComponent.extendComponent({
onCreated() {
this.isBoardReady = new ReactiveVar(false);
this.isConverting = new ReactiveVar(false);
this.isMigrating = new ReactiveVar(false);
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
this._boardProcessed = false; // Track if board has been processed
this._lastProcessedBoardId = null; // Track last processed board ID
@ -27,16 +27,17 @@ BlazeComponent.extendComponent({
this.autorun(() => {
const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return;
const handle = subManager.subscribe('board', currentBoardId, false);
// Use a separate autorun for subscription ready state to avoid reactive loops
this.subscriptionReadyAutorun = Tracker.autorun(() => {
if (handle.ready()) {
// Only run conversion/migration logic once per board
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
this._boardProcessed = true;
this._lastProcessedBoardId = currentBoardId;
// Ensure default swimlane exists (only once per board)
this.ensureDefaultSwimlane(currentBoardId);
// Check if board needs conversion
@ -67,7 +68,7 @@ BlazeComponent.extendComponent({
if (!board) return;
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
// Check if any swimlane exists in the database to avoid race conditions
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
@ -97,31 +98,334 @@ BlazeComponent.extendComponent({
return;
}
this.isBoardReady.set(true);
// Check if board needs migration based on migration version
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
if (needsMigration) {
// Start background migration for old boards
this.isMigrating.set(true);
await this.startBackgroundMigration(boardId);
this.isMigrating.set(false);
}
// Check if board needs conversion (for old structure)
if (boardConverter.isBoardConverted(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been converted, skipping conversion`);
}
this.isBoardReady.set(true);
} else {
const needsConversion = boardConverter.needsConversion(boardId);
if (needsConversion) {
this.isConverting.set(true);
const success = await boardConverter.convertBoard(boardId);
this.isConverting.set(false);
if (success) {
this.isBoardReady.set(true);
} else {
console.error('Board conversion failed, setting ready to true anyway');
this.isBoardReady.set(true); // Still show board even if conversion failed
}
} else {
this.isBoardReady.set(true);
}
}
// Convert shared lists to per-swimlane lists if needed
await this.convertSharedListsToPerSwimlane(boardId);
// Fix missing lists migration (for cards with wrong listId references)
await this.fixMissingLists(boardId);
// Fix duplicate lists created by WeKan 8.10
await this.fixDuplicateLists(boardId);
// Start attachment migration in background if needed
this.startAttachmentMigrationIfNeeded(boardId);
} catch (error) {
console.error('Error during board conversion check:', error);
this.isConverting.set(false);
this.isMigrating.set(false);
this.isBoardReady.set(true); // Show board even if conversion check failed
}
},
async startBackgroundMigration(boardId) {
try {
// Start background migration using the cron system
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
if (error) {
console.error('Failed to start background migration:', error);
} else {
if (process.env.DEBUG === 'true') {
console.log('Background migration started for board:', boardId);
}
}
});
} catch (error) {
console.error('Error starting background migration:', error);
}
},
async convertSharedListsToPerSwimlane(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for shared lists conversion
if (board.hasSharedListsConverted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for shared lists conversion`);
}
return;
}
// Get all lists for this board
const allLists = board.lists();
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`);
}
return;
}
// Find shared lists (lists with empty swimlaneId or null swimlaneId)
const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === '');
if (sharedLists.length === 0) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no shared lists to convert`);
}
// Mark as processed even if no shared lists
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
// Convert each shared list to per-swimlane lists
for (const sharedList of sharedLists) {
// Create a copy of the list for each swimlane
for (const swimlane of swimlanes) {
// Check if this list already exists in this swimlane
const existingList = Lists.findOne({
boardId: boardId,
swimlaneId: swimlane._id,
title: sharedList.title
});
if (!existingList) {
// Double-check to avoid race conditions
const doubleCheckList = ReactiveCache.getList({
boardId: boardId,
swimlaneId: swimlane._id,
title: sharedList.title
});
if (!doubleCheckList) {
// Create a new list in this swimlane
const newListData = {
title: sharedList.title,
boardId: boardId,
swimlaneId: swimlane._id,
sort: sharedList.sort || 0,
archived: sharedList.archived || false, // Preserve archived state from original list
createdAt: new Date(),
modifiedAt: new Date()
};
// Copy other properties if they exist
if (sharedList.color) newListData.color = sharedList.color;
if (sharedList.wipLimit) newListData.wipLimit = sharedList.wipLimit;
if (sharedList.wipLimitEnabled) newListData.wipLimitEnabled = sharedList.wipLimitEnabled;
if (sharedList.wipLimitSoft) newListData.wipLimitSoft = sharedList.wipLimitSoft;
Lists.insert(newListData);
if (process.env.DEBUG === 'true') {
const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)';
console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`);
}
} else {
if (process.env.DEBUG === 'true') {
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id} (double-check), skipping`);
}
}
} else {
if (process.env.DEBUG === 'true') {
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
}
}
}
// Remove the original shared list completely
Lists.remove(sharedList._id);
if (process.env.DEBUG === 'true') {
console.log(`Removed shared list "${sharedList.title}"`);
}
}
// Mark board as processed
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
if (process.env.DEBUG === 'true') {
console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
}
} catch (error) {
console.error('Error converting shared lists to per-swimlane:', error);
}
},
async fixMissingLists(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for missing lists fix
if (board.fixMissingListsCompleted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for missing lists fix`);
}
return;
}
// Check if migration is needed
const needsMigration = await new Promise((resolve, reject) => {
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (!needsMigration) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} does not need missing lists fix`);
}
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Starting fix missing lists migration for board ${boardId}`);
}
// Execute the migration
const result = await new Promise((resolve, reject) => {
Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (result && result.success) {
if (process.env.DEBUG === 'true') {
console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
}
}
} catch (error) {
console.error('Error fixing missing lists:', error);
}
},
async fixDuplicateLists(boardId) {
try {
const board = ReactiveCache.getBoard(boardId);
if (!board) return;
// Check if board has already been processed for duplicate lists fix
if (board.fixDuplicateListsCompleted) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been processed for duplicate lists fix`);
}
return;
}
if (process.env.DEBUG === 'true') {
console.log(`Starting duplicate lists fix for board ${boardId}`);
}
// Execute the duplicate lists fix
const result = await new Promise((resolve, reject) => {
Meteor.call('fixDuplicateLists.fixBoard', boardId, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
if (result && result.fixed > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Successfully fixed ${result.fixed} duplicate lists for board ${boardId}: ${result.fixedSwimlanes} swimlanes, ${result.fixedLists} lists`);
}
// Mark board as processed
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
} else if (process.env.DEBUG === 'true') {
console.log(`No duplicate lists found for board ${boardId}`);
// Still mark as processed to avoid repeated checks
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
} else {
// Still mark as processed to avoid repeated checks
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
}
} catch (error) {
console.error('Error fixing duplicate lists:', error);
}
},
async startAttachmentMigrationIfNeeded(boardId) {
try {
// Check if board has already been migrated
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has already been migrated, skipping`);
}
return;
}
// Check if there are unconverted attachments
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
if (unconvertedAttachments.length > 0) {
if (process.env.DEBUG === 'true') {
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
}
await attachmentMigrationManager.startAttachmentMigration(boardId);
} else {
// No attachments to migrate, mark board as migrated
// This will be handled by the migration manager itself
if (process.env.DEBUG === 'true') {
console.log(`Board ${boardId} has no attachments to migrate`);
}
}
} catch (error) {
console.error('Error starting attachment migration:', error);
}
},
onlyShowCurrentCard() {
const isMiniScreen = Utils.isMiniScreen();
const currentCardId = Utils.getCurrentCardId(true);
return isMiniScreen && currentCardId;
},
openCards() {
// In desktop mode, return array of all open cards
const isMobile = Utils.getMobileMode();
if (!isMobile) {
const openCardIds = Session.get('openCards') || [];
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
}
return [];
},
goHome() {
FlowRouter.go('home');
},
@ -130,6 +434,10 @@ BlazeComponent.extendComponent({
return this.isConverting.get();
},
isMigrating() {
return this.isMigrating.get();
},
isBoardReady() {
return this.isBoardReady.get();
},
@ -221,9 +529,9 @@ BlazeComponent.extendComponent({
const popupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 &&
if (node.nodeType === 1 &&
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
!node.closest('.js-swimlanes') &&
!node.closest('.js-swimlanes') &&
!node.closest('.swimlane') &&
!node.closest('.list') &&
!node.closest('.minicard')) {
@ -339,9 +647,9 @@ BlazeComponent.extendComponent({
.js-add-card[tabindex] {
outline: none;
}
/* Sidebar hamburger menu button in header */
.js-toggle-sidebar .fa-bars {
color: #fff !important;
/* Hamburger menu */
.fa-bars, .icon-hamburger {
color: #222 !important;
}
/* Grey icons in card detail header */
.card-detail-header .fa, .card-detail-header .icon {
@ -540,60 +848,47 @@ BlazeComponent.extendComponent({
isViewSwimlanes() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
// If no board view is set, default to swimlanes
if (!boardView) {
boardView = 'board-view-swimlanes';
}
return boardView === 'board-view-swimlanes';
},
isViewLists() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-lists';
},
isViewCalendar() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-cal';
},
isViewGantt() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-gantt';
},
hasSwimlanes() {
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard) {
@ -602,7 +897,7 @@ BlazeComponent.extendComponent({
}
return false;
}
try {
const swimlanes = currentBoard.swimlanes();
const hasSwimlanes = swimlanes && swimlanes.length > 0;
@ -637,24 +932,27 @@ BlazeComponent.extendComponent({
const currentBoardId = Session.get('currentBoard');
const isBoardReady = this.isBoardReady.get();
const isConverting = this.isConverting.get();
const isMigrating = this.isMigrating.get();
const boardView = Utils.boardView();
if (process.env.DEBUG === 'true') {
console.log('=== BOARD DEBUG STATE ===');
console.log('currentBoardId:', currentBoardId);
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
console.log('isBoardReady:', isBoardReady);
console.log('isConverting:', isConverting);
console.log('isMigrating:', isMigrating);
console.log('boardView:', boardView);
console.log('========================');
}
return {
currentBoardId,
hasCurrentBoard: !!currentBoard,
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
isBoardReady,
isConverting,
isMigrating,
boardView
};
},
@ -1021,8 +1319,3 @@ BlazeComponent.extendComponent({
}
},
}).register('calendarView');
/**
* Gantt View Component
* Displays cards as a Gantt chart with start/due dates
*/

View file

@ -49,12 +49,6 @@ THEME - NEPHRITIS
border-bottom: 2px solid #27ae60;
border-right: 2px solid #27ae60;
}
.board-color-nephritis .checklist-progress-bar {
background-color: #d4f1dd !important;
}
.board-color-nephritis .checklist-progress-bar .checklist-progress {
background-color: #27ae60 !important;
}
.board-color-nephritis .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e7faef;
}
@ -156,12 +150,6 @@ THEME - Pomegranate
border-bottom: 2px solid #c0392b;
border-right: 2px solid #c0392b;
}
.board-color-pomegranate .checklist-progress-bar {
background-color: #f5d5d2 !important;
}
.board-color-pomegranate .checklist-progress-bar .checklist-progress {
background-color: #c0392b !important;
}
.board-color-pomegranate .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #faeae9;
}
@ -263,12 +251,6 @@ THEME - Belize
border-bottom: 2px solid #2980b9;
border-right: 2px solid #2980b9;
}
.board-color-belize .checklist-progress-bar {
background-color: #d1e7f5 !important;
}
.board-color-belize .checklist-progress-bar .checklist-progress {
background-color: #2980b9 !important;
}
.board-color-belize .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e8f3fa;
}
@ -370,12 +352,6 @@ THEME - Wisteria
border-bottom: 2px solid #8e44ad;
border-right: 2px solid #8e44ad;
}
.board-color-wisteria .checklist-progress-bar {
background-color: #e8d9f0 !important;
}
.board-color-wisteria .checklist-progress-bar .checklist-progress {
background-color: #8e44ad !important;
}
.board-color-wisteria .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #f4ecf7;
}
@ -477,12 +453,6 @@ THEME - Midnight
border-bottom: 2px solid #2c3e50;
border-right: 2px solid #2c3e50;
}
.board-color-midnight .checklist-progress-bar {
background-color: #d2dae2 !important;
}
.board-color-midnight .checklist-progress-bar .checklist-progress {
background-color: #2c3e50 !important;
}
.board-color-midnight .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e6ecf1;
}
@ -584,12 +554,6 @@ THEME - Pumpkin
border-bottom: 2px solid #e67e22;
border-right: 2px solid #e67e22;
}
.board-color-pumpkin .checklist-progress-bar {
background-color: #f9e5d1 !important;
}
.board-color-pumpkin .checklist-progress-bar .checklist-progress {
background-color: #e67e22 !important;
}
.board-color-pumpkin .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #fdf2e9;
}
@ -691,12 +655,6 @@ THEME - Moderate Pink
border-bottom: 2px solid #cd5a91;
border-right: 2px solid #cd5a91;
}
.board-color-moderatepink .checklist-progress-bar {
background-color: #f4dde8 !important;
}
.board-color-moderatepink .checklist-progress-bar .checklist-progress {
background-color: #cd5a91 !important;
}
.board-color-moderatepink .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #faeef4;
}
@ -798,12 +756,6 @@ THEME - Strong Cyan
border-bottom: 2px solid #00aecc;
border-right: 2px solid #00aecc;
}
.board-color-strongcyan .checklist-progress-bar {
background-color: #ccf2f9 !important;
}
.board-color-strongcyan .checklist-progress-bar .checklist-progress {
background-color: #00aecc !important;
}
.board-color-strongcyan .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e0fbff;
}
@ -905,12 +857,6 @@ THEME - Lime Green
border-bottom: 2px solid #4bbf6b;
border-right: 2px solid #4bbf6b;
}
.board-color-limegreen .checklist-progress-bar {
background-color: #daf4de !important;
}
.board-color-limegreen .checklist-progress-bar .checklist-progress {
background-color: #4bbf6b !important;
}
.board-color-limegreen .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #edf9f0;
}
@ -1013,12 +959,6 @@ THEME - Dark
border-bottom: 2px solid #2c3e51;
border-right: 2px solid #2c3e51;
}
.board-color-dark .checklist-progress-bar {
background-color: #d2dae2 !important;
}
.board-color-dark .checklist-progress-bar .checklist-progress {
background-color: #2c3e51 !important;
}
.board-color-dark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e6ecf1;
}
@ -1222,12 +1162,6 @@ THEME - Relax
border-bottom: 2px solid #27ae61;
border-right: 2px solid #27ae61;
}
.board-color-relax .checklist-progress-bar {
background-color: #d4f1dd !important;
}
.board-color-relax .checklist-progress-bar .checklist-progress {
background-color: #27ae61 !important;
}
.board-color-relax .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e7faef;
}
@ -1358,12 +1292,6 @@ THEME - Corteza
border-bottom: 2px solid #568ba2;
border-right: 2px solid #568ba2;
}
.board-color-corteza .checklist-progress-bar {
background-color: #dce6ec !important;
}
.board-color-corteza .checklist-progress-bar .checklist-progress {
background-color: #568ba2 !important;
}
.board-color-corteza .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #eef3f6;
}
@ -1469,12 +1397,6 @@ THEME - Clear Blue
border-bottom: 2px solid #499bea;
border-right: 2px solid #499bea;
}
.board-color-clearblue .checklist-progress-bar {
background-color: #daeefb !important;
}
.board-color-clearblue .checklist-progress-bar .checklist-progress {
background-color: #499bea !important;
}
.board-color-clearblue .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e0fbff;
}
@ -1503,7 +1425,7 @@ THEME - Clear Blue
}
.board-color-clearblue .list {
background: rgba(255,255,255,0.35);
margin: 10px 0;
margin: 10px;
border: 0;
border-radius: 14px;
}
@ -1738,12 +1660,6 @@ THEME - Natural
border-bottom: 2px solid #596557;
border-right: 2px solid #596557;
}
.board-color-natural .checklist-progress-bar {
background-color: #dee0dd !important;
}
.board-color-natural .checklist-progress-bar .checklist-progress {
background-color: #596557 !important;
}
.board-color-natural .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #eef0ee;
}
@ -1854,12 +1770,6 @@ THEME - Modern
border-bottom: 2px solid #2a80b8;
border-right: 2px solid #2a80b8;
}
.board-color-modern .checklist-progress-bar {
background-color: #d1e7f5 !important;
}
.board-color-modern .checklist-progress-bar .checklist-progress {
background-color: #2a80b8 !important;
}
.board-color-modern .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e8f3fa;
}
@ -2152,12 +2062,6 @@ THEME - Modern Dark
border-bottom: 2px solid #2a2a2a;
border-right: 2px solid #2a2a2a;
}
.board-color-moderndark .checklist-progress-bar {
background-color: #d1d1d1 !important;
}
.board-color-moderndark .checklist-progress-bar .checklist-progress {
background-color: #2a2a2a !important;
}
.board-color-moderndark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #eaeaea;
}
@ -2643,12 +2547,6 @@ THEME - Exodark
border-bottom: 2px solid #dbdbdb!important;/*Fix contrast of checkbox*/
border-right: 2px solid #dbdbdb!important;
}
.board-color-exodark .checklist-progress-bar {
background-color: #cccccc !important;
}
.board-color-exodark .checklist-progress-bar .checklist-progress {
background-color: #222 !important;
}
.board-color-exodark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
background: #e9e9e9;
}
@ -2690,7 +2588,7 @@ THEME - Exodark
background: #222;
}
.board-color-exodark .list {
margin: 10px 0;
margin: 10px;
color: #fff;
border-radius: 15px;
background-color: #1c1c1c;
@ -3242,12 +3140,6 @@ THEME - Clean Dark
margin-left: 3px;
margin-top: 3px;
}
.board-color-cleandark .checklist-progress-bar {
background-color: #6b6b78 !important;
}
.board-color-cleandark .checklist-progress-bar .checklist-progress {
background-color: #23232B !important;
}
.board-color-cleandark .allBoards {
white-space: nowrap;
@ -4000,13 +3892,6 @@ THEME - Clean Light
margin-left: 3px;
margin-top: 3px;
}
.board-color-cleanlight .checklist-progress-bar {
background-color: #f5f5f5 !important;
}
.board-color-cleanlight .checklist-progress-bar .checklist-progress {
background-color: #c0c0c0 !important;
color: #010101 !important;
}
.board-color-cleanlight .allBoards {
white-space: nowrap;

View file

@ -505,73 +505,73 @@
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !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: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !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: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !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 .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: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !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 .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: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !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 {
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;
@ -667,9 +667,9 @@
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}

View file

@ -14,39 +14,41 @@ template(name="boardHeaderBar")
with currentBoard
if currentUser.isBoardAdmin
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
if showStarCounter
span
= currentBoard.stars
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
span {{_ currentBoard.permission}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
| 👁️
if $eq watchLevel "tracking"
i.fa.fa-bell
| 🔔
if $eq watchLevel "muted"
i.fa.fa-bell-slash
| 🔕
span {{_ watchLevel}}
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span.board-star-counter {{currentBoard.stars}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
| {{sortCardsIcon}}
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
| ❌
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
| 🚪
span {{_ 'log-in'}}
.board-header-btns.center
@ -57,114 +59,99 @@ template(name="boardHeaderBar")
if currentUser
with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
| 👁️
if $eq watchLevel "tracking"
i.fa.fa-bell
| 🔔
if $eq watchLevel "muted"
i.fa.fa-bell-slash
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
| 🔕
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
| {{sortCardsIcon}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
| ❌
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
| 🚪
if isSandstorm
if currentUser
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
| 📦
//if showSort
//
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
//
i.fa(class="{{directionClass}}")
//
span {{_ 'sort'}}{{_ listSortShortDesc}}
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
// i.fa(class="{{directionClass}}")
// span {{_ 'sort'}}{{_ listSortShortDesc}}
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
i.fa.fa-filter
span {{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}
class="{{#if Filter.isActive}}emphasis{{/if}}")
| 🔽
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
| ❌
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search
span {{_ 'search'}}
| 🔍
unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view
i.fa.fa-caret-down
a.board-header-btn.js-toggle-board-view(
title="{{_ 'board-view'}}")
| ▼
if $eq boardView 'board-view-swimlanes'
i.fa.fa-th-large
| 🏊
if $eq boardView 'board-view-lists'
i.fa.fa-trello
| 📋
if $eq boardView 'board-view-cal'
i.fa.fa-calendar
if $eq boardView 'board-view-gantt'
i.fa.fa-bar-chart
span
if $eq boardView 'board-view-swimlanes'
| {{_ 'swimlanes'}}
if $eq boardView 'board-view-lists'
| {{_ 'lists'}}
if $eq boardView 'board-view-cal'
| {{_ 'calendar'}}
if $eq boardView 'board-view-gantt'
| {{_ 'gantt'}}
| 📅
if canModifyBoard
a.board-header-btn.js-multiselection-activate(
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
| ☑️
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
| ❌
.separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-bars
| ☰
template(name="boardVisibilityList")
ul.pop-over-list
li
with "private"
a.js-select-visibility
i.fa.fa-lock
| 🔒
| {{_ 'private'}}
if visibilityCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'private-desc'}}
if notAllowPrivateVisibilityOnly
li
with "public"
a.js-select-visibility
i.fa.fa-globe
| 🌐
| {{_ 'public'}}
if visibilityCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'public-desc'}}
template(name="boardChangeVisibilityPopup")
@ -175,26 +162,26 @@ template(name="boardChangeWatchPopup")
li
with "watching"
a.js-select-watch
i.fa.fa-eye
| 👁️
| {{_ 'watching'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'watching-info'}}
li
with "tracking"
a.js-select-watch
i.fa.fa-bell
| 🔔
| {{_ 'tracking'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'tracking-info'}}
li
with "muted"
a.js-select-watch
i.fa.fa-bell-slash
| 🔕
| {{_ 'muted'}}
if watchCheck
i.fa.fa-check
| ✅
span.sub-name {{_ 'muted-info'}}
template(name="boardChangeViewPopup")
@ -202,31 +189,24 @@ template(name="boardChangeViewPopup")
li
with "board-view-swimlanes"
a.js-open-swimlanes-view
i.fa.fa-th-large
| {{_ 'swimlanes'}}
| 🏊
| {{_ 'board-view-swimlanes'}}
if $eq Utils.boardView "board-view-swimlanes"
i.fa.fa-check
| ✅
li
with "board-view-lists"
a.js-open-lists-view
i.fa.fa-trello
| 📋
| {{_ 'board-view-lists'}}
if $eq Utils.boardView "board-view-lists"
i.fa.fa-check
| ✅
li
with "board-view-cal"
a.js-open-cal-view
i.fa.fa-calendar
| 📅
| {{_ 'board-view-cal'}}
if $eq Utils.boardView "board-view-cal"
i.fa.fa-check
li
with "board-view-gantt"
a.js-open-gantt-view
i.fa.fa-bar-chart
| {{_ 'board-view-gantt'}}
if $eq Utils.boardView "board-view-gantt"
i.fa.fa-check
| ✅
template(name="createBoard")
form
@ -238,11 +218,11 @@ template(name="createBoard")
else
p.quiet
if $eq visibility.get 'public'
span.fa.fa-globe.colorful
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
@ -267,41 +247,11 @@ template(name="createBoardPopup")
else
p.quiet
if $eq visibility.get 'public'
span.fa.fa-globe.colorful
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
a.flex.js-toggle-add-template-container
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
// New popup for Template Container creation; shares the same form content
template(name="createTemplateContainerPopup")
form
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen.get
+boardVisibilityList
else
p.quiet
if $eq visibility.get 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
@ -317,30 +267,19 @@ template(name="createTemplateContainerPopup")
a.js-board-template {{_ 'template'}}
//template(name="listsortPopup")
//
h2
//
| {{_ 'list-sort-by'}}
//
hr
//
ul.pop-over-list
//
each value in allowedSortValues
//
li
//
a.js-sort-by(name="{{value.name}}")
//
if $eq sortby value.name
//
| {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
//
| {{_ value.label }}{{_ value.shortLabel}}
//
if $eq sortby value.name
//
i.fa.fa-check
// h2
// | {{_ 'list-sort-by'}}
// hr
// ul.pop-over-list
// each value in allowedSortValues
// li
// a.js-sort-by(name="{{value.name}}")
// if $eq sortby value.name
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
// | {{_ value.label }}{{_ value.shortLabel}}
// if $eq sortby value.name
// | ✅
template(name="boardChangeTitlePopup")
form
label
@ -360,21 +299,21 @@ template(name="cardsSortPopup")
ul.pop-over-list
li
a.js-sort-due
i.fa.fa-calendar
| 📅
| {{_ 'due-date'}}
hr
li
a.js-sort-title
i.fa.fa-sort-alpha-asc
| 🔤
| {{_ 'title-alphabetically'}}
hr
li
a.js-sort-created-desc
i.fa.fa-arrow-down
| ⬇️
| {{_ 'created-at-newest-first'}}
hr
li
a.js-sort-created-asc
i.fa.fa-arrow-up
| ⬆️
| {{_ 'created-at-oldest-first'}}

View file

@ -1,6 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import dragscroll from '@wekanteam/dragscroll';
/*
@ -10,7 +9,7 @@ const UPCLS = 'fa-sort-up';
const sortCardsBy = new ReactiveVar('');
Template.boardChangeTitlePopup.events({
async submit(event, templateInstance) {
submit(event, templateInstance) {
const newTitle = templateInstance
.$('.js-board-name')
.val()
@ -20,8 +19,8 @@ Template.boardChangeTitlePopup.events({
.val()
.trim();
if (newTitle) {
await this.rename(newTitle);
await this.setDescription(newDesc);
this.rename(newTitle);
this.setDescription(newDesc);
Popup.back();
}
event.preventDefault();
@ -73,10 +72,7 @@ BlazeComponent.extendComponent({
{
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() {
const boardId = Session.get('currentBoard');
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
@ -182,7 +178,7 @@ Template.boardHeaderBar.helpers({
if (!sortBy) {
return '🃏'; // Card icon when nothing is selected
}
// Determine which sort option is active based on sortBy object
if (sortBy.dueAt) {
return '📅'; // Due date icon
@ -191,7 +187,7 @@ Template.boardHeaderBar.helpers({
} else if (sortBy.createdAt) {
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
}
return '🃏'; // Default card icon
},
});
@ -209,10 +205,6 @@ Template.boardChangeViewPopup.events({
Utils.setBoardView('board-view-cal');
Popup.back();
},
'click .js-open-gantt-view'() {
Utils.setBoardView('board-view-gantt');
Popup.back();
},
});
const CreateBoard = BlazeComponent.extendComponent({
@ -299,15 +291,6 @@ const CreateBoard = BlazeComponent.extendComponent({
},
);
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get());
} else {
@ -326,15 +309,6 @@ const CreateBoard = BlazeComponent.extendComponent({
boardId: this.boardId.get(),
});
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(this.boardId.get());
}
},
@ -356,18 +330,11 @@ const CreateBoard = BlazeComponent.extendComponent({
},
}).register('createBoardPopup');
(class CreateTemplateContainerPopup extends CreateBoard {
onRendered() {
// Always pre-check the template container checkbox for this popup
$('#add-template-container').addClass('is-checked');
}
}).register('createTemplateContainerPopup');
(class HeaderBarCreateBoard extends CreateBoard {
async onSubmit(event) {
onSubmit(event) {
super.onSubmit(event);
// Immediately star boards crated with the headerbar popup.
await ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
}
}.register('headerBarCreateBoardPopup'));

View file

@ -8,273 +8,6 @@
padding: 1vh 0;
}
/* Two-column layout for All Boards */
.boards-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 16px;
}
.boards-left-menu {
border-right: 1px solid #e0e0e0;
padding-right: 12px;
}
.boards-left-menu ul.menu {
list-style: none;
padding: 0;
margin: 0 0 12px 0;
}
.boards-left-menu .menu-item {
margin: 4px 0;
}
.boards-left-menu .menu-item a {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-radius: 4px;
cursor: pointer;
}
.boards-left-menu .menu-item .menu-label {
flex: 1;
}
.boards-left-menu .menu-item .menu-count {
background: #ddd;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-left: 8px;
}
.boards-left-menu .menu-item.active a,
.boards-left-menu .menu-item a:hover {
background: #f0f0f0;
}
.boards-left-menu .menu-item.active .menu-count {
background: #bbb;
}
/* Drag-over state for menu items (for dropping boards on Remaining) */
.boards-left-menu .menu-item a.drag-over {
background: #d0e8ff;
border: 2px dashed #2196F3;
}
.workspaces-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: bold;
margin-top: 12px;
}
.workspaces-header .js-add-space {
text-decoration: none;
font-weight: bold;
border: 1px solid #ccc;
padding: 2px 8px;
border-radius: 4px;
}
.workspace-tree {
list-style: none;
padding-left: 10px;
}
.workspace-node {
margin: 2px 0;
position: relative;
}
.workspace-node-content {
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.workspace-node.dragging > .workspace-node-content {
opacity: 0.5;
background: #e0e0e0;
}
.workspace-node.drag-over > .workspace-node-content {
background: #d0e8ff;
border: 2px dashed #2196F3;
}
.workspace-drag-handle {
cursor: grab;
color: #999;
font-size: 14px;
padding: 0 4px;
user-select: none;
}
.workspace-drag-handle:active {
cursor: grabbing;
}
.workspace-node .js-select-space {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
flex: 1;
text-decoration: none;
}
.workspace-node .workspace-icon {
font-size: 16px;
line-height: 1;
}
.workspace-node .workspace-name {
flex: 1;
}
.workspace-node .workspace-count {
background: #ddd;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
font-weight: bold;
min-width: 20px;
text-align: center;
}
.workspace-node .js-edit-space,
.workspace-node .js-add-subspace {
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
text-decoration: none;
font-size: 14px;
opacity: 0.6;
transition: opacity 0.2s;
}
.workspace-node .js-edit-space:hover,
.workspace-node .js-add-subspace:hover {
opacity: 1;
background: #e0e0e0;
}
.workspace-node.active > .workspace-node-content .js-select-space,
.workspace-node > .workspace-node-content:hover .js-select-space {
background: #f0f0f0;
}
.workspace-node.active .workspace-count {
background: #bbb;
}
.boards-right-grid {
min-height: 200px;
}
.boards-path-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 12px 16px;
margin-bottom: 16px;
background: #f5f5f5;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
}
.boards-path-header .path-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.boards-path-header .multiselection-hint {
background: #FFF3CD;
color: #856404;
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: normal;
border: 1px solid #FFE69C;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.boards-path-header .path-right {
display: flex;
align-items: center;
gap: 8px;
}
.boards-path-header .path-icon {
font-size: 18px;
}
.boards-path-header .path-text {
color: #333;
}
.boards-path-header .board-header-btn {
padding: 6px 12px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s;
}
.boards-path-header .board-header-btn:hover {
background: #f0f0f0;
border-color: #bbb;
}
.boards-path-header .board-header-btn.emphasis {
background: #2196F3;
color: #fff;
border-color: #2196F3;
font-weight: bold;
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.5);
transform: scale(1.05);
}
.boards-path-header .board-header-btn.emphasis:hover {
background: #1976D2;
box-shadow: 0 3px 12px rgba(33, 150, 243, 0.7);
}
.boards-path-header .board-header-btn-close {
padding: 4px 10px;
background: #f44336;
color: #000;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-left: 10px; /* Extra space between MultiSelection toggle and Remove Filter */
}
.boards-path-header .board-header-btn-close:hover {
background: #d32f2f;
}
.zoom-controls {
display: flex;
align-items: center;
@ -373,35 +106,23 @@
.board-list li.starred .is-star-active,
.board-list li.starred .is-not-star-active {
opacity: 1;
color: #ffd700;
}
/* Show star icon on hover even for non-starred boards */
.board-list li:hover .is-star-active,
.board-list li:hover .is-not-star-active {
opacity: 1;
}
.board-list .board-list-item {
overflow: hidden;
background-color: inherit; /* Inherit board color from parent li.js-board */
background-color: #999;
color: #f6f6f6;
min-height: 100px;
font-size: 16px;
line-height: 22px;
border-radius: 0; /* No border-radius - parent .js-board has it */
border-radius: 3px;
display: block;
font-weight: 700;
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
margin: 0; /* No margin - moved to parent .js-board */
padding: 8px;
margin: 8px;
position: relative;
text-decoration: none;
word-wrap: break-word;
}
.board-list .board-list-item > .js-open-board {
text-decoration: none;
color: inherit;
display: block;
}
.board-list .board-list-item.template-container {
border: 4px solid #fff;
}
@ -429,27 +150,13 @@
.board-list .js-add-board .label {
font-weight: normal;
line-height: 56px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: #999; /* Darker background for better text contrast */
border-radius: 3px;
padding: 36px 8px 32px 8px;
color: #fff; /* White text */
}
.board-list .js-add-board .label i {
color: #fff; /* White icon */
}
.board-list .js-add-board .label:hover {
background-color: #808080; /* Even darker on hover */
}
.board-list .js-add-board .label:hover i {
color: #fff; /* Keep icon white on hover */
.board-list .js-add-board :hover {
background-color: #939393;
}
.board-list .is-star-active,
.board-list .is-not-star-active {
top: 0;
bottom: 0;
font-size: 14px;
height: 18px;
line-height: 18px;
@ -457,6 +164,7 @@
padding: 9px 9px;
position: absolute;
right: 0;
top: 0;
transition-duration: 0.15s;
transition-property: color, font-size, background;
}
@ -530,107 +238,6 @@
.board-list li:hover a .is-not-star-active {
opacity: 1;
}
/* Board drag handle - always visible and positioned at top */
.board-list .board-handle {
position: absolute;
padding: 4px 6px;
top: 4px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
color: #fff;
background: rgba(0,0,0,0.4);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.2s ease;
cursor: grab;
opacity: 1;
user-select: none;
}
.board-list .board-handle:active {
cursor: grabbing;
}
.board-list .board-handle:hover {
background: rgba(255, 255, 0, 0.8) !important;
color: #000;
}
/* Multiselection checkbox on board items */
.board-list .board-list-item .multi-selection-checkbox {
position: absolute !important;
bottom: 4px !important;
left: 4px !important;
top: auto !important;
width: 24px;
height: 24px;
border: 3px solid #fff;
background: rgba(0,0,0,0.5);
border-radius: 4px;
cursor: pointer;
z-index: 11;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
transform: none !important;
margin: 0 !important;
}
.board-list .board-list-item .multi-selection-checkbox:hover {
background: rgba(0,0,0,0.7);
transform: scale(1.15) !important;
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
}
.board-list .board-list-item .multi-selection-checkbox.is-checked {
background: #3cb500;
border-color: #3cb500;
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
width: 24px !important;
height: 24px !important;
top: auto !important;
left: 4px !important;
transform: none !important;
border-radius: 4px !important;
}
.board-list .board-list-item .multi-selection-checkbox.is-checked::after {
content: '✓';
color: #fff;
font-size: 16px;
font-weight: bold;
}
/* Grey checkboxes when grey icons setting is enabled */
body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
background: #7a7a7a;
border-color: #7a7a7a;
box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
}
body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
outline: 4px solid #7a7a7a;
box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
}
.board-list.is-multiselection-active .js-board.is-checked {
outline: 4px solid #3cb500;
outline-offset: -4px;
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
}
/* Visual hint when multiselection is active */
.board-list.is-multiselection-active .board-list-item {
border: 2px dashed rgba(33, 150, 243, 0.3);
}
.board-backgrounds-list .board-background-select {
box-sizing: border-box;
display: block;
@ -664,19 +271,8 @@ body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checke
}
.board-backgrounds-list .board-background-select .background-box i.fa-check {
font-size: 25px;
color: #3cb500;
color: #fff;
}
/* Grey check icons when grey icons setting is enabled */
body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
color: #7a7a7a;
}
/* Prevent Grey Icons from affecting checkmarks in background color list */
body.grey-icons-enabled .checkmark-no-grey {
filter: none !important;
-webkit-filter: none !important;
}
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-list.mobile-view {
height: calc(100vh - 120px);
@ -1143,62 +739,9 @@ body.grey-icons-enabled .checkmark-no-grey {
#resetBtn {
display: inline;
}
#resetBtn.filter-reset-btn {
background: #f44336;
color: #000;
border: none;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background 0.2s;
}
#resetBtn.filter-reset-btn:hover {
background: #d32f2f;
}
#resetBtn.filter-reset-btn .reset-icon {
font-size: 14px;
}
.js-board {
display: block;
background-color: #999; /* Default gray background if no color class is applied */
border-radius: 3px; /* Rounded corners for board items */
overflow: hidden; /* Ensure children respect rounded corners */
margin: 8px; /* Space between board items */
}
/* Reset background for add-board button */
.js-add-board {
background-color: transparent !important;
margin: 8px !important; /* Keep margin for add-board */
}
/* Apply board colors to li.js-board parent instead of just the link */
.board-list .board-color-nephritis { background-color: #27ae60; }
.board-list .board-color-pomegranate { background-color: #c0392b; }
.board-list .board-color-belize { background-color: #2980b9; }
.board-list .board-color-wisteria { background-color: #8e44ad; }
.board-list .board-color-midnight { background-color: #2c3e50; }
.board-list .board-color-pumpkin { background-color: #e67e22; }
.board-list .board-color-moderatepink { background-color: #cd5a91; }
.board-list .board-color-strongcyan { background-color: #00aecc; }
.board-list .board-color-limegreen { background-color: #4bbf6b; }
.board-list .board-color-dark { background-color: #2c3e51; }
.board-list .board-color-relax { background-color: #27ae61; }
.board-list .board-color-corteza { background-color: #568ba2; }
.board-list .board-color-clearblue { background-color: #3498db; }
.board-list .board-color-natural { background-color: #596557; }
.board-list .board-color-modern { background-color: #2a80b8; }
.board-list .board-color-moderndark { background-color: #2a2a2a; }
.board-list .board-color-exodark { background-color: #222; }
.minicard-members {
padding: 6px 0 6px 8px;
width: 100%;

View file

@ -2,238 +2,158 @@ template(name="boardList")
.wrapper
.board-list-header
.boards-layout
// Left menu
.boards-left-menu
ul.menu
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
a.js-select-menu(data-type="starred")
span.menu-label
span.emoji-icon
i.fa.fa-star
| {{_ 'allboards.starred'}}
span.menu-count {{menuItemCount 'starred'}}
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
a.js-select-menu(data-type="templates")
span.menu-label
span.emoji-icon
i.fa.fa-clipboard
| {{_ 'allboards.templates'}}
span.menu-count {{menuItemCount 'templates'}}
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
a.js-select-menu(data-type="remaining")
span.menu-label
span.emoji-icon
i.fa.fa-folder
| {{_ 'allboards.remaining'}}
span.menu-count {{menuItemCount 'remaining'}}
.workspaces-header
span
span.emoji-icon
i.fa.fa-folder-open
| {{_ 'allboards.workspaces'}}
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
// Workspaces tree
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
ul.AllBoardTeamsOrgs
li.AllBoardTeams
if userHasTeams
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
option(value="-1") {{_ 'teams'}} :
each teamsDatas
option(value="{{teamId}}") {{_ teamDisplayName}}
// Existing filter by orgs/teams (kept)
ul.AllBoardTeamsOrgs
li.AllBoardTeams
if userHasTeams
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
option(value="-1") {{_ 'teams'}} :
each teamsDatas
option(value="{{teamId}}") {{_ teamDisplayName}}
li.AllBoardOrgs
if userHasOrgs
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
option(value="-1") {{_ 'organizations'}} :
each orgsDatas
option(value="{{orgId}}") {{orgDisplayName}}
li.AllBoardOrgs
if userHasOrgs
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
option(value="-1") {{_ 'organizations'}} :
each orgsDatas
option(value="{{orgId}}") {{orgDisplayName}}
//li.AllBoardTemplates
// if userHasTemplates
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
// option(value="-1") {{_ 'templates'}} :
// each templatesDatas
// option(value="{{templateId}}") {{_ templateDisplayName}}
li.AllBoardBtns
div.AllBoardButtonsContainer
if userHasOrgsOrTeams
span.emoji-icon
i.fa.fa-search
input#filterBtn(type="button" value="{{_ 'filter'}}")
button#resetBtn.filter-reset-btn
span.reset-icon
span.emoji-icon
i.fa.fa-times-thin
span {{_ 'filter-clear'}}
li.AllBoardBtns
div.AllBoardButtonsContainer
if userHasOrgsOrTeams
i.fa.fa-filter
input#filterBtn(type="button" value="{{_ 'filter'}}")
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
// Right boards grid
.boards-right-grid
.boards-path-header
.path-left
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
if BoardMultiSelection.isActive
span.multiselection-hint
span.emoji-icon
i.fa.fa-thumb-tack
| {{_ 'multi-selection-active'}}
.path-right
if canModifyBoards
if hasBoardsSelected
button.js-archive-selected-boards.board-header-btn
span.emoji-icon
i.fa.fa-archive
span {{_ 'archive-board'}}
button.js-duplicate-selected-boards.board-header-btn
span.emoji-icon
i.fa.fa-clipboard
span {{_ 'duplicate-board'}}
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
span.emoji-icon
i.fa.fa-check-square-o
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
span.emoji-icon
i.fa.fa-plus
| &nbsp;{{_ 'add-template-container'}}
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
li.js-add-board
a.board-list-item.label(title="{{_ 'add-board'}}")
| {{_ 'add-board'}}
each boards
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
if isInvited
.board-list-item
span.details
span.board-list-item-name= title
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}")
p.board-list-item-desc {{_ 'just-invited'}}
button.js-accept-invite.primary {{_ 'accept'}}
button.js-decline-invite {{_ 'decline'}}
else
if $eq type "template-container"
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'template-container'}}")
+viewer
= title
i.fa.js-star-board(
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}")
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
i.fa.js-has-spenttime-cards(
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
if isSandstorm
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if isAdministrable
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else if currentUser.isAdmin
i.fa.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
i.fa.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
else
a.board-list-item.label(title="{{_ 'add-board'}}")
span.emoji-icon
i.fa.fa-plus
| &nbsp;{{_ 'add-board'}}
each boards
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
if isInvited
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.details
span.board-list-item-name= title
span.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
| {{#if isStarred}}⭐{{else}}☆{{/if}}
p.board-list-item-desc {{_ 'just-invited'}}
button.js-accept-invite.primary {{_ 'accept'}}
button.js-decline-invite {{_ 'decline'}}
else
if $eq type "template-container"
.template-container.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon
i.fa.fa-arrows
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'template-container'}}")
+viewer
= title
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
span.emoji-icon
i.fa.fa-clock-o
span.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
else
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon
i.fa.fa-arrows
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+viewer
= title
unless currentSetting.hideBoardMemberList
if allowsBoardMemberList
.minicard-members
each member in boardMembers _id
a.name
+userAvatar(userId=member noRemove=true)
unless currentSetting.hideCardCounterList
if allowsCardCounterList
.minicard-lists.flex.flex-wrap
each list in boardLists _id
.item
| {{ list }}
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
span.emoji-icon
i.fa.fa-clock-o
a.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+viewer
= title
unless currentSetting.hideBoardMemberList
if allowsBoardMemberList
.minicard-members
each member in boardMembers _id
a.name
+userAvatar(userId=member noRemove=true)
unless currentSetting.hideCardCounterList
if allowsCardCounterList
.minicard-lists.flex.flex-wrap
each list in boardLists _id
.item
| {{ list }}
a.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
| {{#if isStarred}}⭐{{else}}☆{{/if}}
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
i.fa.js-has-spenttime-cards(
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
i.fa.board-handle(
class="fa-arrows"
title="{{_ 'drag-board'}}")
if isSandstorm
a.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
| 📋
a.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
| 📦
else if isAdministrable
a.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
| 📋
a.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
| 📦
else if currentUser.isAdmin
a.js-clone-board(
class="fa-clone"
title="{{_ 'duplicate-board'}}")
| 📋
a.js-archive-board(
class="fa-archive"
title="{{_ 'archive-board'}}")
| 📦
template(name="boardListHeaderBar")
h1 {{_ title }}
//.board-header-btns.right
//
a.board-header-btn.js-open-archived-board
//
i.fa.fa-archive
//
span {{_ 'archives'}}
//
a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
//
i.fa.fa-clone
//
span {{_ 'templates'}}
// Recursive template for workspaces tree
template(name="workspaceTree")
if nodes
ul.workspace-tree.js-workspace-tree
each nodes
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
.workspace-node-content
span.workspace-drag-handle
span.emoji-icon
i.fa.fa-arrows
a.js-select-workspace(data-id="{{id}}")
span.workspace-icon
if icon
+viewer
= icon
else
span.emoji-icon
i.fa.fa-folder
span.workspace-name= name
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}")
span.emoji-icon
i.fa.fa-pencil-square-o
span.workspace-count {{workspaceCount id}}
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
if children
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
// a.board-header-btn.js-open-archived-board
// i.fa.fa-archive
// span {{_ 'archives'}}
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
// i.fa.fa-clone
// span {{_ 'templates'}}

View file

@ -1,7 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import getSlug from 'limax';
const subManager = new SubsManager();
@ -16,10 +14,7 @@ Template.boardList.helpers({
return Utils.isMiniScreen() && Session.get('currentBoard'); */
return true;
},
BoardMultiSelection() {
return BoardMultiSelection;
},
});
})
Template.boardListHeaderBar.events({
'click .js-open-archived-board'() {
@ -27,7 +22,8 @@ Template.boardListHeaderBar.events({
},
});
Template.boardList.events({});
Template.boardList.events({
});
Template.boardListHeaderBar.helpers({
title() {
@ -49,85 +45,17 @@ BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('setting');
Meteor.subscribe('tableVisibilityModeSettings');
this.selectedMenu = new ReactiveVar('starred');
this.selectedWorkspaceIdVar = new ReactiveVar(null);
this.workspacesTreeVar = new ReactiveVar([]);
let currUser = ReactiveCache.getCurrentUser();
let userLanguage;
if (currUser && currUser.profile) {
userLanguage = currUser.profile.language;
userLanguage = currUser.profile.language
}
if (userLanguage) {
TAPi18n.setLanguage(userLanguage);
}
// Load workspaces tree reactively
this.autorun(() => {
const u = ReactiveCache.getCurrentUser();
const tree = (u && u.profile && u.profile.boardWorkspacesTree) || [];
this.workspacesTreeVar.set(tree);
});
},
reorderWorkspaces(draggedSpaceId, targetSpaceId) {
const tree = this.workspacesTreeVar.get();
// Helper to remove a space from tree
const removeSpace = (nodes, id) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === id) {
const removed = nodes.splice(i, 1)[0];
return { tree: nodes, removed };
}
if (nodes[i].children) {
const result = removeSpace(nodes[i].children, id);
if (result.removed) {
return { tree: nodes, removed: result.removed };
}
}
}
return { tree: nodes, removed: null };
};
// Helper to insert a space after target
const insertAfter = (nodes, targetId, spaceToInsert) => {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === targetId) {
nodes.splice(i + 1, 0, spaceToInsert);
return true;
}
if (nodes[i].children) {
if (insertAfter(nodes[i].children, targetId, spaceToInsert)) {
return true;
}
}
}
return false;
};
// Clone the tree
const newTree = EJSON.clone(tree);
// Remove the dragged space
const { tree: treeAfterRemoval, removed } = removeSpace(
newTree,
draggedSpaceId,
);
if (removed) {
// Insert after target
insertAfter(treeAfterRemoval, targetSpaceId, removed);
// Save the new tree
Meteor.call('setWorkspacesTree', treeAfterRemoval, (err) => {
if (err) console.error(err);
});
}
},
onRendered() {
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
// The old sortable code has been removed to prevent conflicts
/* OLD SORTABLE CODE - DISABLED
const itemsSelector = '.js-board:not(.placeholder)';
const $boards = this.$('.js-boards');
@ -144,21 +72,28 @@ BlazeComponent.extendComponent({
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
},
async stop(evt, ui) {
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevBoardDom = ui.item.prev('.js-board').get(0);
const nextBoardDom = ui.item.next('.js-board').get(0);
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
const nextBoardBom = ui.item.next('.js-board').get(0);
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
const boardDomElement = ui.item.get(0);
const board = Blaze.getData(boardDomElement);
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$boards.sortable('cancel');
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
await currentUser.setBoardSortIndex(board._id, sortIndex.base);
}
board.move(sortIndex.base);
},
});
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$boards.sortable({
@ -166,121 +101,56 @@ BlazeComponent.extendComponent({
});
}
});
*/
},
userHasTeams() {
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) return true;
else return false;
if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
return true;
else
return false;
},
teamsDatas() {
const teams = ReactiveCache.getCurrentUser()?.teams;
const teams = ReactiveCache.getCurrentUser()?.teams
if (teams)
return teams.sort((a, b) =>
a.teamDisplayName.localeCompare(b.teamDisplayName),
);
else return [];
return teams.sort((a, b) => a.teamDisplayName.localeCompare(b.teamDisplayName));
else
return [];
},
userHasOrgs() {
if (ReactiveCache.getCurrentUser()?.orgs?.length > 0) return true;
else return false;
if (ReactiveCache.getCurrentUser()?.orgs?.length > 0)
return true;
else
return false;
},
orgsDatas() {
const orgs = ReactiveCache.getCurrentUser()?.orgs;
if (orgs)
return orgs.sort((a, b) =>
a.orgDisplayName.localeCompare(b.orgDisplayName),
);
else return [];
return orgs.sort((a, b) => a.orgDisplayName.localeCompare(b.orgDisplayName));
else
return [];
},
userHasOrgsOrTeams() {
const ret = this.userHasOrgs() || this.userHasTeams();
return ret;
},
currentMenuPath() {
try {
const selectedMenuVar = this.selectedMenu;
if (!selectedMenuVar || typeof selectedMenuVar.get !== 'function') {
return { icon: '🗂️', text: 'Workspaces' };
}
const sel = selectedMenuVar.get();
const currentUser = ReactiveCache.getCurrentUser();
// Helper function to safely get translation or fallback
const safeTranslate = (key, fallback) => {
try {
return TAPi18n.__(key) || fallback;
} catch (e) {
return fallback;
}
};
// Helper to find space by id in tree
const findSpaceById = (nodes, targetId, path = []) => {
if (!nodes || !Array.isArray(nodes)) return null;
for (const node of nodes) {
if (node.id === targetId) {
return [...path, node];
}
if (node.children && node.children.length > 0) {
const result = findSpaceById(node.children, targetId, [
...path,
node,
]);
if (result) return result;
}
}
return null;
};
if (sel === 'starred') {
return { icon: '⭐', text: safeTranslate('allboards.starred', 'Starred') };
} else if (sel === 'templates') {
return { icon: '📋', text: safeTranslate('allboards.templates', 'Templates') };
} else if (sel === 'remaining') {
return { icon: '📂', text: safeTranslate('allboards.remaining', 'Remaining') };
} else {
// sel is a workspaceId, build path
if (!this.workspacesTreeVar || typeof this.workspacesTreeVar.get !== 'function') {
return { icon: '🗂️', text: safeTranslate('allboards.workspaces', 'Workspaces') };
}
const tree = this.workspacesTreeVar.get();
const spacePath = findSpaceById(tree, sel);
if (spacePath && spacePath.length > 0) {
const pathText = spacePath.map((s) => s.name).join(' / ');
return {
icon: '🗂️',
text: `${safeTranslate('allboards.workspaces', 'Workspaces')} / ${pathText}`,
};
}
return { icon: '🗂️', text: safeTranslate('allboards.workspaces', 'Workspaces') };
}
} catch (error) {
console.error('Error in currentMenuPath:', error);
return { icon: '🗂️', text: 'Workspaces' };
}
},
boards() {
let query = {
// { type: 'board' },
// { type: { $in: ['board','template-container'] } },
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ title: { $not: { $regex: /^\^.*\^$/ } } },
],
{ $or: [] },
{ title: { $not: { $regex: /^\^.*\^$/ } } }
]
};
const membershipOrs = [];
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne(
'tableVisibilityMode-allowPrivateOnly',
);
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
if (FlowRouter.getRouteName() === 'home') {
membershipOrs.push({ 'members.userId': Meteor.userId() });
query.$and[2].$or.push({ 'members.userId': Meteor.userId() });
if (
allowPrivateVisibilityOnly !== undefined &&
allowPrivateVisibilityOnly.booleanValue
) {
query.$and.push({ permission: 'private' });
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) {
query.$and.push({ 'permission': 'private' });
}
const currUser = ReactiveCache.getCurrentUser();
@ -292,7 +162,7 @@ BlazeComponent.extendComponent({
// }
//query.$and[2].$or.push({'orgs': {$elemMatch : {orgId: orgsIds[0]}}});
membershipOrs.push({ 'orgs.orgId': { $in: orgsIds } });
query.$and[2].$or.push({ 'orgs.orgId': { $in: orgsIds } });
}
let teamIdsUserBelongs = currUser?.teamIdsUserBelongs() || '';
@ -302,15 +172,10 @@ BlazeComponent.extendComponent({
// query.$or[2].$or.push({'teams.teamId': teamsIds[i]});
// }
//query.$and[2].$or.push({'teams': { $elemMatch : {teamId: teamsIds[0]}}});
membershipOrs.push({ 'teams.teamId': { $in: teamsIds } });
query.$and[2].$or.push({ 'teams.teamId': { $in: teamsIds } });
}
if (membershipOrs.length) {
query.$and.splice(2, 0, { $or: membershipOrs });
}
} else if (
allowPrivateVisibilityOnly !== undefined &&
!allowPrivateVisibilityOnly.booleanValue
) {
}
else if (allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue) {
query = {
archived: false,
//type: { $in: ['board','template-container'] },
@ -319,38 +184,10 @@ BlazeComponent.extendComponent({
};
}
const boards = ReactiveCache.getBoards(query, {});
const currentUser = ReactiveCache.getCurrentUser();
let list = boards;
// Apply left menu filtering
const sel = this.selectedMenu.get();
const assignments =
(currentUser &&
currentUser.profile &&
currentUser.profile.boardWorkspaceAssignments) ||
{};
if (sel === 'starred') {
list = list.filter((b) => currentUser && currentUser.hasStarred(b._id));
} else if (sel === 'templates') {
list = list.filter((b) => b.type === 'template-container');
} else if (sel === 'remaining') {
// Show boards not in any workspace AND not templates
// Keep starred boards visible in Remaining too
list = list.filter(
(b) => !assignments[b._id] && b.type !== 'template-container',
);
} else {
// assume sel is a workspaceId
// Keep starred boards visible in their workspace too
list = list.filter((b) => assignments[b._id] === sel);
}
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
return currentUser.sortBoardsForUser(list);
}
return list
.slice()
.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
const ret = ReactiveCache.getBoards(query, {
sort: { sort: 1 /* boards default sorting */ },
});
return ret;
},
boardLists(boardId) {
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
@ -398,82 +235,11 @@ BlazeComponent.extendComponent({
events() {
return [
{
'click .js-select-menu'(evt) {
const type = evt.currentTarget.getAttribute('data-type');
this.selectedWorkspaceIdVar.set(null);
this.selectedMenu.set(type);
},
'click .js-select-workspace'(evt) {
const id = evt.currentTarget.getAttribute('data-id');
this.selectedWorkspaceIdVar.set(id);
this.selectedMenu.set(id);
},
'click .js-add-workspace'(evt) {
evt.preventDefault();
const name = prompt(
TAPi18n.__('allboards.add-workspace-prompt') || 'New Space name',
);
if (name && name.trim()) {
Meteor.call(
'createWorkspace',
{ parentId: null, name: name.trim() },
(err, res) => {
if (err) console.error(err);
},
);
}
},
'click .js-add-board'(evt) {
// Store the currently selected workspace/menu for board creation
const selectedWorkspaceId = this.selectedWorkspaceIdVar.get();
const selectedMenu = this.selectedMenu.get();
if (selectedWorkspaceId) {
Session.set('createBoardInWorkspace', selectedWorkspaceId);
} else {
Session.set('createBoardInWorkspace', null);
}
// Open different popup based on context
if (selectedMenu === 'templates') {
Popup.open('createTemplateContainer')(evt);
} else {
Popup.open('createBoard')(evt);
}
},
'click .js-add-board': Popup.open('createBoard'),
'click .js-star-board'(evt) {
const boardId = this.currentData()._id;
ReactiveCache.getCurrentUser().toggleBoardStar(boardId);
evt.preventDefault();
evt.stopPropagation();
const boardId = this.currentData()._id;
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
},
// HTML5 DnD from boards to spaces
'dragstart .js-board'(evt) {
const boardId = this.currentData()._id;
// Support multi-drag
if (
BoardMultiSelection.isActive() &&
BoardMultiSelection.isSelected(boardId)
) {
const selectedIds = BoardMultiSelection.getSelectedBoardIds();
try {
evt.originalEvent.dataTransfer.setData(
'text/plain',
JSON.stringify(selectedIds),
);
evt.originalEvent.dataTransfer.setData(
'application/x-board-multi',
'true',
);
} catch (e) {}
} else {
try {
evt.originalEvent.dataTransfer.setData('text/plain', boardId);
} catch (e) {}
}
},
'click .js-clone-board'(evt) {
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
@ -524,115 +290,47 @@ BlazeComponent.extendComponent({
}
});
},
'click .js-multiselection-activate'(evt) {
evt.preventDefault();
if (BoardMultiSelection.isActive()) {
BoardMultiSelection.disable();
} else {
BoardMultiSelection.activate();
}
},
'click .js-multiselection-reset'(evt) {
evt.preventDefault();
BoardMultiSelection.disable();
},
'click .js-toggle-board-multi-selection'(evt) {
evt.preventDefault();
evt.stopPropagation();
const boardId = this.currentData()._id;
BoardMultiSelection.toogle(boardId);
},
'click .js-archive-selected-boards'(evt) {
evt.preventDefault();
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
if (
selectedBoards.length > 0 &&
confirm(TAPi18n.__('archive-board-confirm'))
) {
selectedBoards.forEach((boardId) => {
Meteor.call('archiveBoard', boardId);
});
BoardMultiSelection.reset();
}
},
'click .js-duplicate-selected-boards'(evt) {
evt.preventDefault();
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
if (
selectedBoards.length > 0 &&
confirm(TAPi18n.__('duplicate-board-confirm'))
) {
selectedBoards.forEach((boardId) => {
const board = ReactiveCache.getBoard(boardId);
if (board) {
Meteor.call(
'copyBoard',
boardId,
{
sort: ReactiveCache.getBoards({ archived: false }).length,
type: 'board',
title: board.title,
},
(err, res) => {
if (err) console.error(err);
},
);
}
});
BoardMultiSelection.reset();
}
},
'click #resetBtn'(event) {
let allBoards = document.getElementsByClassName('js-board');
let allBoards = document.getElementsByClassName("js-board");
let currBoard;
for (let i = 0; i < allBoards.length; i++) {
currBoard = allBoards[i];
currBoard.style.display = 'block';
currBoard.style.display = "block";
}
},
'click #filterBtn'(event) {
event.preventDefault();
let selectedTeams = document.querySelectorAll(
'#jsAllBoardTeams option:checked',
);
let selectedTeamsValues = Array.from(selectedTeams).map(
function (elt) {
return elt.value;
},
);
let index = selectedTeamsValues.indexOf('-1');
let selectedTeams = document.querySelectorAll('#jsAllBoardTeams option:checked');
let selectedTeamsValues = Array.from(selectedTeams).map(function (elt) { return elt.value });
let index = selectedTeamsValues.indexOf("-1");
if (index > -1) {
selectedTeamsValues.splice(index, 1);
}
let selectedOrgs = document.querySelectorAll(
'#jsAllBoardOrgs option:checked',
);
let selectedOrgsValues = Array.from(selectedOrgs).map(function (elt) {
return elt.value;
});
index = selectedOrgsValues.indexOf('-1');
let selectedOrgs = document.querySelectorAll('#jsAllBoardOrgs option:checked');
let selectedOrgsValues = Array.from(selectedOrgs).map(function (elt) { return elt.value });
index = selectedOrgsValues.indexOf("-1");
if (index > -1) {
selectedOrgsValues.splice(index, 1);
}
if (selectedTeamsValues.length > 0 || selectedOrgsValues.length > 0) {
const query = {
$and: [{ archived: false }, { type: 'board' }],
$and: [
{ archived: false },
{ type: 'board' },
{ $or: [] }
]
};
const ors = [];
if (selectedTeamsValues.length > 0) {
ors.push({ 'teams.teamId': { $in: selectedTeamsValues } });
query.$and[2].$or.push({ 'teams.teamId': { $in: selectedTeamsValues } });
}
if (selectedOrgsValues.length > 0) {
ors.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
}
if (ors.length) {
query.$and.push({ $or: ors });
query.$and[2].$or.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
}
let filteredBoards = ReactiveCache.getBoards(query, {});
let allBoards = document.getElementsByClassName('js-board');
let allBoards = document.getElementsByClassName("js-board");
let currBoard;
if (filteredBoards.length > 0) {
let currBoardId;
@ -644,312 +342,21 @@ BlazeComponent.extendComponent({
return board._id == currBoardId;
});
if (found !== undefined) currBoard.style.display = 'block';
else currBoard.style.display = 'none';
if (found !== undefined)
currBoard.style.display = "block";
else
currBoard.style.display = "none";
}
} else {
}
else {
for (let i = 0; i < allBoards.length; i++) {
currBoard = allBoards[i];
currBoard.style.display = 'none';
currBoard.style.display = "none";
}
}
}
},
'click .js-edit-workspace'(evt) {
evt.preventDefault();
evt.stopPropagation();
const workspaceId = evt.currentTarget.getAttribute('data-id');
// Find the space in the tree
const findSpace = (nodes, id) => {
for (const node of nodes) {
if (node.id === id) return node;
if (node.children) {
const found = findSpace(node.children, id);
if (found) return found;
}
}
return null;
};
const tree = this.workspacesTreeVar.get();
const space = findSpace(tree, workspaceId);
if (space) {
const newName = prompt(
TAPi18n.__('allboards.edit-workspace-name') || 'Space name:',
space.name,
);
const newIcon = prompt(
TAPi18n.__('allboards.edit-workspace-icon') ||
'Space icon (markdown):',
space.icon || '📁',
);
if (newName !== null && newName.trim()) {
// Update space in tree
const updateSpaceInTree = (nodes, id, updates) => {
return nodes.map((node) => {
if (node.id === id) {
return { ...node, ...updates };
}
if (node.children) {
return {
...node,
children: updateSpaceInTree(node.children, id, updates),
};
}
return node;
});
};
const updatedTree = updateSpaceInTree(tree, workspaceId, {
name: newName.trim(),
icon: newIcon || '📁',
});
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
if (err) console.error(err);
});
}
}
},
'click .js-add-subworkspace'(evt) {
evt.preventDefault();
evt.stopPropagation();
const parentId = evt.currentTarget.getAttribute('data-id');
const name = prompt(
TAPi18n.__('allboards.add-subworkspace-prompt') || 'Subspace name:',
);
if (name && name.trim()) {
Meteor.call(
'createWorkspace',
{ parentId, name: name.trim() },
(err) => {
if (err) console.error(err);
},
);
}
},
'dragstart .workspace-node'(evt) {
const workspaceId =
evt.currentTarget.getAttribute('data-workspace-id');
evt.originalEvent.dataTransfer.effectAllowed = 'move';
evt.originalEvent.dataTransfer.setData(
'application/x-workspace-id',
workspaceId,
);
// Create a better drag image
const dragImage = evt.currentTarget.cloneNode(true);
dragImage.style.position = 'absolute';
dragImage.style.top = '-9999px';
dragImage.style.opacity = '0.8';
document.body.appendChild(dragImage);
evt.originalEvent.dataTransfer.setDragImage(dragImage, 0, 0);
setTimeout(() => document.body.removeChild(dragImage), 0);
evt.currentTarget.classList.add('dragging');
},
'dragend .workspace-node'(evt) {
evt.currentTarget.classList.remove('dragging');
document.querySelectorAll('.workspace-node').forEach((el) => {
el.classList.remove('drag-over');
});
},
'dragover .workspace-node'(evt) {
evt.preventDefault();
evt.stopPropagation();
const draggingEl = document.querySelector('.workspace-node.dragging');
const targetEl = evt.currentTarget;
// Allow dropping boards on any space
// Or allow dropping spaces on other spaces (but not on itself or descendants)
if (
!draggingEl ||
(targetEl !== draggingEl && !draggingEl.contains(targetEl))
) {
evt.originalEvent.dataTransfer.dropEffect = 'move';
targetEl.classList.add('drag-over');
}
},
'dragleave .workspace-node'(evt) {
evt.currentTarget.classList.remove('drag-over');
},
'drop .workspace-node'(evt) {
evt.preventDefault();
evt.stopPropagation();
const targetEl = evt.currentTarget;
targetEl.classList.remove('drag-over');
// Check what's being dropped - board or workspace
const draggedWorkspaceId = evt.originalEvent.dataTransfer.getData(
'application/x-workspace-id',
);
const isMultiBoard = evt.originalEvent.dataTransfer.getData(
'application/x-board-multi',
);
const boardData =
evt.originalEvent.dataTransfer.getData('text/plain');
if (draggedWorkspaceId && !boardData) {
// This is a workspace reorder operation
const targetWorkspaceId =
targetEl.getAttribute('data-workspace-id');
if (draggedWorkspaceId !== targetWorkspaceId) {
this.reorderWorkspaces(draggedWorkspaceId, targetWorkspaceId);
}
} else if (boardData) {
// This is a board assignment operation
// Get the workspace ID directly from the dropped workspace-node's data-workspace-id attribute
const workspaceId = targetEl.getAttribute('data-workspace-id');
if (workspaceId) {
if (isMultiBoard) {
// Multi-board drag
try {
const boardIds = JSON.parse(boardData);
boardIds.forEach((boardId) => {
Meteor.call('assignBoardToWorkspace', boardId, workspaceId);
});
} catch (e) {
// Error parsing multi-board data
}
} else {
// Single board drag
Meteor.call('assignBoardToWorkspace', boardData, workspaceId);
}
}
}
},
'dragover .js-select-menu'(evt) {
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
// Only allow drop on "remaining" menu to unassign boards from spaces
if (menuType === 'remaining') {
evt.originalEvent.dataTransfer.dropEffect = 'move';
evt.currentTarget.classList.add('drag-over');
}
},
'dragleave .js-select-menu'(evt) {
evt.currentTarget.classList.remove('drag-over');
},
'drop .js-select-menu'(evt) {
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
evt.currentTarget.classList.remove('drag-over');
// Only handle drops on "remaining" menu
if (menuType !== 'remaining') return;
const isMultiBoard = evt.originalEvent.dataTransfer.getData(
'application/x-board-multi',
);
const boardData =
evt.originalEvent.dataTransfer.getData('text/plain');
if (boardData) {
if (isMultiBoard) {
// Multi-board drag - unassign all from workspaces
try {
const boardIds = JSON.parse(boardData);
boardIds.forEach((boardId) => {
Meteor.call('unassignBoardFromWorkspace', boardId);
});
} catch (e) {
// Error parsing multi-board data
}
} else {
// Single board drag - unassign from workspace
Meteor.call('unassignBoardFromWorkspace', boardData);
}
}
},
},
];
},
// Helpers for templates
workspacesTree() {
return this.workspacesTreeVar.get();
},
selectedWorkspaceId() {
return this.selectedWorkspaceIdVar.get();
},
isSelectedMenu(type) {
return this.selectedMenu.get() === type;
},
isSpaceSelected(id) {
return this.selectedWorkspaceIdVar.get() === id;
},
menuItemCount(type) {
const currentUser = ReactiveCache.getCurrentUser();
const assignments =
(currentUser &&
currentUser.profile &&
currentUser.profile.boardWorkspaceAssignments) ||
{};
// Get all boards for counting
let query = {
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ $or: [{ 'members.userId': Meteor.userId() }] },
{ title: { $not: { $regex: /^\^.*\^$/ } } },
],
};
const allBoards = ReactiveCache.getBoards(query, {});
if (type === 'starred') {
return allBoards.filter(
(b) => currentUser && currentUser.hasStarred(b._id),
).length;
} else if (type === 'templates') {
return allBoards.filter((b) => b.type === 'template-container').length;
} else if (type === 'remaining') {
// Count boards not in any workspace AND not templates
// Include starred boards (they appear in both Starred and Remaining)
return allBoards.filter(
(b) => !assignments[b._id] && b.type !== 'template-container',
).length;
}
return 0;
},
workspaceCount(workspaceId) {
const currentUser = ReactiveCache.getCurrentUser();
const assignments =
(currentUser &&
currentUser.profile &&
currentUser.profile.boardWorkspaceAssignments) ||
{};
// Get all boards for counting
let query = {
$and: [
{ archived: false },
{ type: { $in: ['board', 'template-container'] } },
{ $or: [{ 'members.userId': Meteor.userId() }] },
{ title: { $not: { $regex: /^\^.*\^$/ } } },
],
};
const allBoards = ReactiveCache.getBoards(query, {});
// Count boards directly assigned to this space (not including children)
return allBoards.filter((b) => assignments[b._id] === workspaceId).length;
},
canModifyBoards() {
const currentUser = ReactiveCache.getCurrentUser();
return currentUser && !currentUser.isCommentOnly();
},
hasBoardsSelected() {
return BoardMultiSelection.count() > 0;
},
}).register('boardList');

View file

@ -3,7 +3,6 @@ template(name="miniboard")
class="minicard-{{colorClass}}")
.minicard-title
.handle
span.drag-handle(title="{{_ 'dragBoard'}}")
i.fa.fa-arrows
.fa.fa-arrows
+viewer
= title

View file

@ -164,32 +164,32 @@
margin: 5px 0;
padding: 10px;
}
.original-positions-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.original-positions-header .btn {
justify-content: center;
}
.original-positions-filters .btn-group {
justify-content: center;
}
.original-position-item-header {
flex-wrap: wrap;
gap: 6px;
}
.entity-name {
flex: 1;
min-width: 0;
word-break: break-word;
}
.original-position-item-details {
margin-left: 0;
margin-top: 8px;
@ -203,60 +203,60 @@
border-color: #4a5568;
color: #e2e8f0;
}
.original-positions-content {
background-color: #1a202c;
border-color: #4a5568;
}
.original-position-item {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-item:hover {
background-color: #4a5568;
border-color: #718096;
}
.original-position-item-header {
color: #e2e8f0;
}
.original-position-item-header i {
color: #a0aec0;
}
.entity-name {
color: #e2e8f0;
}
.entity-id {
color: #a0aec0;
}
.original-position-description {
color: #e2e8f0;
}
.original-title {
background-color: #4a5568;
color: #a0aec0;
}
.original-title strong {
color: #e2e8f0;
}
.original-position-date {
color: #a0aec0;
}
.no-original-positions {
color: #a0aec0;
}
.no-original-positions i {
color: #718096;
}

View file

@ -5,7 +5,7 @@
<i class="fa fa-history"></i>
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
</button>
{{#if isShowingOriginalPositions}}
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
<i class="fa fa-refresh"></i> Refresh
@ -22,22 +22,22 @@
{{else}}
<div class="original-positions-filters">
<div class="btn-group btn-group-sm" role="group">
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'all'}}">
All
</button>
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'swimlane'}}">
<i class="fa fa-bars"></i> Swimlanes
</button>
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'list'}}">
<i class="fa fa-columns"></i> Lists
</button>
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'card'}}">
<i class="fa fa-sticky-note"></i> Cards

View file

@ -26,7 +26,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
if (!boardId) return;
this.isLoading.set(true);
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
this.isLoading.set(false);
if (error) {
@ -57,11 +57,11 @@ class OriginalPositionsViewComponent extends BlazeComponent {
getFilteredHistory() {
const history = this.getBoardHistory();
const filterType = this.filterType.get();
if (filterType === 'all') {
return history;
}
return history.filter(item => item.entityType === filterType);
}
@ -93,7 +93,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
getEntityOriginalPositionDescription(entity) {
const position = entity.originalPosition || {};
let description = `Position: ${position.sort || 0}`;
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
description += ` in swimlane ${entity.originalSwimlaneId}`;
} else if (entity.entityType === 'card') {
@ -104,7 +104,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
description += ` in list ${entity.originalListId}`;
}
}
return description;
}

View file

@ -55,12 +55,6 @@
flex-direction: row;
align-items: center;
}
.attachment-actions a {
margin-left: 16px;
}
.attachment-actions a:first-child {
margin-left: 0;
}
.add-attachment {
display: flex;
align-items: center;
@ -112,9 +106,6 @@
color: white;
cursor: pointer;
font-size: 4em;
position: absolute;
right: 50px;
top: 16px;
}
/* Upload progress indicators for drag-and-drop uploads */
@ -250,6 +241,10 @@
.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;
}
.attachment-arrow {
font-size: 4em;
@ -258,20 +253,6 @@
align-self: center;
margin: 0 20px;
}
#prev-attachment {
font-size: 4em;
color: white;
cursor: pointer;
align-self: center;
margin-left: 70px;
}
#next-attachment {
font-size: 4em;
color: white;
cursor: pointer;
align-self: center;
margin-right: 70px;
}
#viewer-content {
display: flex;
justify-content: center;
@ -285,13 +266,6 @@
max-width: 100%;
max-height: 100%;
}
#video-viewer {
max-width: 100%;
max-height: 100%;
}
#audio-viewer {
max-width: 100%;
}
#pdf-viewer {
width: 40vw;
height: 100%;
@ -326,19 +300,9 @@
}
#prev-attachment {
left: 0;
position: absolute;
bottom: 2.2em;
font-size: 1.6em;
padding: 16px;
margin-left: 0;
}
#next-attachment {
right: 0;
position: absolute;
bottom: 2.2em;
font-size: 1.6em;
padding: 16px;
margin-right: 0;
}
#pdf-viewer {
width: 100%;
@ -372,3 +336,36 @@
margin-top: 10px;
}
}
/* Attachment migration styles */
.attachment-item.migrating {
position: relative;
opacity: 0.7;
}
.attachment-migration-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 4px;
}
.migration-spinner {
font-size: 24px;
color: #007cba;
margin-bottom: 8px;
}
.migration-text {
font-size: 12px;
color: #666;
text-align: center;
}

View file

@ -34,11 +34,10 @@ template(name="attachmentViewer")
#viewer-overlay.hidden
#viewer-top-bar
span#attachment-name
a#viewer-close
i.fa.fa-times-thin
a#viewer-close ❌
#viewer-container
i.fa.fa-caret-left#prev-attachment
| ◀️
#viewer-content
img#image-viewer.hidden
video#video-viewer.hidden(controls="true")
@ -46,7 +45,7 @@ template(name="attachmentViewer")
object#pdf-viewer.hidden(type="application/pdf")
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
object#txt-viewer.hidden(type="text/plain")
i.fa.fa-caret-right#next-attachment
| ▶️
template(name="attachmentGallery")
@ -54,7 +53,7 @@ template(name="attachmentGallery")
if canModifyCard
a.attachment-item.add-attachment.js-add-attachment
i.fa.fa-plus
|
each attachments
@ -88,21 +87,22 @@ template(name="attachmentGallery")
span.file-size ({{fileSize size}})
.attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
i.fa.fa-arrow-down
| ⬇️
if currentUser.isBoardMember
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.js-rename(title="{{_ 'rename'}}")
i.fa.fa-pencil-square-o
| ✏️
a.js-confirm-delete(title="{{_ 'delete'}}")
i.fa.fa-trash
| 🗑️
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
i.fa.fa-bars
| ☰
// Migration spinner overlay
if isAttachmentMigrating _id
.attachment-migration-overlay
.migration-spinner
i.fa.fa-cog.fa-spin
| ⚙️
.migration-text {{_ 'migrating-attachment'}}
template(name="attachmentActionsPopup")
@ -110,12 +110,16 @@ template(name="attachmentActionsPopup")
li
if isImage
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-picture-o
| {{#if isCover}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}
| 📖
| 🖼️
if isCover
| {{_ 'remove-cover'}}
else
| {{_ 'add-cover'}}
if currentUser.isBoardAdmin
if isImage
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
i.fa.fa-picture-o
| 🖼️
if isBackgroundImage
| {{_ 'remove-background-image'}}
else
@ -123,19 +127,19 @@ template(name="attachmentActionsPopup")
if $neq versions.original.storage "fs"
a.js-move-storage-fs
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-fs'}}
if $neq versions.original.storage "gridfs"
if versions.original.storage
a.js-move-storage-gridfs
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-gridfs'}}
if $neq versions.original.storage "s3"
if versions.original.storage
a.js-move-storage-s3
i.fa.fa-arrow-right
| ▶️
| {{_ 'attachment-move-storage-s3'}}
template(name="attachmentRenamePopup")

View file

@ -6,10 +6,10 @@ template(name="cardCustomFieldsPopup")
span.full-name
= name
if hasCustomField
i.fa.fa-check
| ✅
hr
a.quiet-button.full.js-settings
i.fa.fa-cog
| ⚙️
span {{_ 'settings'}}
template(name="cardCustomField")
@ -55,11 +55,10 @@ template(name="cardCustomField-number")
template(name="cardCustomField-checkbox")
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
if canModifyCard
span.check-box-unicode
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
.check-box-container
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
else
span.check-box-unicode
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
template(name="cardCustomField-currency")
if canModifyCard

View file

@ -1,26 +1,26 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
formatDateTime,
formatDate,
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import { CustomFieldStringTemplate } from '/client/lib/customFields'
@ -112,7 +112,6 @@ CardCustomField.register('cardCustomField');
events() {
return [
{
'click .js-checklist-item .check-box-unicode': this.toggleItem,
'click .js-checklist-item .check-box-container': this.toggleItem,
},
];

View file

@ -95,61 +95,37 @@ template(name="minicardCustomFieldDate")
| {{showWeek}}
template(name="editCardReceivedDatePopup")
.datepicker-container
form.edit-date
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
input.js-time-field#time(type="time" name="time" value=showTime)
if error.get
.warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
form.edit-card-received-date
.datepicker
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}
template(name="editCardStartDatePopup")
.datepicker-container
form.edit-date
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
input.js-time-field#time(type="time" name="time" value=showTime)
if error.get
.warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
form.edit-card-start-date
.datepicker
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}
template(name="editCardDueDatePopup")
.datepicker-container
form.edit-date
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
input.js-time-field#time(type="time" name="time" value=showTime)
if error.get
.warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
form.edit-card-due-date
.datepicker
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}
template(name="editCardEndDatePopup")
.datepicker-container
form.edit-date
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
.right
label(for="time") {{_ 'time'}}
input.js-time-field#time(type="time" name="time" value=showTime)
if error.get
.warning {{_ error.get}}
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
form.edit-card-end-date
.datepicker
.clear-date
a.js-clear-date {{_ 'clear'}}
.datepicker-actions
button.primary.wide.left(type="submit") {{_ 'save'}}
button.js-delete-date.negate.wide.right {{_ 'delete'}}

View file

@ -1,24 +1,24 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar,
diff
} from '/imports/lib/dateUtils';
@ -47,6 +47,11 @@ import {
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
}
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
this.card.setStart(formatDateTime(date));
}
@ -63,6 +68,11 @@ import {
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
}
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
this.card.setDue(formatDateTime(date));
}
@ -79,6 +89,11 @@ import {
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
}
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) {
this.card.setEnd(formatDateTime(date));
}
@ -143,7 +158,7 @@ class CardReceivedDate extends CardDate {
const startAt = this.data().getStart();
const theDate = this.date.get();
const now = this.now.get();
// Received date logic: if received date is after start, due, or end dates, it's overdue
if (
(startAt && isAfter(theDate, startAt)) ||
@ -187,7 +202,7 @@ class CardStartDate extends CardDate {
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// Start date logic: if start date is after due or end dates, it's overdue
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
@ -230,7 +245,7 @@ class CardDueDate extends CardDate {
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// If there's an end date and it's before the due date, task is completed early
if (endAt && isBefore(endAt, theDate)) {
classes += 'completed-early';
@ -242,7 +257,7 @@ class CardDueDate extends CardDate {
// Due date logic based on current time
else {
const daysDiff = diff(theDate, now, 'days');
if (daysDiff < 0) {
// Due date is in the past - overdue
classes += 'overdue';
@ -254,7 +269,7 @@ class CardDueDate extends CardDate {
classes += 'not-due';
}
}
return classes;
}
@ -286,7 +301,7 @@ class CardEndDate extends CardDate {
let classes = 'end-date ';
const dueAt = this.data().getDue();
const theDate = this.date.get();
if (!dueAt) {
// No due date set - just show as completed
classes += 'completed';
@ -371,7 +386,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardReceivedDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -383,7 +398,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardStartDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -395,7 +410,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardDueDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -407,7 +422,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardEndDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -419,7 +434,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardCustomFieldDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';

View file

@ -17,10 +17,10 @@ BlazeComponent.extendComponent({
events() {
return [
{
async 'submit .js-card-description'(event) {
'submit .js-card-description'(event) {
event.preventDefault();
const description = this.currentComponent().getValue();
await this.data().setDescription(description);
this.data().setDescription(description);
},
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt) {

View file

@ -1,25 +1,23 @@
/* Date Format Selector */
.card-details-item-date-format {
margin-bottom: 12px;
margin-bottom: 10px;
}
.card-details-item-date-format .card-details-item-title {
font-size: 15px;
font-size: 14px;
font-weight: bold;
margin-bottom: 6px;
margin-bottom: 5px;
color: #333;
letter-spacing: 0.03em;
}
.card-details-item-date-format .js-date-format-selector {
width: 100%;
padding: 9px 10px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 5px;
border-radius: 4px;
background-color: #fff;
font-size: 15px;
font-size: 14px;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
.card-details-item-date-format .js-date-format-selector:focus {
@ -29,18 +27,18 @@
}
.assignee {
border-radius: 3px;
display: block;
position: relative;
float: left;
height: clamp(24px, 3.5vw, 36px);
width: clamp(24px, 3.5vw, 36px);
margin: 0.3vh;
height: 30px;
width: 30px;
margin: .3vh;
cursor: pointer;
user-select: none;
z-index: 1;
text-decoration: none;
border-radius: 50%;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.04);
}
.assignee .avatar {
overflow: hidden;
@ -53,18 +51,12 @@
background-color: #dbdbdb;
color: #444;
position: absolute;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.assignee .avatar.avatar-image {
object-fit: cover;
object-position: center;
height: 100%;
width: 100%;
display: block;
}
.assignee .assignee-presence-status {
background-color: #b3b3b3;
@ -75,6 +67,7 @@
position: absolute;
right: -1px;
bottom: -1px;
border: 1px solid #fff;
z-index: 15;
}
.assignee .assignee-presence-status.active {
@ -98,7 +91,6 @@
align-items: center;
justify-content: center;
box-shadow: 0 0 0 2px #bfbfbf inset;
transition: box-shadow 0.12s;
}
.assignee.add-assignee:hover,
.assignee.add-assignee.is-active {
@ -110,83 +102,22 @@
background-color: rgba(0,0,0,0.875);
color: #fff;
border-radius: 0.7vw;
font-size: 0.98em;
}
.card-details {
padding: 0;
flex-shrink: 0;
flex-basis: min(600px, 80vw);
will-change: flex-basis;
overflow-y: auto;
overflow-y: scroll;
overflow-x: hidden;
background: #f7f7f7;
border-radius: 0 0 0.4vw 0.4vw;
border-radius: bottom 0.4vw;
z-index: 30;
animation: flexGrowIn 0.1s;
box-shadow: 0 0 0.9vh 0 #b3b3b3;
transition: flex-basis 0.1s, box-shadow 0.15s;
transition: flex-basis 0.1s;
box-sizing: border-box;
}
/* Desktop mode: position card below board header */
body.desktop-mode .card-details:not(.card-details-popup) {
position: fixed;
width: auto;
max-width: 800px;
flex-basis: auto;
border-radius: 8px;
z-index: 100;
}
/* Default position for first card or when dragged */
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
top: 50px;
left: 20px;
right: 20px;
bottom: 20px;
}
/* Stagger positions for multiple cards using nth-of-type */
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
top: 50px;
left: 20px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
top: 80px;
left: 50px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
top: 110px;
left: 80px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
top: 140px;
left: 110px;
}
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
top: 170px;
left: 140px;
}
/* For expanded cards, set dimensions */
body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
right: 20px;
bottom: 20px;
}
/* Collapsed card state - hide content and set height to title row only */
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
display: none !important;
}
.card-details.card-details-collapsed {
height: auto !important;
bottom: auto !important;
overflow: visible;
}
body.desktop-mode .card-details.card-details-collapsed {
bottom: auto !important;
}
.card-details .mCustomScrollBox {
padding-left: 0;
}
@ -196,47 +127,18 @@ body.desktop-mode .card-details.card-details-collapsed {
}
.card-details .card-details-header {
margin: 0 -20px 5px;
padding: 8px 20px;
padding: 7px 20px;
background: #ededed;
border-bottom: 1px solid #dbdbdb;
position: sticky;
top: 0px;
z-index: 500;
display: flow-root;
min-height: 44px;
}
.card-details .card-details-header .card-number {
color: #b3b3b3;
display: inline-block;
margin-right: 6px;
}
/* Collapse toggle triangle */
.card-details .card-details-header .card-collapse-toggle {
float: left;
font-size: 20px;
padding: 7px 10px;
margin-left: -10px;
margin-right: 5px;
cursor: pointer;
user-select: none;
color: #000;
vertical-align: middle;
line-height: 1.2;
}
.card-details .card-details-header .card-drag-handle {
font-size: 20px;
padding: 8px 10px;
margin-right: 10px;
cursor: move;
user-select: none;
display: inline-block;
float: right;
vertical-align: middle;
line-height: 1.2;
}
.card-details .card-details-header .close-card-details,
.card-details .card-details-header .maximize-card-details,
.card-details .card-details-header .minimize-card-details,
@ -254,19 +156,11 @@ body.desktop-mode .card-details.card-details-collapsed {
font-size: 24px;
padding: 5px 10px 5px 10px;
margin-right: -8px;
cursor: pointer;
user-select: none;
vertical-align: middle;
line-height: 1.2;
transition: color 0.13s;
}
.card-details .card-details-header .close-card-details-mobile-web,
.card-details .card-details-header .card-mobile-desktop-toggle {
.card-details .card-details-header .close-card-details-mobile-web {
font-size: 24px;
padding: 5px;
margin-right: 5px;
cursor: pointer;
user-select: none;
margin-right: 40px;
}
.card-details .card-details-header .card-copy-button {
font-size: 17px;
@ -281,44 +175,12 @@ body.desktop-mode .card-details.card-details-collapsed {
.card-details .card-details-header .card-details-menu {
font-size: 17px;
padding: 10px;
vertical-align: middle;
line-height: 1.2;
}
.card-details .card-details-header .card-details-menu-mobile-web {
font-size: 17px;
padding: 10px;
margin-right: 30px;
}
.card-details .card-details-header .card-mobile-desktop-toggle,
.card-details .card-details-header .card-zoom-in,
.card-details .card-details-header .card-zoom-out {
font-size: 24px;
padding: 5px 10px 5px 10px;
margin-right: 5px;
cursor: pointer;
user-select: none;
float: right;
}
/* Unify all card text to match title size */
.card-details {
font-size: 1em;
}
.card-details p,
.card-details span,
.card-details div,
.card-details a,
.card-details label,
.card-details input,
.card-details textarea,
.card-details select,
.card-details button,
.card-details .card-details-item-title,
.card-details .card-label,
.card-details .viewer {
font-size: inherit;
line-height: 1.5;
}
.card-details .card-details-header .card-details-watch {
font-size: 17px;
padding-left: 7px;
@ -326,13 +188,9 @@ body.desktop-mode .card-details.card-details-collapsed {
}
.card-details .card-details-header .card-details-title {
font-weight: bold;
font-size: 1.35em;
font-size: 1.33em;
margin: 7px 0 0;
padding: 0;
display: inline-block;
vertical-align: middle;
line-height: 1.3;
letter-spacing: 0.01em;
}
.card-details .card-details-header .linked-card-location {
font-style: italic;
@ -347,10 +205,10 @@ body.desktop-mode .card-details.card-details-collapsed {
margin-bottom: 10px;
}
.card-details .card-details-header form.inlined-form .copied-tooltip {
padding: 0 10px;
padding: 0px 10px;
}
.card-details .card-details-header .card-details-list {
font-size: 0.9em;
font-size: 0.85em;
margin-bottom: 3px;
}
.card-details .card-details-header .card-details-list a.card-details-list-title {
@ -360,7 +218,7 @@ body.desktop-mode .card-details.card-details-collapsed {
display: inline-block;
background: #e6e6e6;
border-radius: 3px;
padding: 0 5px;
padding: 0px 5px;
}
.card-details .card-details-header .copied-tooltip {
margin-right: 10px;
@ -371,13 +229,11 @@ body.desktop-mode .card-details.card-details-collapsed {
}
.card-details .card-description textarea {
min-height: 100px;
resize: vertical;
}
.card-details .card-details-items {
display: flex;
flex-wrap: wrap;
margin: 15px 0;
gap: 0.5em;
}
.card-details .card-details-items .card-details-item {
margin-right: 0.5em;
@ -428,28 +284,15 @@ body.desktop-mode .card-details.card-details-collapsed {
position: fixed;
resize: both;
}
/* Override for mobile mode even on larger screens */
body.mobile-mode .card-details {
width: 100vw !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
height: 100vh !important;
max-height: 100vh !important;
resize: none !important;
}
.card-details-maximized {
padding: 0;
flex-shrink: 0;
flex-basis: calc(100% - 20px);
will-change: flex-basis;
overflow-y: auto;
overflow-x: auto;
overflow-y: scroll;
overflow-x: scroll;
background: #f7f7f7;
border-radius: 0 0 3px 3px;
border-radius: bottom 3px;
z-index: 100;
animation: flexGrowIn 0.1s;
box-shadow: 0 0 7px 0 #b3b3b3;
@ -492,52 +335,19 @@ input[type="submit"].attachment-add-link-submit {
}
@media screen and (max-width: 800px) {
.card-details {
width: 100% !important;
padding: 0 !important;
margin: 0 !important;
width: calc(100% - 1px);
padding: 0px 20px 0px 20px;
margin: 0px;
transition: none;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 100 !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0 !important;
box-shadow: none !important;
}
/* Ensure card details are above everything on mobile */
body.mobile-mode .card-details {
z-index: 100 !important;
width: 100vw !important;
left: 0 !important;
right: 0 !important;
overflow-y: revert;
overflow-x: revert;
}
.card-details .card-details-canvas {
width: 100%;
padding-left: 0px;
padding: 0 15px;
}
.card-details .card-details-header .close-card-details {
margin-right: 0px;
display: block !important;
}
.card-details .card-details-header .close-card-details-mobile-web {
display: block !important;
margin-right: 5px !important;
}
.card-details .card-details-header .card-mobile-desktop-toggle {
display: block !important;
margin-right: 5px !important;
}
.card-details .card-details-header .card-mobile-desktop-toggle {
display: block !important;
margin-right: 5px !important;
}
.card-details .card-details-header .card-details-menu {
margin-right: 40px;
@ -563,62 +373,6 @@ input[type="submit"].attachment-add-link-submit {
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
margin: 0;
}
/* iPhone mobile: enlarge header buttons and increase spacing */
body.mobile-mode.iphone-device .card-details .card-details-header {
padding-right: 16px;
}
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
font-size: 2em !important; /* 2x bigger */
padding: 0.3em !important;
margin-right: 0.75em !important; /* 2x space compared to default */
margin-left: 0 !important;
}
/* Avoid clipping of the close button on the right edge */
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
margin-right: 0.75em !important;
}
/* Enlarge the header title too */
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
font-size: 1.2em !important;
font-weight: bold;
}
}
/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
body.mobile-mode .card-details {
width: 100vw !important;
padding: 0px !important;
margin: 0px !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 100 !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0 !important;
box-shadow: none !important;
overflow-y: auto !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
}
body.mobile-mode .card-details .card-details-canvas {
width: 100% !important;
padding: 0 15px !important;
}
body.mobile-mode .card-details .card-details-header .close-card-details,
body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
display: block !important;
}
.card-details-white {
background: #fff !important;
@ -727,15 +481,13 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
.vote-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.vote-title .js-edit-date {
align-self: flex-start;
margin-left: 6px;
align-self: baseline;
margin-left: 5px;
}
.vote-result {
display: flex;
gap: 6px;
}
.js-show-positive-votes {
cursor: pointer;
@ -746,33 +498,29 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
.poker-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.poker-title .js-edit-date {
align-self: flex-start;
margin-left: 6px;
align-self: baseline;
margin-left: 5px;
}
.poker-result {
display: flex;
flex-wrap: wrap;
gap: 7px;
flex-flow: row wrap;
}
.js-show-positive-poker-votes {
cursor: pointer;
}
.poker-deck {
display: grid;
grid-auto-flow: row;
flex-direction: column;
text-align: center;
gap: 6px;
}
.poker-card-result {
width: 34px;
width: 32px;
font-size: 1em;
font-weight: bold;
padding: 4px 2px;
padding: 4px 2px 4px 2px;
cursor: default;
border-radius: 3px;
}
.winner {
font-weight: bold;
@ -783,7 +531,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
}
.responsive-table {
overflow-x: auto;
width: 100%;
}
.poker-table {
display: table;
@ -846,15 +593,11 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
margin: auto;
margin-right: 10px;
width: 100px;
border-radius: 2px;
padding: 3px 6px;
}
.estimation-add button {
display: inline-block;
float: right;
margin: auto;
border-radius: 2px;
padding: 3px 10px;
}
.poker-card {
width: 48px;
@ -873,7 +616,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
text-align: center;
position: relative;
cursor: pointer;
transition: box-shadow 0.12s;
}
.poker-card .inner {
display: table-cell;

View file

@ -5,65 +5,48 @@ template(name="cardDetails")
+attachmentViewer
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
+inlinedForm(classNames="js-card-details-title")
+editCardTitleForm
else
unless isMiniScreen
unless isPopup
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
if cardCollapsed
i.fa.fa-caret-right
else
i.fa.fa-caret-down
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
i.fa.fa-times-thin
if cardMaximized
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| ❌
if canModifyCard
if cardMaximized
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
if canModifyCard
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
| ☰
a.card-copy-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon
i.fa.fa-link
if canModifyCard
span.card-drag-handle.js-card-drag-handle(title="Drag card")
i.fa.fa-arrows
span.copied-tooltip {{_ 'copied'}}
else
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
i.fa.fa-times-thin
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
i.fa.fa-search-minus
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
i.fa.fa-search-plus
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
if mobileMode
i.fa.fa-desktop
else
i.fa.fa-mobile
if cardMaximized
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon
i.fa.fa-link
span.copied-tooltip {{_ 'copied'}}
unless isPopup
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy"
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.copied-tooltip {{_ 'copied'}}
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
+viewer
if currentBoard.allowsCardNumber
span.card-number
@ -71,7 +54,7 @@ template(name="cardDetails")
= getTitle
if isWatching
i.card-details-watch
i.fa.fa-eye
| 👁️
.card-details-path
each parentList
| &nbsp; &gt; &nbsp;
@ -93,7 +76,7 @@ template(name="cardDetails")
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}}")
@ -102,11 +85,11 @@ template(name="cardDetails")
.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
@ -115,7 +98,7 @@ template(name="cardDetails")
if currentBoard.allowsLabels
.card-details-item.card-details-item-labels
h3.card-details-item-title
i.fa.fa-tags
| 🏷️
| {{_ 'labels'}}
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
each labels
@ -125,14 +108,14 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
i.fa.fa-plus
|
if currentBoard.hasAnyAllowsDate
hr
.card-details-item.card-details-item-date-format
h3.card-details-item-title
i.fa.fa-calendar
| 📅
| {{_ 'date-format'}}
.card-details-item-content
select.js-date-format-selector
@ -143,7 +126,7 @@ template(name="cardDetails")
if currentBoard.allowsReceivedDate
.card-details-item.card-details-item-received
h3.card-details-item-title
i.fa.fa-sign-out
| 📥
| {{_ 'card-received'}}
if getReceived
+cardReceivedDate
@ -151,12 +134,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-received-date
i.fa.fa-plus
|
if currentBoard.allowsStartDate
.card-details-item.card-details-item-start
h3.card-details-item-title
i.fa.fa-hourglass-start
| 🚀
| {{_ 'card-start'}}
if getStart
+cardStartDate
@ -164,12 +147,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-start-date
i.fa.fa-plus
|
if currentBoard.allowsDueDate
.card-details-item.card-details-item-due
h3.card-details-item-title
i.fa.fa-clock-o
| ⏰
| {{_ 'card-due'}}
if getDue
+cardDueDate
@ -177,12 +160,12 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-due-date
i.fa.fa-plus
|
if currentBoard.allowsEndDate
.card-details-item.card-details-item-end
h3.card-details-item-title
i.fa.fa-hourglass-end
| 🏁
| {{_ 'card-end'}}
if getEnd
+cardEndDate
@ -190,7 +173,7 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-end-date
i.fa.fa-plus
|
if currentBoard.hasAnyAllowsUser
hr
@ -198,7 +181,7 @@ template(name="cardDetails")
if currentBoard.allowsCreator
.card-details-item.card-details-item-creator
h3.card-details-item-title
i.fa.fa-user
| 👤
| {{_ 'creator'}}
+userAvatar(userId=userId noRemove=true)
@ -208,7 +191,7 @@ template(name="cardDetails")
if currentBoard.allowsMembers
.card-details-item.card-details-item-members
h3.card-details-item-title
i.fa.fa-users
| 👤s
| {{_ 'members'}}
each userId in getMembers
+userAvatar(userId=userId cardId=_id)
@ -216,30 +199,30 @@ template(name="cardDetails")
if canModifyCard
unless currentUser.isWorker
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
i.fa.fa-plus
|
//if assigneeSelected
if currentBoard.allowsAssignee
.card-details-item.card-details-item-assignees
h3.card-details-item-title
i.fa.fa-user
| 👤
| {{_ 'assignee'}}
each userId in getAssignees
+userAvatar(userId=userId cardId=_id assignee=true)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
|
if currentUser.isWorker
unless assigneeSelected
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
|
//.card-details-items
if currentBoard.allowsRequestedBy
.card-details-item.card-details-item-name
h3.card-details-item-title
i.fa.fa-shopping-cart
| 🛒
| {{_ 'requested-by'}}
if canModifyCard
unless currentUser.isWorker
@ -259,7 +242,7 @@ template(name="cardDetails")
if currentBoard.allowsAssignedBy
.card-details-item.card-details-item-name
h3.card-details-item-title
i.fa.fa-user-plus
| 👤-plus
| {{_ 'assigned-by'}}
if canModifyCard
unless currentUser.isWorker
@ -282,7 +265,7 @@ template(name="cardDetails")
if currentBoard.allowsCardSortingByNumber
.card-details-item.card-details-sort-order
h3.card-details-item-title
i.fa.fa-sort-numeric-asc
| 🔢
| {{_ 'sort'}}
if canModifyCard
+inlinedForm(classNames="js-card-details-sort")
@ -295,7 +278,7 @@ template(name="cardDetails")
if currentBoard.allowsShowLists
.card-details-item.card-details-show-lists
h3.card-details-item-title
i.fa.fa-list
| 📋
| {{_ 'list'}}
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
each currentBoard.lists
@ -321,7 +304,7 @@ template(name="cardDetails")
hr
.card-details-item.card-details-item-customfield
h3.card-details-item-title
i.fa.fa-list
| 📋-alt
= definition.name
+cardCustomField
@ -339,7 +322,7 @@ template(name="cardDetails")
.vote-title
div.flex
h3
i.fa.fa-thumbs-up
| 👍
| {{_ 'vote-question'}}
if getVoteEnd
+voteEndDate
@ -351,14 +334,13 @@ template(name="cardDetails")
.card-label.card-label-green {{ voteCountPositive }}
.card-label.card-label-red {{ voteCountNegative }}
unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
.card-label.card-label-gray
| {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
.card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
+viewer
= getVoteQuestion
if showVotingButtons
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}")
if voteState
i.fa.fa-thumbs-up
| 👍
| {{_ 'vote-for-it'}}
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
if $eq voteState false
@ -370,7 +352,7 @@ template(name="cardDetails")
.poker-title
div.flex
h3
i.fa.fa-thumbs-up
| 👍
| {{_ 'poker-question'}}
if getPokerEnd
+pokerEndDate
@ -378,60 +360,59 @@ template(name="cardDetails")
.poker-result
if expiredPoker
unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
.card-label.card-label-gray
| {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
.card-label.card-label-gray {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
if showPlanningPokerButtons
.poker-result
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
if $eq pokerState "one"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
if $eq pokerState "two"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
if $eq pokerState "three"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
if $eq pokerState "five"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
if $eq pokerState "eight"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
if $eq pokerState "thirteen"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
if $eq pokerState "twenty"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
if $eq pokerState "forty"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
if $eq pokerState "oneHundred"
i.fa.fa-check
| ✅
.poker-deck
.poker-card
span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
if $eq pokerState "unsure"
i.fa.fa-check
| ✅
if currentUser.isBoardAdmin
button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
@ -561,7 +542,7 @@ template(name="cardDetails")
button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
div.estimation-add
button.js-poker-estimation
i.fa.fa-plus
|
| {{_ 'set-estimation'}}
input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
@ -571,7 +552,7 @@ template(name="cardDetails")
if currentBoard.allowsDescriptionTitle
hr
h3.card-details-item-title
i.fa.fa-file-text-o
| 📝
| {{_ 'description'}}
if currentBoard.allowsDescriptionText
+inlinedCardDescription(classNames="card-description js-card-description")
@ -582,7 +563,7 @@ template(name="cardDetails")
else
if currentBoard.allowsDescriptionText
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
| ✏️
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
if getDescription
+viewer
@ -612,7 +593,7 @@ template(name="cardDetails")
if currentBoard.allowsAttachments
hr
h3.card-details-item-title
i.fa.fa-paperclip
| 📎
| {{_ 'attachments'}}
if Meteor.settings.public.attachmentsUploadMaxSize
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
@ -628,24 +609,22 @@ template(name="cardDetails")
unless currentUser.isNoComments
.comment-title
h3.card-details-item-title
i.fa.fa-comment-o
| 💬
| {{_ 'comments'}}
if currentBoard.allowsComments
if currentUser.isBoardMember
unless currentUser.isNoComments
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
+commentForm
+commentForm
+comments
hr
.card-details-right
if currentUser.isBoardAdmin
unless currentUser.isNoComments
.activity-title
h3.card-details-item-title
i.fa.fa-history
| 📜
| {{ _ 'activities'}}
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'show-activities'}}")
@ -655,7 +634,7 @@ template(name="cardDetails")
input.toggle-switch(type="checkbox" id="toggleShowActivitiesCard")
label.toggle-label(for="toggleShowActivitiesCard")
if currentUser.isBoardAdmin
unless currentUser.isNoComments
if isLoaded.get
if isLinkedCard
+activities(card=this mode="linkedcard")
@ -696,10 +675,10 @@ template(name="cardDetailsActionsPopup")
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye
| 👁️-slash
| {{_ 'watch'}}
hr
if canModifyCard
@ -710,16 +689,16 @@ template(name="cardDetailsActionsPopup")
//li: a.js-attachments {{_ 'card-edit-attachments'}}
li
a.js-start-voting
i.fa.fa-thumbs-up
| 👍
| {{_ 'card-edit-voting'}}
li
a.js-start-planning-poker
i.fa.fa-thumbs-up
| 👍
| {{_ 'card-edit-planning-poker'}}
if currentUser.isBoardAdmin
li
a.js-custom-fields
i.fa.fa-list
| 📋-alt
| {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
@ -727,260 +706,114 @@ template(name="cardDetailsActionsPopup")
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
li
a.js-spent-time
i.fa.fa-clock-o
| 🕐
| {{_ 'editCardSpentTimePopup-title'}}
li
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'}}
if canModifyCard
hr
else
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
hr
ul.pop-over-list
li
a.js-export-card
i.fa.fa-upload
| 📤
| {{_ 'export-card'}}
unless canModifyCard
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
ul.pop-over-list
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-clipboard
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-archive
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
if canModifyCard
hr
ul.pop-over-list
hr
ul.pop-over-list
li
a.js-move-card-to-top
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
i.fa.fa-arrow-right
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
i.fa.fa-clipboard
| {{_ 'copyCardPopup-title'}}
a.js-move-card
| ➡️
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-copy
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-archive
| {{_ 'archive-card'}}
li
a.js-copy-card
| 📋
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
| 📋
| 📋
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
a.js-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
| 🔗
| {{_ 'cardMorePopup-title'}}
template(name="exportCardPopup")
ul.pop-over-list
li
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
i.fa.fa-upload
| 📤
| {{_ 'export-card-pdf'}}
template(name="moveCardPopup")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
+copyAndMoveCard
template(name="copyCardPopup")
label(for='copy-card-title') {{_ 'title'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
= getTitle
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
+copyAndMoveCard
template(name="copyManyCardsPopup")
label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
| {{_ 'copyManyCardsPopup-format'}}
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
+copyAndMoveCard
template(name="convertChecklistItemToCardPopup")
label(for='convert-checklist-item-to-card-title') {{_ 'title'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
= item.title
+copyAndMoveCard
template(name="copyAndMoveCard")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
@ -991,13 +824,13 @@ template(name="cardMembersPopup")
each members
li.item(class="{{#if isCardMember}}active{{/if}}")
a.name.js-select-member(href="#")
+userAvatar(userId=userId)
+userAvatar(userId=user._id)
span.full-name
= userData.profile.fullname
if userData.username
| (#{userData.username})
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardMember
i.fa.fa-check
| ✅
template(name="cardAssigneesPopup")
input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
unless currentUser.isWorker
@ -1005,13 +838,12 @@ template(name="cardAssigneesPopup")
each members
li.item(class="{{#if isCardAssignee}}active{{/if}}")
a.name.js-select-assignee(href="#")
+userAvatar(userId=userId)
+userAvatar(userId=user._id)
span.full-name
= userData.profile.fullname
if userData.username
| (#{userData.username})
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardAssignee
i.fa.fa-check
| ✅
if currentUser.isWorker
ul.pop-over-list.js-card-assignee-list
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
@ -1019,10 +851,10 @@ template(name="cardAssigneesPopup")
+userAvatar(userId=currentUser._id)
span.full-name
= currentUser.profile.fullname
if currentUser.username
| (#{currentUser.username})
| (<span class="username">{{ currentUser.username }}</span>)
if currentUser.isCardAssignee
i.fa.fa-check
| ✅
template(name="cardAssigneePopup")
.board-assignee-menu
.mini-profile-info
@ -1045,7 +877,7 @@ template(name="cardMorePopup")
span.clearfix
span {{_ 'link-card'}}
= ' '
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
.copied-tooltip {{_ 'copied'}}
@ -1083,14 +915,13 @@ template(name="cardMorePopup")
template(name="setCardColorPopup")
form.edit-label
.palette-colors
each colors
unless $eq color 'white'
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
.palette-colors: each colors
unless $eq color 'white'
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
template(name="cardDeletePopup")
p {{_ "card-delete-pop"}}
@ -1122,12 +953,12 @@ template(name="cardStartVotingPopup")
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
span {{_ 'vote-public'}}
.check-div.flex
i.fa.fa-clock-o
| ⏰
a.js-end-date
span
| {{_ 'card-end'}}
unless getVoteEnd
i.fa.fa-plus
|
if getVoteEnd
+voteEndDate
@ -1168,12 +999,12 @@ template(name="cardStartPlanningPokerPopup")
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
span {{_ 'allowNonBoardMembers'}}
.check-div.flex
i.fa.fa-clock-o
| ⏰
a.js-end-date
span
| {{_ 'card-end'}}
unless getPokerEnd
i.fa.fa-plus
|
if getPokerEnd
+pokerEndDate

View file

@ -1,26 +1,25 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import Boards from '/models/boards';
@ -32,7 +31,6 @@ import CardComments from '/models/cardComments';
import { ALLOWED_COLORS } from '/config/const';
import { UserAvatar } from '../users/userAvatar';
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
@ -65,11 +63,7 @@ BlazeComponent.extendComponent({
const boardBody = this.parentComponent().parentComponent();
//in Miniview parent is Board, not BoardBody.
if (boardBody !== null) {
// Only show overlay in mobile mode, not in desktop mode
const isMobile = Utils.getMobileMode();
if (isMobile) {
boardBody.showOverlay.set(true);
}
boardBody.showOverlay.set(true);
boardBody.mouseHasEnterCardDetails = false;
}
}
@ -87,7 +81,6 @@ BlazeComponent.extendComponent({
isWatching() {
const card = this.currentData();
if (!card || typeof card.findWatcher !== 'function') return false;
return card.findWatcher(Meteor.userId());
},
@ -100,23 +93,6 @@ BlazeComponent.extendComponent({
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
},
showActivities() {
const user = ReactiveCache.getCurrentUser();
return user && user.hasShowActivities();
},
cardCollapsed() {
const user = ReactiveCache.getCurrentUser();
if (user && user.profile) {
return !!user.profile.cardCollapsed;
}
if (Users.getPublicCardCollapsed) {
const stored = Users.getPublicCardCollapsed();
if (typeof stored === 'boolean') return stored;
}
return false;
},
presentParentTask() {
let result = this.currentBoard.presentParentTask;
if (result === null || result === undefined) {
@ -169,9 +145,8 @@ BlazeComponent.extendComponent({
* @return is the list id the current list id ?
*/
isCurrentListId(listId) {
const data = this.data();
if (!data || typeof data.listId === 'undefined') return false;
return data.listId == listId;
const ret = this.data().listId == listId;
return ret;
},
onRendered() {
@ -321,110 +296,12 @@ BlazeComponent.extendComponent({
return [
{
...events,
'click .js-card-collapse-toggle'() {
const user = ReactiveCache.getCurrentUser();
const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
if (user) {
Meteor.call('setCardCollapsed', !currentState);
} else if (Users.setPublicCardCollapsed) {
Users.setPublicCardCollapsed(!currentState);
}
},
'mousedown .js-card-drag-handle'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
},
'mousedown .js-card-title-drag-handle'(event) {
// Allow dragging from title for ReadOnly users
// Don't interfere with text selection
if (event.target.tagName === 'A' || $(event.target).closest('a').length > 0) {
return; // Don't drag if clicking on links
}
event.preventDefault();
const $card = $(event.target).closest('.card-details');
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
},
'click .js-close-card-details'() {
// Get board ID from either the card data or current board in session
const card = this.currentData() || this.data();
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
const cardId = card && card._id;
if (boardId) {
// In desktop mode, remove from openCards array
const isMobile = Utils.getMobileMode();
if (!isMobile && cardId) {
const openCards = Session.get('openCards') || [];
const filtered = openCards.filter(id => id !== cardId);
Session.set('openCards', filtered);
// If this was the current card, clear it
if (Session.get('currentCard') === cardId) {
Session.set('currentCard', null);
}
// Don't navigate away in desktop mode - just close the card
return;
}
// Mobile mode: Clear the current card session to close the card
Session.set('currentCard', null);
// Navigate back to board without card
const board = ReactiveCache.getBoard(boardId);
if (board) {
FlowRouter.go('board', {
id: board._id,
slug: board.slug,
});
}
}
Utils.goBoardId(this.data().boardId);
},
'click .js-copy-link'(event) {
event.preventDefault();
const url = this.data().absoluteUrl();
const promise = Utils.copyTextToClipboard(url);
const promise = Utils.copyTextToClipboard(event.target.href);
const $tooltip = this.$('.card-details-header .copied-tooltip');
Utils.showCopied(promise, $tooltip);
@ -434,46 +311,18 @@ BlazeComponent.extendComponent({
Meteor.call('changeDateFormat', dateFormat);
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
// Mobile: switch to desktop popup view (maximize)
'click .js-mobile-switch-to-desktop'(event) {
event.preventDefault();
// Switch global mode to desktop so the card appears as desktop popup
Utils.setMobileMode(false);
},
'click .js-card-zoom-in'(event) {
event.preventDefault();
const current = Utils.getCardZoom();
const newZoom = Math.min(3.0, current + 0.1);
Utils.setCardZoom(newZoom);
},
'click .js-card-zoom-out'(event) {
event.preventDefault();
const current = Utils.getCardZoom();
const newZoom = Math.max(0.5, current - 0.1);
Utils.setCardZoom(newZoom);
},
'click .js-card-mobile-desktop-toggle'(event) {
event.preventDefault();
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'click .js-card-mobile-desktop-toggle'(event) {
event.preventDefault();
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
async 'submit .js-card-description'(event) {
'submit .js-card-description'(event) {
event.preventDefault();
const description = this.currentComponent().getValue();
await this.data().setDescription(description);
this.data().setDescription(description);
},
async 'submit .js-card-details-title'(event) {
'submit .js-card-details-title'(event) {
event.preventDefault();
const title = this.currentComponent().getValue().trim();
if (title) {
await this.data().setTitle(title);
this.data().setTitle(title);
} else {
await this.data().setTitle('');
this.data().setTitle('');
}
},
'submit .js-card-details-assigner'(event) {
@ -500,23 +349,23 @@ BlazeComponent.extendComponent({
this.find('button[type=submit]').click();
}
},
async 'submit .js-card-details-sort'(event) {
'submit .js-card-details-sort'(event) {
event.preventDefault();
const sort = parseFloat(this.currentComponent()
.getValue()
.trim());
if (!Number.isNaN(sort)) {
let card = this.data();
await card.move(card.boardId, card.swimlaneId, card.listId, sort);
card.move(card.boardId, card.swimlaneId, card.listId, sort);
}
},
async 'change .js-select-card-details-lists'(event) {
'change .js-select-card-details-lists'(event) {
let card = this.data();
const listSelect = this.$('.js-select-card-details-lists')[0];
const listId = listSelect.options[listSelect.selectedIndex].value;
const minOrder = card.getMinSort(listId, card.swimlaneId);
await card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
},
'click .js-go-to-linked-card'() {
Utils.goCardId(this.data().linkedId);
@ -554,8 +403,11 @@ BlazeComponent.extendComponent({
Session.set('cardDetailsIsDragging', false);
Session.set('cardDetailsIsMouseDown', false);
},
async 'click #toggleHideCheckedChecklistItems'() {
await this.data().toggleHideCheckedChecklistItems();
'click #toggleShowActivitiesCard'() {
this.data().toggleShowActivities();
},
'click #toggleHideCheckedChecklistItems'() {
this.data().toggleHideCheckedChecklistItems();
},
'click #toggleCustomFieldsGridButton'() {
Meteor.call('toggleCustomFieldsGrid');
@ -578,57 +430,56 @@ BlazeComponent.extendComponent({
) {
newState = forIt;
}
// Use secure server method; direct client updates to vote are blocked
Meteor.call('cards.vote', this.data()._id, newState);
this.data().setVote(Meteor.userId(), newState);
},
'click .js-poker'(e) {
let newState = null;
if ($(e.target).hasClass('js-poker-vote-one')) {
newState = 'one';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-two')) {
newState = 'two';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-three')) {
newState = 'three';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-five')) {
newState = 'five';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-eight')) {
newState = 'eight';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-thirteen')) {
newState = 'thirteen';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-twenty')) {
newState = 'twenty';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-forty')) {
newState = 'forty';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
newState = 'oneHundred';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
if ($(e.target).hasClass('js-poker-vote-unsure')) {
newState = 'unsure';
Meteor.call('cards.pokerVote', this.data()._id, newState);
this.data().setPoker(Meteor.userId(), newState);
}
},
'click .js-poker-finish'(e) {
if ($(e.target).hasClass('js-poker-finish')) {
e.preventDefault();
const now = new Date();
Meteor.call('cards.setPokerEnd', this.data()._id, now);
const now = formatDateTime(new Date());
this.data().setPokerEnd(now);
}
},
@ -636,9 +487,9 @@ BlazeComponent.extendComponent({
if ($(e.target).hasClass('js-poker-replay')) {
e.preventDefault();
this.currentCard = this.currentData();
Meteor.call('cards.replayPoker', this.currentCard._id);
Meteor.call('cards.unsetPokerEnd', this.currentCard._id);
Meteor.call('cards.unsetPokerEstimation', this.currentCard._id);
this.currentCard.replayPoker();
this.data().unsetPokerEnd();
this.data().unsetPokerEstimation();
}
},
'click .js-poker-estimation'(event) {
@ -649,9 +500,9 @@ BlazeComponent.extendComponent({
this.find('#pokerEstimation').value = '';
if (ruleTitle) {
Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10));
this.data().setPokerEstimation(parseInt(ruleTitle, 10));
} else {
Meteor.call('cards.unsetPokerEstimation', this.data()._id);
this.data().setPokerEstimation('');
}
}
},
@ -831,7 +682,6 @@ Template.editCardSortOrderForm.onRendered(function () {
Template.cardDetailsActionsPopup.helpers({
isWatching() {
if (!this || typeof this.findWatcher !== 'function') return false;
return this.findWatcher(Meteor.userId());
},
@ -862,21 +712,21 @@ Template.cardDetailsActionsPopup.events({
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
'click .js-copy-checklist-cards': Popup.open('copyManyCards'),
'click .js-set-card-color': Popup.open('setCardColor'),
async 'click .js-move-card-to-top'(event) {
'click .js-move-card-to-top'(event) {
event.preventDefault();
const minOrder = this.getMinSort();
await this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
Popup.back();
},
async 'click .js-move-card-to-bottom'(event) {
'click .js-move-card-to-bottom'(event) {
event.preventDefault();
const maxOrder = this.getMaxSort();
await this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
Popup.back();
},
'click .js-archive': Popup.afterConfirm('cardArchive', async function () {
'click .js-archive': Popup.afterConfirm('cardArchive', function () {
Popup.close();
await this.archive();
this.archive();
Utils.goBoardId(this.boardId);
}),
'click .js-more': Popup.open('cardMore'),
@ -928,12 +778,6 @@ Template.cardMembersPopup.onCreated(function () {
});
Template.cardMembersPopup.events({
'click .js-select-member'(event) {
const card = Utils.getCurrentCard();
const memberId = this.userId;
card.toggleMember(memberId);
event.preventDefault();
},
'keyup .card-members-filter'(event) {
const members = filterMembers(event.target.value);
Template.instance().members.set(members);
@ -941,23 +785,8 @@ Template.cardMembersPopup.events({
});
Template.cardMembersPopup.helpers({
isCardMember() {
const card = Template.parentData();
const cardMembers = card.getMembers();
return _.contains(cardMembers, this.userId);
},
members() {
const members = Template.instance().members.get();
const uniqueMembers = _.uniq(members, 'userId');
return _.sortBy(uniqueMembers, member => {
const user = ReactiveCache.getUser(member.userId);
return user ? user.profile.fullname : '';
});
},
userData() {
return ReactiveCache.getUser(this.userId);
return _.sortBy(Template.instance().members.get(),'fullname');
},
});
@ -1006,81 +835,36 @@ Template.editCardAssignerForm.events({
});
/** Move Card Dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
async setDone(cardId, options) {
// Capture DOM values immediately before any async operations
const position = this.$('input[name="position"]:checked').val();
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// If no card selected, move to end
const maxSort = card.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
const minOrder = card.getMinSort(listId, swimlaneId);
card.move(boardId, swimlaneId, listId, minOrder - 1);
}
}).register('moveCardPopup');
/** Copy Card Dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
async setDone(cardId, options) {
// Capture DOM values immediately before any async operations
const textarea = this.$('#copy-card-title');
const title = textarea.val().trim();
const position = this.$('input[name="position"]:checked').val();
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
// const textarea = $('#copy-card-title');
const textarea = this.$('#copy-card-title');
const title = textarea.val().trim();
if (title) {
const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title});
// Position the copied card (newCard may be null for cross-board copies
// if the client hasn't received the publication update yet)
if (newCardId) {
const newCard = ReactiveCache.getCard(newCardId);
if (newCard) {
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// If no card selected, copy to end
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
}
}
// insert new card to the top of new list
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, true, {title: title});
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@ -1092,46 +876,29 @@ Template.editCardAssignerForm.events({
}).register('copyCardPopup');
/** Convert Checklist-Item to card dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
async setDone(cardId, options) {
// Capture DOM values immediately before any async operations
const textarea = this.$('#copy-card-title');
const title = textarea.val().trim();
const position = this.$('input[name="position"]:checked').val();
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
const textarea = this.$('#copy-card-title');
const title = textarea.val().trim();
if (title) {
const _id = Cards.insert({
title: title,
listId: options.listId,
boardId: options.boardId,
swimlaneId: options.swimlaneId,
listId: listId,
boardId: boardId,
swimlaneId: swimlaneId,
sort: 0,
});
const newCard = ReactiveCache.getCard(_id);
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
const card = ReactiveCache.getCard(_id);
const minOrder = card.getMinSort();
card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
Filter.addException(_id);
}
@ -1139,46 +906,22 @@ Template.editCardAssignerForm.events({
}).register('convertChecklistItemToCardPopup');
/** Copy many cards dialog */
(class extends DialogWithBoardSwimlaneListCard {
(class extends DialogWithBoardSwimlaneList {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
return ret;
}
async setDone(cardId, options) {
// Capture DOM values immediately before any async operations
const textarea = this.$('#copy-card-title');
const title = textarea.val().trim();
const position = this.$('input[name="position"]:checked').val();
setDone(boardId, swimlaneId, listId, options) {
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
const card = this.data();
const textarea = this.$('#copy-card-title');
const title = textarea.val().trim();
if (title) {
const titleList = JSON.parse(title);
for (const obj of titleList) {
const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description});
// Position the copied card
if (newCardId) {
const newCard = ReactiveCache.getCard(newCardId);
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
}
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, false, {title: obj.title, description: obj.description});
// In case the filter is active we need to add the newly inserted card in
// the list of exceptions -- cards that are not filtered. Otherwise the
@ -1213,14 +956,14 @@ BlazeComponent.extendComponent({
'click .js-palette-color'() {
this.currentColor.set(this.currentData().color);
},
async 'click .js-submit'(event) {
'click .js-submit'(event) {
event.preventDefault();
await this.currentCard.setColor(this.currentColor.get());
this.currentCard.setColor(this.currentColor.get());
Popup.back();
},
async 'click .js-remove-color'(event) {
'click .js-remove-color'(event) {
event.preventDefault();
await this.currentCard.setColor(null);
this.currentCard.setColor(null);
Popup.back();
},
},
@ -1228,51 +971,6 @@ BlazeComponent.extendComponent({
},
}).register('setCardColorPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentColor = new ReactiveVar(null);
},
colors() {
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
},
isSelected(color) {
return this.currentColor.get() === color;
},
events() {
return [
{
'click .js-palette-color'(event) {
// Extract color from class name like "card-details-red"
const classes = $(event.currentTarget).attr('class').split(' ');
const colorClass = classes.find(cls => cls.startsWith('card-details-'));
const color = colorClass ? colorClass.replace('card-details-', '') : null;
this.currentColor.set(color);
},
async 'click .js-submit'(event) {
event.preventDefault();
const color = this.currentColor.get();
// Use MultiSelection to get selected cards and set color on each
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
await card.setColor(color);
}
Popup.back();
},
async 'click .js-remove-color'(event) {
event.preventDefault();
// Use MultiSelection to get selected cards and remove color from each
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
await card.setColor(null);
}
Popup.back();
},
},
];
},
}).register('setSelectionColorPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentCard = this.currentData();
@ -1407,15 +1105,20 @@ BlazeComponent.extendComponent({
'is-checked',
);
const endString = this.currentCard.getVoteEnd();
Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
this.currentCard.setVoteQuestion(
voteQuestion,
publicVote,
allowNonBoardMembers,
);
if (endString) {
Meteor.call('cards.setVoteEnd', this.currentCard._id, endString);
this.currentCard.setVoteEnd(endString);
}
Popup.back();
},
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
event.preventDefault();
Meteor.call('cards.unsetVote', this.currentCard._id);
this.currentCard.unsetVote();
Popup.back();
}),
'click a.js-toggle-vote-public'(event) {
@ -1473,13 +1176,13 @@ BlazeComponent.extendComponent({
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
@ -1614,10 +1317,10 @@ BlazeComponent.extendComponent({
];
}
_storeDate(newDate) {
Meteor.call('cards.setVoteEnd', this.card._id, newDate);
this.card.setVoteEnd(newDate);
}
_deleteDate() {
Meteor.call('cards.unsetVoteEnd', this.card._id);
this.card.unsetVoteEnd();
}
}.register('editVoteEndDatePopup'));
@ -1639,14 +1342,17 @@ BlazeComponent.extendComponent({
);
const endString = this.currentCard.getPokerEnd();
Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers);
this.currentCard.setPokerQuestion(
pokerQuestion,
allowNonBoardMembers,
);
if (endString) {
Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString));
this.currentCard.setPokerEnd(endString);
}
Popup.back();
},
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
Meteor.call('cards.unsetPoker', this.currentCard._id);
this.currentCard.unsetPoker();
Popup.back();
}),
'click a.js-toggle-poker-allow-non-members'(event) {
@ -1725,13 +1431,13 @@ BlazeComponent.extendComponent({
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
@ -1867,17 +1573,17 @@ BlazeComponent.extendComponent({
];
}
_storeDate(newDate) {
Meteor.call('cards.setPokerEnd', this.card._id, newDate);
this.card.setPokerEnd(newDate);
}
_deleteDate() {
Meteor.call('cards.unsetPokerEnd', this.card._id);
this.card.unsetPokerEnd();
}
}.register('editPokerEndDatePopup'));
// Close the card details pane by pressing escape
EscapeActions.register(
'detailsPane',
async () => {
() => {
// if card description diverges from database due to editing
// ask user whether changes should be applied
if (ReactiveCache.getCurrentUser()) {
@ -1885,7 +1591,7 @@ EscapeActions.register(
currentDescription = document.getElementsByClassName("editor js-new-description-input").item(0)
if (currentDescription?.value && !(currentDescription.value === Utils.getCurrentCard().getDescription())) {
if (confirm(TAPi18n.__('rescue-card-description-dialogue'))) {
await Utils.getCurrentCard().setDescription(document.getElementsByClassName("editor js-new-description-input").item(0).value);
Utils.getCurrentCard().setDescription(document.getElementsByClassName("editor js-new-description-input").item(0).value);
// Save it!
console.log(document.getElementsByClassName("editor js-new-description-input").item(0).value);
console.log("current description", Utils.getCurrentCard().getDescription());
@ -1942,15 +1648,10 @@ Template.cardAssigneesPopup.helpers({
},
members() {
const members = Template.instance().members.get();
const uniqueMembers = _.uniq(members, 'userId');
return _.sortBy(uniqueMembers, member => {
const user = ReactiveCache.getUser(member.userId);
return user ? user.profile.fullname : '';
});
return _.sortBy(Template.instance().members.get(),'fullname');
},
userData() {
user() {
return ReactiveCache.getUser(this.userId);
},
});

View file

@ -37,12 +37,10 @@ textarea.js-edit-checklist-item {
.checklist-progress-bar-container .checklist-progress-bar {
width: 80%;
height: 10px;
background-color: #e0e0e0;
border-radius: 16px;
}
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
color: #fff;
background-color: #666;
color: #fff !important;
background-color: #2196f3 !important;
padding: 0.01em 16px;
border-radius: 16px;
height: 100%;
@ -74,6 +72,10 @@ textarea.js-edit-checklist-item {
padding-top: 3px;
float: left;
}
.checklist-title span.fa.checklist-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
#card-details-overlay {
top: 0;
bottom: -600px;
@ -150,6 +152,10 @@ textarea.js-edit-checklist-item {
padding-top: 2px;
padding-right: 10px;
}
.checklist-item span.fa.checklistitem-handle.fa-arrows::before {
content: "↕️" !important;
font-family: inherit !important;
}
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {
margin: 0 0 0.5em 1.33em;

View file

@ -1,15 +1,14 @@
template(name="checklists")
.checklists-title
h3.card-details-item-title
i.fa.fa-check
| ✅
| {{_ 'checklists'}}
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId
position="top")
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
+addChecklistItemForm
else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
i.fa.fa-plus
|
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
//span.toggle-switch-title
@ -29,7 +28,7 @@ template(name="checklists")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
else
a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}")
i.fa.fa-plus
|
template(name="checklistDetail")
.js-checklist.checklist.nodragscroll
@ -39,7 +38,7 @@ template(name="checklistDetail")
.checklist-title
span
if canModifyCard
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
if canModifyCard
h4.title.js-open-inlined-form.is-editable
@ -64,12 +63,13 @@ template(name="checklistDeletePopup")
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="addChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-add-checklist-item(rows='1' autofocus)
.edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
| ❌
if showNewlineBecomesNewChecklistItem
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
@ -82,7 +82,7 @@ template(name="addChecklistItemForm")
| {{_ 'originOrder'}}
template(name="editChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item'
@ -91,12 +91,13 @@ template(name="editChecklistItemForm")
= checklist.title
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
| ❌
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
a.js-delete-checklist-item {{_ "delete"}}...
a.js-convert-checklist-item-to-card
i.fa.fa-copy
| 📋
| {{_ 'convertChecklistItemToCardPopup-title'}}
template(name="checklistItems")
@ -106,7 +107,7 @@ template(name="checklistItems")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
|
.checklist-items.js-checklist-items
each item in checklist.items
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
@ -118,7 +119,7 @@ template(name="checklistItems")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true)
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
|
template(name='checklistItemDetail')
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
@ -126,8 +127,7 @@ template(name='checklistItemDetail')
if canModifyCard
.check-box-container
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
if isTouchScreenOrShowDesktopDragHandles
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
@ -141,16 +141,16 @@ template(name="checklistActionsPopup")
ul.pop-over-list
li
a.js-delete-checklist.delete-checklist
i.fa.fa-trash
| 🗑️
| {{_ "delete"}} ...
a.js-move-checklist.move-checklist
i.fa.fa-arrow-right
| ➡️
| {{_ "moveChecklist"}} ...
a.js-copy-checklist.copy-checklist
i.fa.fa-copy
| 📋
| {{_ "copyChecklist"}} ...
a.js-hide-checked-checklist-items
i.fa.fa-eye-slash
| 🙈
| {{_ "hideCheckedChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
if checklist.hideCheckedChecklistItems
@ -159,7 +159,7 @@ template(name="checklistActionsPopup")
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
a.js-hide-all-checklist-items
i.fa.fa-ban
| 🚫
| {{_ "hideAllChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
if checklist.hideAllChecklistItems

View file

@ -157,7 +157,7 @@ BlazeComponent.extendComponent({
textarea.focus();
},
async deleteItem() {
deleteItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
@ -372,9 +372,9 @@ BlazeComponent.extendComponent({
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
return ret;
}
async setDone(cardId, options) {
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
await this.data().checklist.move(cardId);
this.data().checklist.move(cardId);
}
}).register('moveChecklistPopup');
@ -384,8 +384,8 @@ BlazeComponent.extendComponent({
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
return ret;
}
async setDone(cardId, options) {
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
await this.data().checklist.copy(cardId);
this.data().checklist.copy(cardId);
}
}).register('copyChecklistPopup');

View file

@ -223,13 +223,9 @@
.card-label-edit-button:hover {
background: #dbdbdb;
}
ul.edit-labels-pop-over span.label-handle {
ul.edit-labels-pop-over span.fa.label-handle {
padding-right: 10px;
display: inline-block;
width: 1.2em;
text-align: center;
color: #999;
}
ul.edit-labels-pop-over span.label-handle + .card-label {
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
max-width: 180px;
}

View file

@ -6,7 +6,7 @@ template(name="formLabel")
.palette-colors: each labels
span.card-label.palette-color.js-palette-color(class="card-label-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
template(name="createLabelPopup")
form.create-label
@ -28,7 +28,8 @@ template(name="cardLabelsPopup")
ul.edit-labels-pop-over
each board.labels
li.js-card-label-item
a.card-label-edit-button.fa.fa-pencil.js-edit-label
a.card-label-edit-button.js-edit-label
| ✏️
if isTouchScreenOrShowDesktopDragHandles
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
@ -36,5 +37,5 @@ template(name="cardLabelsPopup")
+viewer
= name
if(isLabelSelected ../_id)
i.card-label-selectable-icon.fa.fa-check
| ✅
a.quiet-button.full.js-add-label {{_ 'label-create'}}

View file

@ -125,8 +125,19 @@ Template.createLabelPopup.events({
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
board.addLabel(name, color);
// Find the selected color by looking for the palette color that contains the checkmark
let selectedColor = null;
templateInstance.$('.js-palette-color').each(function() {
if ($(this).text().includes('✅')) {
selectedColor = Blaze.getData(this).color;
return false; // break out of loop
}
});
if (selectedColor) {
board.addLabel(name, selectedColor);
}
Popup.back();
},
});
@ -144,8 +155,19 @@ Template.editLabelPopup.events({
.$('#labelName')
.val()
.trim();
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
board.editLabel(this._id, name, color);
// Find the selected color by looking for the palette color that contains the checkmark
let selectedColor = null;
templateInstance.$('.js-palette-color').each(function() {
if ($(this).text().includes('✅')) {
selectedColor = Blaze.getData(this).color;
return false; // break out of loop
}
});
if (selectedColor) {
board.editLabel(this._id, name, selectedColor);
}
Popup.back();
},
});

View file

@ -45,10 +45,9 @@
}
.minicard-details-menu-with-handle {
float: right;
padding-left: 0.7vw;
font-size: clamp(14px, 3vw, 18px);
padding: 0;
z-index: 1;
padding-right: 4vw;
padding-left: 0.7vw;
}
.minicard-details-menu {
float: right;
@ -98,7 +97,6 @@
}
.minicard .minicard-labels {
float: none;
margin-right: 6vw;
}
.minicard .minicard-labels .minicard-label {
width: clamp(12px, 1.5vw, 16px);
@ -113,7 +111,6 @@
}
.minicard .minicard-custom-fields {
display: block;
margin-right: 6vw;
}
.minicard .minicard-custom-field {
display: flex;
@ -136,25 +133,18 @@
width: clamp(20px, 2.5vw, 28px);
height: clamp(20px, 2.5vw, 28px);
position: absolute;
right: 0vw;
top: 4vh;
right: 0.7vw;
top: 0.7vh;
display: none;
z-index: 1;
}
@media only screen {
.minicard .handle {
display: block;
}
}
.minicard .handle .drag-handle {
.minicard .handle .fa-arrows {
font-size: clamp(16px, 3vw, 20px);
color: #ccc;
display: inline-block;
width: 1.4em;
text-align: center;
}
.minicard .minicard-title {
margin-right: 1.5vw;
}
.minicard .minicard-title .card-number {
color: #b3b3b3;
@ -174,10 +164,6 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
position: relative;
z-index: 5;
margin-right: 6vw;
clear: both;
}
.minicard .date {
margin-right: 0.4vw;
@ -311,6 +297,19 @@
background-color: #1976d2 !important;
}
/* Font Awesome icons in minicard dates */
.minicard .card-date i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
/* Font Awesome icons in minicard spent time */
.minicard .card-time i.fa {
margin-right: 0.3vw;
font-size: 0.9em;
vertical-align: middle;
}
.minicard .badges {
float: left;
margin-top: 1vh;
@ -742,80 +741,7 @@
gap: 0.3vw;
}
/* Checklist display on minicard */
.minicard-checklist {
width: 100%;
margin-top: 0.5vh;
margin-bottom: 0.5vh;
padding: 0.3vh 0.5vw;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0.3vw;
border: 1px solid #e0e0e0;
}
.minicard-checklist .checklist-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.3vh;
}
.minicard-checklist .checklist-title {
.minicard-list-name i.fa {
font-size: 0.8em;
font-weight: bold;
color: #4d4d4d;
flex: 1;
}
.minicard-checklist .checklist-menu {
font-size: 1.2em;
color: #666;
cursor: pointer;
padding: 0 0.3vw;
border-radius: 0.2vw;
transition: background-color 0.2s;
}
.minicard-checklist .checklist-menu:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.minicard-checklist .checklist-item {
font-size: 0.75em;
color: #666;
margin-bottom: 0.2vh;
display: flex;
align-items: flex-start;
gap: 0.3vw;
line-height: 1.2;
cursor: pointer;
padding: 0.2vh 0;
border-radius: 0.2vw;
transition: background-color 0.2s;
}
.minicard-checklist .checklist-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.minicard-checklist .checklist-item.is-checked {
text-decoration: line-through;
color: #999;
}
.minicard-checklist .checklist-item .check-box-unicode {
flex-shrink: 0;
font-size: 0.8em;
margin-top: 0.1vh;
transition: transform 0.2s;
}
.minicard-checklist .checklist-item:hover .check-box-unicode {
transform: scale(1.1);
}
.minicard-checklist .checklist-item .item-title {
flex: 1;
word-wrap: break-word;
overflow-wrap: break-word;
opacity: 0.7;
}

View file

@ -3,13 +3,10 @@ template(name="minicard")
class="{{#if isLinkedCard}}linked-card{{/if}}"
class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
if canMoveCard
if isTouchScreenOrShowDesktopDragHandles
.handle
i.fa.fa-arrows
if canModifyCard
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
.handle
| ↕️
.dates
if getReceived
.date
@ -33,7 +30,7 @@ template(name="minicard")
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}}")
@ -42,11 +39,11 @@ template(name="minicard")
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-warning
| ⚠️
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
| ✅
span {{_ 'upload-completed'}}
.minicard-title
@ -58,15 +55,12 @@ template(name="minicard")
| {{ parentCardName }}
if isLinkedBoard
a.js-linked-link
span.linked-icon
i.fa.fa-folder
span.linked-icon | 📁
else if isLinkedCard
a.js-linked-link
span.linked-icon
i.fa.fa-id-card
span.linked-icon | 🃏
if getArchived
span.linked-icon.linked-archived
i.fa.fa-archive
span.linked-icon.linked-archived | 📦
+viewer
if currentBoard.allowsCardNumber
span.card-number
@ -147,53 +141,45 @@ template(name="minicard")
if canModifyCard
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text
i.fa.fa-comment-o
span.badge-icon.badge-comment.badge-text 💬
= ' '
= comments.length
//span.badge-comment.badge-text
//|
{{_ 'comment'}}
//| {{_ 'comment'}}
if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription)
span.badge-icon
i.fa.fa-file-text-o
span.badge-icon 📝
if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon(class="{{#if voteState}}text-green{{/if}}")
i.fa.fa-thumbs-up
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") 👍
span.badge-text {{ voteCountPositive }}
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}")
i.fa.fa-thumbs-down
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") 👎
span.badge-text {{ voteCountNegative }}
if getPokerQuestion
.badge.badge-state-image-only(title=getPokerQuestion)
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}")
i.fa.fa-check-square
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") ✅
if expiredPoker
span.badge-text {{ getPokerEstimation }}
if attachments.length
if currentBoard.allowsBadgeAttachmentOnMinicard
.badge
span.badge-icon
i.fa.fa-paperclip
span.badge-icon 📎
span.badge-text= attachments.length
if checklists.length
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
span.badge-icon ☑️
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
span.badge-icon 🌐
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if currentBoard.allowsCardSortingByNumber
if currentBoard.allowsCardSortingByNumberOnMinicard
.badge
span.badge-icon
i.fa.fa-sort-numeric-asc
span.badge-icon 🔢
span.badge-text.check-list-sort {{ sort }}
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
.minicard-description
@ -201,7 +187,7 @@ template(name="minicard")
| {{ getDescription }}
if shouldShowListOnMinicard
.minicard-list-name
i.fa.fa-list
| 📋
| {{ listName }}
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
@ -215,13 +201,55 @@ template(name="editCardSortOrderPopup")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
template(name="minicardChecklist")
.minicard-checklist
.checklist-header
.checklist-title= checklist.title
if canModifyCard
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
i.fa.fa-bars
each visibleItems
+checklistItemDetail(item = . checklist = checklist card = card)
template(name="minicardDetailsActionsPopup")
ul.pop-over-list
if canModifyCard
li
a.js-move-card
| ➡️
| {{_ 'moveCardPopup-title'}}
li
a.js-copy-card
| 📋
| {{_ 'copyCardPopup-title'}}
hr
li
a.js-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
li
a.js-move-card-to-top
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
li
a.js-add-labels
| 🏷️
| {{_ 'card-edit-labels'}}
li
a.js-due-date
| 📥
| {{_ 'editCardDueDatePopup-title'}}
li
a.js-set-card-color
| 🎨
| {{_ 'setCardColorPopup-title'}}
li
a.js-link
| 🔗
| {{_ 'link-card'}}
li
a.js-toggle-watch-card
if isWatching
| 👁️
| {{_ 'unwatch'}}
else
| 👁️-slash
| {{_ 'watch'}}

View file

@ -91,13 +91,6 @@ BlazeComponent.extendComponent({
}
},
async toggleChecklistItem() {
const item = this.currentData();
if (item && item._id) {
await item.toggleItem();
}
},
events() {
return [
{
@ -115,11 +108,7 @@ 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'(event) {
event.preventDefault();
event.stopPropagation();
Popup.open('cardDetailsActions').call(this, event);
},
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// Only prevent default for file drags to avoid interfering with sortable
@ -181,43 +170,6 @@ BlazeComponent.extendComponent({
},
}).register('minicard');
BlazeComponent.extendComponent({
template() {
return 'minicardChecklist';
},
events() {
return [
{
'click .js-open-checklist-menu'(event) {
const data = this.currentData();
const checklist = data.checklist || data;
const card = data.card || this.data();
const context = { currentData: () => ({ checklist, card }) };
Popup.open('checklistActions').call(context, event);
},
},
];
},
visibleItems() {
const checklist = this.currentData().checklist || this.currentData();
const items = checklist.items();
return items.filter(item => {
// Hide finished items if hideCheckedChecklistItems is true
if (item.isFinished && checklist.hideCheckedChecklistItems) {
return false;
}
// Hide all items if hideAllChecklistItems is true
if (checklist.hideAllChecklistItems) {
return false;
}
return true;
});
},
}).register('minicardChecklist');
Template.minicard.helpers({
hiddenMinicardLabelText() {
const currentUser = ReactiveCache.getCurrentUser();
@ -257,29 +209,9 @@ Template.minicard.helpers({
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
const currentBoard = this.board();
const currentBoard = this.currentBoard;
if (!currentBoard) return false;
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
},
shouldShowChecklistAtMinicard() {
// Return checklists that should be shown on minicard
const currentBoard = this.board();
if (!currentBoard) return [];
const checklists = this.checklists();
const visibleChecklists = [];
checklists.forEach(checklist => {
// Show checklist if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific checklist has the setting enabled
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
visibleChecklists.push(checklist);
}
});
return visibleChecklists;
}
});
@ -310,3 +242,35 @@ BlazeComponent.extendComponent({
}
}).register('editCardSortOrderPopup');
Template.minicardDetailsActionsPopup.events({
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-move-card': Popup.open('moveCard'),
'click .js-copy-card': Popup.open('copyCard'),
'click .js-set-card-color': Popup.open('setCardColor'),
'click .js-add-labels': Popup.open('cardLabels'),
'click .js-link': Popup.open('linkCard'),
'click .js-move-card-to-top'(event) {
event.preventDefault();
const minOrder = this.getMinSort();
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
Popup.back();
},
'click .js-move-card-to-bottom'(event) {
event.preventDefault();
const maxOrder = this.getMaxSort();
this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
Popup.back();
},
'click .js-archive': Popup.afterConfirm('cardArchive', function () {
Popup.close();
this.archive();
Utils.goBoardId(this.boardId);
}),
'click .js-toggle-watch-card'() {
const currentCard = this;
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
if (!err && ret) Popup.back();
});
},
});

View file

@ -11,9 +11,9 @@ template(name="resultCard")
= getBoard.title
else
.broken-cards-null
| {{_ 'no-name'}}
| NULL
if getBoard.archived
i.fa.fa-archive
| 📦
li.result-card-context.result-card-context-separator
= ' '
| {{_ 'context-separator'}}
@ -25,9 +25,9 @@ template(name="resultCard")
= getSwimlane.title
else
.broken-cards-null
| {{_ 'no-name'}}
| NULL
if getSwimlane.archived
i.fa.fa-archive
| 📦
li.result-card-context.result-card-context-separator
= ' '
| {{_ 'context-separator'}}
@ -39,6 +39,6 @@ template(name="resultCard")
= getList.title
else
.broken-cards-null
| {{_ 'no-name'}}
| NULL
if getList.archived
i.fa.fa-archive
| 📦

View file

@ -87,15 +87,6 @@ textarea.js-edit-subtask-item {
top: 0;
bottom: -600px;
right: 0;
z-index: 15;
}
/* Fix for mobile Safari: ensure this doesn't block card interaction */
@media screen and (max-width: 800px) {
#card-details-overlay {
z-index: 15;
pointer-events: none;
}
}
.subtasks {
background: #f7f7f7;
@ -136,25 +127,6 @@ textarea.js-edit-subtask-item {
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
}
/* Unicode checkbox icons styling */
.subtasks-item .check-box-unicode {
font-size: 1.3em;
margin-right: 8px;
cursor: pointer;
display: inline-block;
vertical-align: middle;
line-height: 1;
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .subtasks-item .check-box.is-checked {
border-bottom: 2px solid #7a7a7a;
border-right: 2px solid #7a7a7a;
}
body.grey-icons-enabled .subtasks-item .check-box-unicode {
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
opacity: 0.85;
}
.subtasks-item .item-title {
flex: 1;
padding-left: 10px;

View file

@ -1,6 +1,6 @@
template(name="subtasks")
h3.card-details-item-title
i.fa.fa-globe
| 🌐
| {{_ 'subtasks'}}
if currentUser.isBoardAdmin
if toggleDeleteDialog.get
@ -16,7 +16,7 @@ template(name="subtasks")
+addSubtaskItemForm
else
a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
i.fa.fa-plus
|
template(name="subtaskDetail")
.js-subtasks.subtask
@ -68,20 +68,18 @@ template(name="subtasksItems")
+addSubtaskItemForm
else
a.add-subtask-item.js-open-inlined-form
i.fa.fa-plus
|
| {{_ 'add-subtask-item'}}...
template(name='subtaskItemDetail')
.js-subtasks-item.subtasks-item
if canModifyCard
span.check-box-unicode
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
else
span.check-box-unicode
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
= item.title
@ -94,10 +92,10 @@ template(name="subtaskActionsPopup")
ul.pop-over-list
li
a.js-view-subtask(title="{{ subtask.title }}")
i.fa.fa-eye
| 👁️
| {{_ "view-it"}}
if currentUser.isBoardAdmin
a.js-delete-subtask.delete-subtask
i.fa.fa-trash
| 🗑️
| {{_ "delete"}} ...

View file

@ -1,5 +1,4 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
BlazeComponent.extendComponent({
addSubtask(event) {
@ -62,10 +61,10 @@ BlazeComponent.extendComponent({
textarea.focus();
},
async deleteSubtask() {
deleteSubtask() {
const subtask = this.currentData().subtask;
if (subtask && subtask._id) {
await subtask.archive();
subtask.archive();
}
},
@ -73,12 +72,12 @@ BlazeComponent.extendComponent({
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
async editSubtask(event) {
editSubtask(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-subtask-item');
const title = textarea.value.trim();
const subtask = this.currentData().subtask;
await subtask.setTitle(title);
subtask.setTitle(title);
},
pressKey(event) {
@ -105,19 +104,7 @@ BlazeComponent.extendComponent({
}).register('subtasks');
BlazeComponent.extendComponent({
async toggleItem() {
const item = this.currentData().item;
if (item && item._id) {
await item.toggleItem();
}
},
events() {
return [
{
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
},
];
},
// ...
}).register('subtaskItemDetail');
BlazeComponent.extendComponent({
@ -138,11 +125,11 @@ BlazeComponent.extendComponent({
});
}
},
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () {
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', function () {
Popup.back(2);
const subtask = this.subtask;
if (subtask && subtask._id) {
await subtask.archive();
subtask.archive();
}
}),
}

View file

@ -81,11 +81,11 @@
font-size: 11px;
padding: 6px;
}
.original-position-details {
padding: 4px 6px;
}
.original-position-moved,
.original-position-unchanged {
padding: 3px 5px;
@ -99,24 +99,24 @@
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-moved {
background-color: #744210;
border-color: #b7791f;
color: #fbd38d;
}
.original-position-unchanged {
background-color: #22543d;
border-color: #38a169;
color: #9ae6b4;
}
.original-title {
color: #a0aec0;
border-color: #4a5568;
}
.original-title strong {
color: #e2e8f0;
}

View file

@ -2,20 +2,22 @@
<div class="original-position-info">
{{#if isLoading}}
<div class="original-position-loading">
Loading original position...
<i class="fa fa-spinner fa-spin"></i> Loading original position...
</div>
{{else if showOriginalPosition}}
<div class="original-position-details">
{{#if hasMovedFromOriginal}}
<div class="original-position-moved">
<span class="original-position-text"> {{getOriginalPositionDescription}}</span>
<i class="fa fa-info-circle"></i>
<span class="original-position-text">{{getOriginalPositionDescription}}</span>
</div>
{{else}}
<div class="original-position-unchanged">
<span class="original-position-text">✅ In original position</span>
<i class="fa fa-check-circle"></i>
<span class="original-position-text">In original position</span>
</div>
{{/if}}
{{#if getOriginalTitle}}
<div class="original-title">
<strong>Original title:</strong> {{getOriginalTitle}}

View file

@ -13,7 +13,7 @@ class OriginalPositionComponent extends BlazeComponent {
this.originalPosition = new ReactiveVar(null);
this.isLoading = new ReactiveVar(false);
this.hasMoved = new ReactiveVar(false);
this.autorun(() => {
const data = this.data();
if (data && data.entityId && data.entityType) {
@ -24,9 +24,9 @@ class OriginalPositionComponent extends BlazeComponent {
loadOriginalPosition(entityId, entityType) {
this.isLoading.set(true);
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
Meteor.call(methodName, entityId, (error, result) => {
this.isLoading.set(false);
if (error) {
@ -34,7 +34,7 @@ class OriginalPositionComponent extends BlazeComponent {
this.originalPosition.set(null);
} else {
this.originalPosition.set(result);
// Check if the entity has moved
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
@ -61,11 +61,11 @@ class OriginalPositionComponent extends BlazeComponent {
getOriginalPositionDescription() {
const position = this.getOriginalPosition();
if (!position) return 'No original position data';
if (position.originalPosition) {
const entityType = this.data().entityType;
let description = `Original position: ${position.originalPosition.sort || 0}`;
if (entityType === 'list' && position.originalSwimlaneId) {
description += ` in swimlane ${position.originalSwimlaneId}`;
} else if (entityType === 'card') {
@ -76,10 +76,10 @@ class OriginalPositionComponent extends BlazeComponent {
description += ` in list ${position.originalListId}`;
}
}
return description;
}
return 'No original position data';
}

View file

@ -4,7 +4,7 @@ template(name="datepicker")
.fields
.left
label(for="date") {{_ 'date'}}
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
input.js-date-field#date(type="text" name="date" value=showDate autofocus placeholder=dateFormat)
.right
label(for="time") {{_ 'time'}}
input.js-time-field#time(type="time" name="time" value=showTime)

View file

@ -130,8 +130,8 @@ textarea.editor {
}
input[type="submit"],
button {
background: #000;
background: linear-gradient(#000, #000);
background: #cfcfcf;
background: linear-gradient(#cfcfcf, #c2c2c2);
border: none;
cursor: pointer;
display: inline-block;
@ -139,7 +139,6 @@ button {
line-height: 1.3;
padding: 1vh 2.5vw;
text-align: center;
color: #fff;
}
input[type="submit"] .wide,
button .wide {
@ -150,16 +149,14 @@ input[type="submit"]:hover,
button:hover,
input[type="submit"]:focus,
button:focus {
background: #222;
background: linear-gradient(#222, #222);
color: #fff;
background: #c2c2c2;
background: linear-gradient(#c2c2c2, #b5b5b5);
}
input[type="submit"]:active,
button:active {
background: #111;
background: linear-gradient(#111, #111);
box-shadow: inset 0 3px 6px rgba(0,0,0,0.3);
color: #fff;
background: #b5b5b5;
background: linear-gradient(#b5b5b5, #a8a8a8);
box-shadow: inset 0 3px 6px rgba(0,0,0,0.1);
}
input[type="submit"]:active:hover,
button:active:hover,
@ -186,12 +183,6 @@ input[type="submit"].primary:active,
button.primary:active {
background: #01628c;
}
input[type="submit"].negate,
button.negate {
background: #eb5a46;
box-shadow: 0 1px 0 #4d4d4d;
color: #fff;
}
input[type="submit"].negate:hover,
button.negate:hover,
input[type="submit"].negate:focus,
@ -226,10 +217,10 @@ input[type="submit"]:disabled:active,
input[type="button"].disabled:active,
button.disabled:active,
.button.disabled:active {
background: #555;
background: #cfcfcf;
cursor: default;
box-shadow: none;
color: #999;
color: #a8a8a8;
}
fieldset {
border: 1px solid #bfbfbf;
@ -324,18 +315,11 @@ textarea::-moz-placeholder {
margin-right: 6px;
border-top: 2px solid transparent;
border-left: 2px solid transparent;
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
transform: rotate(40deg);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform-origin: 100% 100%;
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .materialCheckBox.is-checked {
border-bottom: 2px solid #7a7a7a;
border-right: 2px solid #7a7a7a;
}
.button-link {
background: #fff;
background: linear-gradient(#fff, #f5f5f5);
@ -409,12 +393,12 @@ body.grey-icons-enabled .materialCheckBox.is-checked {
.button-link.setting.disabled.primary,
.button-link.setting.disabled.primary:hover,
.button-link.setting.disabled.primary:active {
background: #555;
border-color: #444;
border-bottom-color: #333;
background: #cfcfcf;
border-color: #c2c2c2;
border-bottom-color: #b5b5b5;
cursor: default;
box-shadow: none;
color: #999;
color: #a8a8a8;
}
.button-link.setting .label {
color: #222;

View file

@ -1,203 +0,0 @@
/* Gantt chart cell background colors for Received, Start, Due, End (matching cardDetails) */
.ganttview-received {
background-color: #dbdbdb !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
.ganttview-start {
background-color: #90ee90 !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
.ganttview-due {
background-color: #ffd700 !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
.ganttview-end {
background-color: #ffb3b3 !important;
color: #000 !important;
font-size: 18px !important;
font-weight: bold !important;
}
/* Gantt View Styles */
.gantt-view {
width: 100%;
height: auto;
overflow: visible;
background-color: #fff;
}
.gantt-view.swimlane {
background-color: #fff;
padding: 10px;
}
.gantt-container {
overflow-x: auto;
overflow-y: visible;
background-color: #fff;
display: block;
width: 100%;
}
.gantt-container table,
.gantt-table {
border-collapse: collapse;
width: 100%;
min-width: 800px;
border: 2px solid #666;
font-family: sans-serif;
font-size: 13px;
background-color: #fff;
}
.gantt-container thead {
background-color: #e8e8e8;
border-bottom: 2px solid #666;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
}
.gantt-container thead th,
.gantt-container thead tr > td:first-child {
border-right: 2px solid #666;
padding: 4px; /* half of 8px */
width: 100px; /* half of 200px */
text-align: left;
font-weight: bold;
background-color: #e8e8e8;
min-width: 100px; /* half of 200px */
}
.gantt-container thead td {
border-right: 1px solid #999;
padding: 2px 1px; /* half */
text-align: center;
background-color: #f5f5f5;
font-size: 11px;
min-width: 15px; /* half of 30px */
font-weight: bold;
height: auto;
line-height: 1.2;
white-space: normal;
word-break: break-word;
}
.gantt-container tbody tr {
border-bottom: 1px solid #999;
height: 32px;
}
.gantt-container tbody tr:hover {
background-color: #f9f9f9;
}
.gantt-container tbody tr:hover td {
background-color: #f9f9f9 !important;
}
.gantt-container tbody td {
border-right: 1px solid #ccc;
padding: 1px; /* half */
text-align: center;
min-width: 15px; /* half of 30px */
height: 32px;
vertical-align: middle;
line-height: 28px;
background-color: #ffffff;
font-size: 18px;
font-weight: bold;
}
.gantt-container tbody td:nth-child(even) {
background-color: #fafafa;
}
.gantt-container tbody td:first-child {
border-right: 2px solid #666;
padding: 4px; /* half of 8px */
font-weight: 500;
cursor: pointer;
background-color: #fafafa !important;
text-align: left;
width: 100px; /* half of 200px */
min-width: 100px; /* half of 200px */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: auto;
line-height: normal;
}
.gantt-container tbody td:first-child:hover {
background-color: #f0f0f0 !important;
text-decoration: underline;
}
.js-gantt-task-cell {
cursor: pointer;
}
.js-gantt-date-icon {
cursor: pointer;
}
.gantt-container .ganttview-weekend {
background-color: #efefef;
}
.gantt-container .ganttview-today {
background-color: #fcf8e3;
border-right: 2px solid #ffb347;
}
/* Task bar styling - VERY VISIBLE */
.gantt-container tbody td.ganttview-block {
background-color: #4CAF50 !important;
color: #fff !important;
font-size: 18px !important;
font-weight: bold !important;
padding: 2px !important;
border-radius: 2px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.gantt-container table {
font-size: 11px;
}
.gantt-container thead td {
min-width: 20px;
padding: 2px;
}
.gantt-container tbody td {
min-width: 20px;
padding: 1px;
height: 20px;
}
.gantt-container tbody td:first-child {
width: 100px;
font-size: 12px;
}
}
/* Print styles */
@media print {
.gantt-container {
overflow: visible;
}
.gantt-container table {
page-break-inside: avoid;
}
}

View file

@ -1,27 +0,0 @@
//- Gantt Chart View Template
template(name="ganttView")
link(rel="stylesheet" href="/client/components/gantt/gantt.css")
link(rel="stylesheet" href="/client/components/gantt/ganttCard.css")
.gantt-view
h2 {{_ 'board-view-gantt'}}
if hasSelectedCard
+ganttCard(selectedCard)
each weeks
table.gantt-table
thead
tr
th {{_ 'task'}} {{_ 'predicate-week'}} {{week}}
each weekDays this
th
| {{formattedDate .}} {{weekdayLabel .}}
tbody
each cardsInWeek this
tr(data-card-id="{{cardId .}}")
td.js-gantt-task-cell
a.js-gantt-card-title(href="#")
+viewer
| {{cardTitle .}}
each weekDays ..
td(class="{{cellClasses .. .}}" data-card-id="{{cardId ..}}" data-date-type="{{cellContentClass .. .}}")
| {{cellContent .. .}}

View file

@ -1,219 +0,0 @@
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
// Add click handler to ganttView for card titles
Template.ganttView.events({
'click .js-gantt-card-title'(event, template) {
event.preventDefault();
// Get card ID from the closest row's data attribute
const $row = template.$(event.currentTarget).closest('tr');
const cardId = $row.data('card-id');
if (cardId) {
template.selectedCardId.set(cardId);
}
},
});
import { Template } from 'meteor/templating';
// Blaze template helpers for ganttView
function getISOWeekInfo(d) {
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
const dayNum = date.getUTCDay() || 7;
date.setUTCDate(date.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
const week = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
return { year: date.getUTCFullYear(), week };
}
function startOfISOWeek(d) {
const date = new Date(d);
const day = date.getDay() || 7;
if (day !== 1) date.setDate(date.getDate() - (day - 1));
date.setHours(0,0,0,0);
return date;
}
Template.ganttView.helpers({
weeks() {
const board = Utils.getCurrentBoard();
if (!board) return [];
const cards = Cards.find({ boardId: board._id }, { sort: { startAt: 1, dueAt: 1 } }).fetch();
const weeksMap = new Map();
const relevantCards = cards.filter(c => c.receivedAt || c.startAt || c.dueAt || c.endAt);
relevantCards.forEach(card => {
['receivedAt','startAt','dueAt','endAt'].forEach(field => {
if (card[field]) {
const dt = new Date(card[field]);
const info = getISOWeekInfo(dt);
const key = `${info.year}-W${info.week}`;
if (!weeksMap.has(key)) {
weeksMap.set(key, { year: info.year, week: info.week, start: startOfISOWeek(dt) });
}
}
});
});
return Array.from(weeksMap.values()).sort((a,b) => a.start - b.start);
},
weekDays(week) {
const weekStart = new Date(week.start);
return Array.from({length:7}, (_,i) => {
const d = new Date(weekStart);
d.setDate(d.getDate() + i);
d.setHours(0,0,0,0);
return d;
});
},
weekdayLabel(day) {
const weekdayKeys = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
return TAPi18n.__(weekdayKeys[day.getDay() === 0 ? 6 : day.getDay() - 1]);
},
formattedDate(day) {
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(day, dateFormat, false);
},
cardsInWeek(week) {
const board = Utils.getCurrentBoard();
if (!board) return [];
const cards = Cards.find({ boardId: board._id }).fetch();
return cards.filter(card => {
return ['receivedAt','startAt','dueAt','endAt'].some(field => {
if (card[field]) {
const dt = new Date(card[field]);
const info = getISOWeekInfo(dt);
return info.week === week.week && info.year === week.year;
}
return false;
});
});
},
cardTitle(card) {
return card.title;
},
cardId(card) {
return card._id;
},
cardUrl(card) {
if (!card) return '#';
const board = ReactiveCache.getBoard(card.boardId);
if (!board) return '#';
return FlowRouter.path('card', {
boardId: card.boardId,
slug: board.slug,
cardId: card._id,
});
},
cellContentClass(card, day) {
const cardDates = {
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
startAt: card.startAt ? new Date(card.startAt) : null,
dueAt: card.dueAt ? new Date(card.dueAt) : null,
endAt: card.endAt ? new Date(card.endAt) : null,
};
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return 'ganttview-received';
if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return 'ganttview-start';
if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return 'ganttview-due';
if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return 'ganttview-end';
return '';
},
cellContent(card, day) {
const cardDates = {
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
startAt: card.startAt ? new Date(card.startAt) : null,
dueAt: card.dueAt ? new Date(card.dueAt) : null,
endAt: card.endAt ? new Date(card.endAt) : null,
};
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return '📥';
if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return '🚀';
if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return '⏰';
if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return '🏁';
return '';
},
isToday(day) {
const today = new Date();
return day.toDateString() === today.toDateString();
},
isWeekend(day) {
const idx = day.getDay();
return idx === 0 || idx === 6;
},
hasSelectedCard() {
return Template.instance().selectedCardId.get() !== null;
},
selectedCard() {
const cardId = Template.instance().selectedCardId.get();
return cardId ? ReactiveCache.getCard(cardId) : null;
},
cellClasses(card, day) {
// Get the base class from cellContentClass logic
const cardDates = {
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
startAt: card.startAt ? new Date(card.startAt) : null,
dueAt: card.dueAt ? new Date(card.dueAt) : null,
endAt: card.endAt ? new Date(card.endAt) : null,
};
let classes = '';
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) classes = 'ganttview-received';
else if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) classes = 'ganttview-start';
else if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) classes = 'ganttview-due';
else if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) classes = 'ganttview-end';
// Add conditional classes
const today = new Date();
if (day.toDateString() === today.toDateString()) classes += ' ganttview-today';
const idx = day.getDay();
if (idx === 0 || idx === 6) classes += ' ganttview-weekend';
if (classes.trim()) classes += ' js-gantt-date-icon';
return classes.trim();
}
});
Template.ganttView.onCreated(function() {
this.selectedCardId = new ReactiveVar(null);
// Provide properties expected by cardDetails component
this.showOverlay = new ReactiveVar(false);
this.mouseHasEnterCardDetails = false;
});
// Blaze onRendered logic for ganttView
Template.ganttView.onRendered(function() {
const self = this;
this.autorun(() => {
// If you have legacy imperative rendering, keep it here
if (typeof renderGanttChart === 'function') {
renderGanttChart();
}
});
// Add click handler for date cells (Received, Start, Due, End)
this.$('.gantt-table').on('click', '.js-gantt-date-icon', function(e) {
e.preventDefault();
e.stopPropagation();
const $cell = self.$(this);
const cardId = $cell.data('card-id');
let dateType = $cell.data('date-type');
// Remove 'ganttview-' prefix to match popup map
if (typeof dateType === 'string' && dateType.startsWith('ganttview-')) {
dateType = dateType.replace('ganttview-', '');
}
const popupMap = {
received: 'editCardReceivedDate',
start: 'editCardStartDate',
due: 'editCardDueDate',
end: 'editCardEndDate',
};
const popupName = popupMap[dateType];
if (!popupName || typeof Popup === 'undefined' || typeof Popup.open !== 'function') return;
const card = ReactiveCache.getCard(cardId);
if (!card) return;
const openFn = Popup.open(popupName);
openFn.call({ currentData: () => card }, e, { dataContextIfCurrentDataIsUndefined: card });
});
});
import markdownit from 'markdown-it';
import { TAPi18n } from '/imports/i18n';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import { ReactiveCache } from '/imports/reactiveCache';
const md = markdownit({ breaks: true, linkify: true });

View file

@ -1,47 +0,0 @@
.gantt-card-wrapper {
background: white;
border: 1px solid #ddd;
border-radius: 5px;
margin: 1rem 0;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.gantt-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.gantt-card-header h3 {
margin: 0;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
background-color: #f0f0f0;
color: #333;
}
.gantt-card-content {
max-height: 400px;
overflow-y: auto;
}

View file

@ -1,2 +0,0 @@
template(name="ganttCard")
+cardDetails(selectedCard)

View file

@ -1,45 +0,0 @@
BlazeComponent.extendComponent({
onCreated() {
// Provide the expected parent component properties for cardDetails
this.showOverlay = new ReactiveVar(false);
this.mouseHasEnterCardDetails = false;
},
selectedCard() {
// The selected card is now passed as a parameter to the component
return this.currentData();
},
events() {
return [
{
'click .js-close-card-details'(event) {
event.preventDefault();
// Find the ganttView template instance and clear selectedCardId
let view = Blaze.currentView;
while (view) {
if (view.templateInstance && view.templateInstance().selectedCardId) {
view.templateInstance().selectedCardId.set(null);
break;
}
view = view.parentView;
}
},
},
];
},
}).register('ganttCard');
// Add click handler to ganttView for card titles
Template.ganttView.events({
'click .js-gantt-card-title'(event, template) {
event.preventDefault();
// Get card ID from the closest row's data attribute
const $row = template.$(event.currentTarget).closest('tr');
const cardId = $row.data('card-id');
if (cardId) {
template.selectedCardId.set(cardId);
}
},
});

View file

@ -1,7 +1,7 @@
template(name="importHeaderBar")
h1
a.back-btn(href="{{pathFor 'home'}}")
i.fa-arrow-left
i.fa.fa-chevron-left
| {{_ title}}
template(name="import")

View file

@ -1,9 +1,7 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { trelloGetMembersToMap } from './trelloMembersMapper';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { wekanGetMembersToMap } from './wekanMembersMapper';
import { csvGetMembersToMap } from './csvMembersMapper';
import getSlug from 'limax';
const Papa = require('papaparse');
@ -347,7 +345,7 @@ BlazeComponent.extendComponent({
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
this.searchResults.set(results);
this.searching.set(false);
if (results.length === 0) {
this.noResults.set(true);
}
@ -358,11 +356,11 @@ BlazeComponent.extendComponent({
{
'keyup .js-search-member-input'(event) {
const query = event.target.value.trim();
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, 300);

View file

@ -161,66 +161,74 @@ body.list-resizing-active * {
/* Use original display for consistent button positioning */
display: block !important;
position: relative !important;
/* Allow overflow for text wrapping and forms */
overflow: visible !important;
}
/* Clearfix for floated buttons */
.list-header::after {
content: "";
display: table;
clear: both;
/* Prevent vertical expansion but allow normal height */
overflow: hidden !important;
}
/* Ensure title text doesn't cause height changes for all lists */
.list-header .list-header-name {
/* Allow text wrapping to flow below buttons */
white-space: normal !important;
/* Prevent text wrapping to maintain consistent height */
white-space: nowrap !important;
/* Truncate text with ellipsis if too long */
text-overflow: ellipsis !important;
/* Ensure proper line height */
line-height: 1.2 !important;
/* Ensure it doesn't overflow horizontally */
overflow-wrap: break-word !important;
word-wrap: break-word !important;
/* Full width since buttons are now absolutely positioned above */
width: 100% !important;
/* Ensure it doesn't overflow */
overflow: hidden !important;
/* Add margin to prevent overlap with buttons */
margin-right: 120px !important;
}
/* Position elements at top aligned with collapse button */
.list-header .js-open-list-menu {
/* Position drag handle at top-right corner for ALL lists */
.list-header .list-header-handle {
/* Position at top-right corner, aligned with title text top */
position: absolute !important;
top: 5px !important;
right: 10px !important;
z-index: 15 !important;
display: inline-block !important;
padding: 4px !important;
}
.list-header .list-header-plus-top {
position: absolute !important;
top: 5px !important;
right: 30px !important;
z-index: 15 !important;
display: inline-block !important;
padding: 4px !important;
}
.list-header .list-header-handle-desktop {
position: absolute !important;
top: 5px !important;
right: 80px !important;
top: 2.5vh !important;
right: 1.5vw !important;
/* Ensure it's above other elements */
z-index: 15 !important;
/* Remove margin since it's absolutely positioned */
margin-right: 0 !important;
/* Ensure proper display */
display: inline-block !important;
/* Ensure it's clickable and shows proper cursor */
cursor: move !important;
pointer-events: auto !important;
/* Add some padding for better clickability */
padding: 4px !important;
}
/* Anchor header action buttons within header during resize */
.list .list-header { position: relative; z-index: 5; }
.list .list-header .js-open-list-menu,
.list .list-header .list-header-plus-top,
.list .list-header .list-header-handle-desktop {
position: absolute !important;
/* Ensure buttons maintain original positioning */
.js-swimlane .list[style*="--list-width"] .list-header .list-header-plus-top,
.js-swimlane .list[style*="--list-width"] .list-header .js-collapse,
.js-swimlane .list[style*="--list-width"] .list-header .js-open-list-menu,
.dragscroll .list[style*="--list-width"] .list-header .list-header-plus-top,
.dragscroll .list[style*="--list-width"] .list-header .js-collapse,
.dragscroll .list[style*="--list-width"] .list-header .js-open-list-menu,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-plus-top,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-collapse,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-open-list-menu {
/* Use original positioning to maintain layout */
position: relative !important;
/* Maintain original spacing */
margin-right: 15px !important;
/* Ensure proper display */
display: inline-block !important;
}
/* Ensure watch icon and card count maintain original positioning */
.js-swimlane .list[style*="--list-width"] .list-header .list-header-watch-icon,
.dragscroll .list[style*="--list-width"] .list-header .list-header-watch-icon,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-watch-icon,
.js-swimlane .list[style*="--list-width"] .list-header .cardCount,
.dragscroll .list[style*="--list-width"] .list-header .cardCount,
[id^="swimlane-"] .list[style*="--list-width"] .list-header .cardCount {
/* Use original positioning to maintain layout */
position: relative !important;
/* Maintain original spacing */
margin-right: 15px !important;
/* Ensure proper display */
display: inline-block !important;
}
[id^="swimlane-"] .list:first-child {
min-width: 2.5vw;
@ -251,61 +259,36 @@ body.list-resizing-active * {
}
.list.list-collapsed {
flex: none;
min-width: 30px;
max-width: 30px;
width: 30px;
min-width: 60px;
max-width: 80px;
width: 60px;
min-height: 60vh;
height: 60vh;
overflow: visible;
position: relative;
}
.list.list-collapsed .list-header {
padding: 5px 0;
min-height: 100% !important;
height: 100% !important;
padding: 1vh 1.5vw 0.5vh;
min-height: 2.5vh !important;
height: auto !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
overflow: visible !important;
width: 30px;
max-width: 30px;
margin: 0;
width: 100%;
max-width: 60px;
margin: 0 auto;
}
.list.list-collapsed .list-header .js-collapse {
position: relative !important;
left: -10px !important;
margin: 5px auto;
margin: 0 auto 20px auto;
z-index: 10;
padding: 5px;
font-size: 16px;
padding: 8px 12px;
font-size: 12px;
white-space: nowrap;
display: block;
width: auto;
left: auto !important;
top: auto !important;
}
.list.list-collapsed .list-header .list-header-handle {
position: static !important;
margin: 5px auto;
z-index: 10;
padding: 5px;
display: block;
width: auto;
top: auto !important;
right: auto !important;
}
.list.list-collapsed .list-header .list-header-handle-desktop {
position: static !important;
margin: 5px auto;
z-index: 10;
padding: 5px;
display: block;
width: auto;
top: auto !important;
right: auto !important;
width: fit-content;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
@ -313,43 +296,31 @@ body.list-resizing-active * {
margin: 20px 0 0 0 !important;
position: relative !important;
overflow: visible !important;
transform: rotate(90deg);
transform-origin: center center;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
text-align: center;
text-align: left;
overflow: visible;
white-space: nowrap;
display: block !important;
font-size: 12px;
line-height: 1.2;
color: #333;
padding: 4px 8px;
margin: 0;
width: auto;
height: auto;
position: static;
left: auto;
top: auto;
transform: none;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
padding: 8px 4px;
border-radius: 4px;
margin: 0 auto;
width: 25vh;
height: 60vh;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
z-index: 10;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto;
}
.list.list-composer,
.list-composer {
display: none;
}
/* Show list-composer when inside an active inlined form */
form.inlined-form .list-composer {
display: block;
pointer-events: none;
}
.list.list-composer .open-list-composer,
@ -386,29 +357,16 @@ form.inlined-form .list-composer {
display: none;
}
.list-header .list-header-name {
display: block;
display: inline;
font-size: clamp(14px, 3vw, 18px);
line-height: 1.2;
margin: 0;
font-weight: bold;
min-height: 1.2vh;
min-width: 4vw;
overflow-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
vertical-align: top;
width: 100%;
}
/* Sum badge shown before list title */
.list-header .list-sum-badge {
display: inline-block;
margin-right: 8px;
padding: 0;
border-radius: 0;
background: transparent;
color: #8c8c8c;
font-weight: bold;
font-size: 12px;
vertical-align: middle;
}
.list-rotated {
width: 1.3vw;
@ -420,6 +378,9 @@ form.inlined-form .list-composer {
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-header .list-rotated {
}
.list-header .list-header-watch-icon {
padding-left: 10px;
@ -437,8 +398,6 @@ form.inlined-form .list-composer {
.list-header .list-header-plus-top {
color: #a6a6a6;
margin-right: 15px;
vertical-align: middle;
line-height: 1.2;
}
.list-header .list-header-collapse-right {
color: #a6a6a6;
@ -447,194 +406,151 @@ form.inlined-form .list-composer {
color: #a6a6a6;
margin-right: 15px;
}
/* List header collapse button styling - positioned at top left */
.list-header .js-collapse {
position: absolute !important;
top: 5px !important;
left: 10px !important;
color: #a6a6a6;
margin-right: 15px;
display: inline-block;
vertical-align: top;
vertical-align: middle;
padding: 5px 8px;
border: none;
border-radius: 0;
background-color: transparent;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f5f5f5;
cursor: pointer;
font-size: 18px;
line-height: 1.2;
min-width: 30px;
text-align: center;
text-decoration: none;
margin: 0;
z-index: 15;
font-size: 14px;
}
.list-header .js-collapse:hover {
background-color: transparent;
background-color: #e0e0e0;
color: #333;
}
/* Title text container - full width below buttons */
.list-header > div {
padding-top: 25px;
width: 100%;
display: block;
clear: both;
}
.list.list-collapsed .list-header .js-collapse {
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Hide menu button in collapsed state */
.list.list-collapsed .list-header .js-open-list-menu,
.list.list-collapsed .list-header .list-header-menu {
display: none !important;
}
/* Responsive adjustments for collapsed lists */
@media (min-width: 768px) {
.list.list-collapsed {
min-width: 30px;
max-width: 30px;
width: 30px;
min-width: 60px;
max-width: 80px;
width: 60px;
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
width: 30px;
max-width: 30px;
margin: 0;
min-height: 100% !important;
height: 100% !important;
max-width: 60px;
margin: 0 auto;
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
transform: rotate(90deg);
flex: 1;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: auto;
width: 15vh;
font-size: 12px;
height: auto;
height: 30px;
line-height: 1.2;
padding: 4px 8px;
margin: 0;
overflow: visible;
position: static;
left: auto;
top: auto;
transform: none;
text-align: center;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: transparent;
border: none;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 5px auto;
margin: 0 auto 20px auto;
}
}
@media (min-width: 1024px) {
.list.list-collapsed {
min-width: 30px;
max-width: 30px;
width: 30px;
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
width: 30px;
max-width: 30px;
min-height: 100% !important;
height: 100% !important;
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
transform: rotate(90deg);
flex: 1;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: auto;
width: 15vh;
font-size: 12px;
height: auto;
height: 30px;
line-height: 1.2;
padding: 4px 8px;
margin: 0;
overflow: visible;
position: static;
left: auto;
top: auto;
transform: none;
text-align: center;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: transparent;
border: none;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 5px auto;
margin: 0 auto 20px auto;
}
}
@media (min-width: 1200px) {
.list.list-collapsed {
min-width: 30px;
max-width: 30px;
width: 30px;
min-height: 60vh;
height: 60vh;
}
.list.list-collapsed .list-header {
width: 30px;
max-width: 30px;
min-height: 100% !important;
height: 100% !important;
min-height: 2.5vh !important;
height: auto !important;
}
.list.list-collapsed .list-header .list-rotated {
width: auto !important;
height: auto !important;
margin: 20px 0 0 0 !important;
position: relative !important;
transform: rotate(90deg);
flex: 1;
}
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
width: auto;
width: 15vh;
font-size: 12px;
height: auto;
height: 30px;
line-height: 1.2;
padding: 4px 8px;
margin: 0;
overflow: visible;
position: static;
left: auto;
top: auto;
transform: none;
text-align: center;
padding: 8px 4px;
margin: 0 auto;
position: absolute;
left: 50%;
top: 50%;
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
text-align: left;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
background-color: transparent;
border: none;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #ddd;
color: #333;
z-index: 10;
}
.list.list-collapsed .list-header .js-collapse {
margin: 5px auto;
margin: 0 auto 20px auto;
}
}
.list-header .list-header-collapse {
@ -657,8 +573,6 @@ form.inlined-form .list-composer {
}
.js-open-list-menu {
font-size: 18px;
vertical-align: middle;
line-height: 1.2;
}
.list-body {
flex: 1 1 auto;
@ -730,22 +644,17 @@ form.inlined-form .list-composer {
.mini-list.mobile-view {
flex: 0 0 60px;
height: auto;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
display: block !important;
}
.list.mobile-view {
display: block !important;
display: contents;
flex-basis: auto;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
border-left: 0px !important;
margin: 0 !important;
padding: 0 !important;
}
.list.mobile-view:first-child {
margin-left: 0px;
@ -753,11 +662,9 @@ form.inlined-form .list-composer {
.list.mobile-view.ui-sortable-helper {
flex: 0 0 60px;
height: 60px;
width: 100vw;
max-width: 100vw;
width: 100%;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
display: block !important;
}
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
cursor: grabbing;
@ -765,17 +672,14 @@ form.inlined-form .list-composer {
.list.mobile-view.placeholder {
flex: 0 0 60px;
height: 60px;
width: 100vw;
max-width: 100vw;
width: 100%;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
display: block !important;
}
.list.mobile-view .list-body {
padding: 15px 19px;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
}
.list.mobile-view .list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/
@ -784,9 +688,8 @@ form.inlined-form .list-composer {
min-height: 30px;
margin-top: 10px;
align-items: center;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
/* Force grid layout for iPhone */
display: grid !important;
grid-template-columns: 30px 1fr auto auto !important;
@ -839,9 +742,6 @@ form.inlined-form .list-composer {
grid-row: 2;
grid-column: 2;
align-self: start;
text-align: left;
padding-left: 0;
margin-left: 0;
font-size: 16px !important;
line-height: 1.2;
}
@ -870,22 +770,17 @@ form.inlined-form .list-composer {
.mini-list {
flex: 0 0 60px;
height: auto;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
display: block !important;
}
.list {
display: block !important;
display: contents;
flex-basis: auto;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
border-left: 0px !important;
margin: 0 !important;
padding: 0 !important;
}
.list:first-child {
margin-left: 0px;
@ -893,11 +788,9 @@ form.inlined-form .list-composer {
.list.ui-sortable-helper {
flex: 0 0 60px;
height: 60px;
width: 100vw;
max-width: 100vw;
width: 100%;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
display: block !important;
}
.list.ui-sortable-helper .list-header.ui-sortable-handle {
cursor: grabbing;
@ -905,17 +798,14 @@ form.inlined-form .list-composer {
.list.placeholder {
flex: 0 0 60px;
height: 60px;
width: 100vw;
max-width: 100vw;
width: 100%;
border-left: 0px !important;
border-bottom: 1px solid #ccc;
display: block !important;
}
.list-body {
padding: 15px 19px;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
}
.list-header {
/*Updated padding values for mobile devices, this should fix text grouping issue*/
@ -924,9 +814,8 @@ form.inlined-form .list-composer {
min-height: 30px;
margin-top: 10px;
align-items: center;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
width: 100%;
min-width: 100%;
}
.list-header .list-header-left-icon {
padding: 7px;
@ -1056,9 +945,6 @@ form.inlined-form .list-composer {
grid-row: 2 !important;
grid-column: 2 !important;
align-self: start !important;
text-align: left !important;
padding-left: 0 !important;
margin-left: 0 !important;
font-size: 16px !important;
line-height: 1.2 !important;
}
@ -1130,23 +1016,6 @@ form.inlined-form .list-composer {
grid-row: 1/3 !important;
grid-column: 1 !important;
}
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
.list:not(.mobile-view):not(.list-collapsed) .list-header {
overflow: visible !important;
}
.list:not(.mobile-view):not(.list-collapsed) .list-header .list-header-name {
/* Permit wrapping and full visibility */
white-space: normal !important;
overflow: visible !important;
text-overflow: clip !important;
display: block !important;
/* Full width since buttons are absolutely positioned */
width: 100% !important;
/* Break long words to avoid overflow */
word-break: break-word !important;
}
.link-board-wrapper {
display: flex;
align-items: baseline;
@ -1236,48 +1105,3 @@ form.inlined-form .list-composer {
.list-header-indigo {
border-bottom: 6px solid #4b0082;
}
.list.list-collapsed .collapsed-list-drag-area {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
user-select: none;
}
.list.list-collapsed .collapsed-list-drag-area:active {
cursor: grabbing;
}
.list.list-collapsed .list-header-name-collapsed {
writing-mode: vertical-rl;
text-align: center;
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list.list-collapsed .list-header .js-collapse {
position: relative !important;
left: -10px !important;
color: #333;
background: transparent;
border: none;
border-radius: 0;
width: auto;
height: auto;
min-width: 0;
min-height: 0;
display: block !important;
align-items: initial;
justify-content: initial;
font-size: 16px !important;
box-shadow: none;
margin: 5px auto;
z-index: 10;
}

View file

@ -3,9 +3,8 @@ template(name='list')
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
+listHeader
unless collapsed
+listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
+listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
template(name='miniList')
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")

View file

@ -198,7 +198,7 @@ BlazeComponent.extendComponent({
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!list) return 270; // Return default width if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListWidthFromStorage(list.boardId, list._id);
@ -223,7 +223,7 @@ BlazeComponent.extendComponent({
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!list) return 550; // Return default constraint if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListConstraintFromStorage(list.boardId, list._id);
@ -260,11 +260,11 @@ BlazeComponent.extendComponent({
console.warn('No current template data available for list resize initialization');
return;
}
const list = Template.currentData();
const $list = this.$('.js-list');
const $resizeHandle = this.$('.js-list-resize-handle');
// Check if elements exist
if (!$list.length || !$resizeHandle.length) {
console.warn('List or resize handle not found, retrying in 100ms');
@ -275,22 +275,20 @@ BlazeComponent.extendComponent({
}, 100);
return;
}
// Reactively show/hide resize handle based on collapse and auto-width state
this.autorun(() => {
const isAutoWidth = this.autoWidth();
const isCollapsed = Utils.getListCollapseState(list);
if (isCollapsed || isAutoWidth) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
}
});
// Only enable resize for non-collapsed, non-auto-width lists
const isAutoWidth = this.autoWidth();
if (list.collapsed || isAutoWidth) {
$resizeHandle.hide();
return;
}
let isResizing = false;
let startX = 0;
let startWidth = 0;
let minWidth = 270; // Minimum width matching system default
let minWidth = 100; // Minimum width as defined in the existing code
let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
const component = this; // Store reference to component for use in event handlers
@ -298,16 +296,16 @@ BlazeComponent.extendComponent({
isResizing = true;
startX = e.pageX || e.originalEvent.touches[0].pageX;
startWidth = $list.outerWidth();
// Add visual feedback
$list.addClass('list-resizing');
$('body').addClass('list-resizing-active');
// Prevent text selection during resize
$('body').css('user-select', 'none');
e.preventDefault();
e.stopPropagation();
};
@ -316,11 +314,11 @@ BlazeComponent.extendComponent({
if (!isResizing) {
return;
}
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const newWidth = Math.max(minWidth, startWidth + deltaX);
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Apply the new width immediately for real-time feedback
$list[0].style.setProperty('--list-width', `${newWidth}px`);
$list[0].style.setProperty('width', `${newWidth}px`);
@ -330,22 +328,22 @@ BlazeComponent.extendComponent({
$list[0].style.setProperty('flex-basis', 'auto');
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
e.preventDefault();
e.stopPropagation();
};
const stopResize = (e) => {
if (!isResizing) return;
isResizing = false;
// Calculate final width
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, startWidth + deltaX);
const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Ensure the final width is applied
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
$list[0].style.setProperty('width', `${finalWidth}px`);
@ -355,23 +353,23 @@ BlazeComponent.extendComponent({
$list[0].style.setProperty('flex-basis', 'auto');
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
// Remove visual feedback but keep the width
$list.removeClass('list-resizing');
$('body').removeClass('list-resizing-active');
$('body').css('user-select', '');
// Keep the CSS custom property for persistent width
// The CSS custom property will remain on the element to maintain the width
// Save the new width using the existing system
const boardId = list.boardId;
const listId = list._id;
// Use the new storage method that handles both logged-in and non-logged-in users
if (process.env.DEBUG === 'true') {
}
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
// For logged-in users, use server method
@ -389,32 +387,32 @@ BlazeComponent.extendComponent({
// Save list width
const storedWidths = localStorage.getItem('wekan-list-widths');
let widths = storedWidths ? JSON.parse(storedWidths) : {};
if (!widths[boardId]) {
widths[boardId] = {};
}
widths[boardId][listId] = finalWidth;
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
// Save list constraint
const storedConstraints = localStorage.getItem('wekan-list-constraints');
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
if (!constraints[boardId]) {
constraints[boardId] = {};
}
constraints[boardId][listId] = listConstraint;
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
if (process.env.DEBUG === 'true') {
}
} catch (e) {
console.warn('Error saving list width/constraint to localStorage:', e);
}
}
e.preventDefault();
};
@ -422,23 +420,22 @@ BlazeComponent.extendComponent({
$resizeHandle.on('mousedown', startResize);
$(document).on('mousemove', doResize);
$(document).on('mouseup', stopResize);
// Touch events for mobile
$resizeHandle.on('touchstart', startResize, { passive: false });
$(document).on('touchmove', doResize, { passive: false });
$(document).on('touchend', stopResize, { passive: false });
// Prevent dragscroll interference
$resizeHandle.on('mousedown', (e) => {
e.stopPropagation();
});
// Reactively update resize handle visibility when auto-width or collapse changes
// Reactively update resize handle visibility when auto-width changes
component.autorun(() => {
const collapsed = Utils.getListCollapseState(list);
if (component.autoWidth() || collapsed) {
if (component.autoWidth()) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
@ -455,28 +452,9 @@ BlazeComponent.extendComponent({
},
}).register('list');
Template.list.helpers({
collapsed() {
return Utils.getListCollapseState(this);
},
});
Template.miniList.events({
'click .js-select-list'() {
const listId = this._id;
Session.set('currentList', listId);
},
});
// Enable drag-reorder for collapsed lists from .js-collapsed-list-drag area
this.$('.js-collapsed-list-drag').draggable({
axis: 'x',
helper: 'clone',
revert: 'invalid',
start(evt, ui) {
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
}
});

View file

@ -2,8 +2,9 @@ template(name="listBody")
unless collapsed
.list-body(class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
+inlinedForm(autoclose=false position="top")
+addCardForm(listId=_id position="top")
if cards.length
+inlinedForm(autoclose=false position="top")
+addCardForm(listId=_id position="top")
ul.sidebar-list
each customFieldsSum
li
@ -25,15 +26,13 @@ template(name="listBody")
+minicard(this)
if (showSpinner (idOrNull ../../_id))
+spinnerList
if canSeeAddCard
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
else
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
i.fa.fa-plus
| {{_ 'add-card'}}
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
|
template(name="spinnerList")
.sk-spinner.sk-spinner-list(
@ -55,8 +54,7 @@ template(name="addCardForm")
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
a.js-close-inlined-form
i.fa.fa-times-thin
a.js-close-inlined-form | ❌
.add-controls.clearfix
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
@ -87,19 +85,16 @@ template(name="linkCardPopup")
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
option(value="") {{_ 'custom-field-dropdown-none'}}
each swimlanes
option(value="{{_id}}") {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
option(value="") {{_ 'custom-field-dropdown-none'}}
each lists
option(value="{{_id}}") {{isTitleDefault title}}
label {{_ 'cards'}}:
select.js-select-cards
option(value="") {{_ 'custom-field-dropdown-none'}}
each cards
option(value="{{getRealId}}") {{getTitle}}

View file

@ -1,8 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { Spinner } from '/client/lib/spinner';
import getSlug from 'limax';
const subManager = new SubsManager();
const InfiniteScrollIter = 10;
@ -18,50 +16,11 @@ BlazeComponent.extendComponent({
},
customFieldsSum() {
const list = Template.currentData();
if (!list) return [];
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
const ret = ReactiveCache.getCustomFields({
boardIds: { $in: [Session.get('currentBoard')] },
showSumAtTopOfList: true,
});
if (!fields || !fields.length) return [];
const cards = ReactiveCache.getCards({
listId: list._id,
archived: false,
});
const result = fields.map(field => {
let sum = 0;
if (cards && cards.length) {
cards.forEach(card => {
const cfs = (card.customFields || []);
const cf = cfs.find(f => f && f._id === field._id);
if (!cf || cf.value === null || cf.value === undefined) return;
let v = cf.value;
if (typeof v === 'string') {
// try to parse string numbers, accept comma decimal
const parsed = parseFloat(v.replace(',', '.'));
if (isNaN(parsed)) return;
v = parsed;
}
if (typeof v === 'number' && isFinite(v)) {
sum += v;
}
});
}
return {
_id: field._id,
name: field.name,
type: field.type,
settings: field.settings || {},
value: sum,
};
});
return result;
return ret;
},
openForm(options) {
@ -210,12 +169,6 @@ BlazeComponent.extendComponent({
evt.stopImmediatePropagation();
evt.preventDefault();
Utils.goBoardId(Session.get('currentBoard'));
} else {
// Allow normal href navigation, but if it's the same card URL,
// we'll handle it by directly setting the session
evt.preventDefault();
const card = this.currentData();
Session.set('currentCard', card._id);
}
},
@ -301,22 +254,6 @@ BlazeComponent.extendComponent({
},
}).register('listBody');
// Helpers for listBody template context
Template.listBody.helpers({
formattedCurrencyCustomFieldValue(val) {
// `this` is the custom field sum object from customFieldsSum each-iteration
const field = this || {};
const code = (field.settings && field.settings.currencyCode) || 'USD';
try {
const n = typeof val === 'number' ? val : parseFloat(val);
if (!isFinite(n)) return val;
return new Intl.NumberFormat(undefined, { style: 'currency', currency: code }).format(n);
} catch (e) {
return `${code} ${val}`;
}
},
});
function toggleValueInReactiveArray(reactiveValue, value) {
const array = reactiveValue.get();
const valueIndex = array.indexOf(value);
@ -539,10 +476,10 @@ BlazeComponent.extendComponent({
if (!board) {
return [];
}
// Ensure default swimlane exists
board.getDefaultSwimline();
const swimlanes = ReactiveCache.getSwimlanes(
{
boardId: this.selectedBoardId.get()
@ -550,6 +487,8 @@ BlazeComponent.extendComponent({
{
sort: { sort: 1 },
});
if (swimlanes.length)
this.selectedSwimlaneId.set(swimlanes[0]._id);
return swimlanes;
},
@ -564,6 +503,7 @@ BlazeComponent.extendComponent({
{
sort: { sort: 1 },
});
if (lists.length) this.selectedListId.set(lists[0]._id);
return lists;
},
@ -572,17 +512,19 @@ BlazeComponent.extendComponent({
return [];
}
const ownCardsIds = this.board.cards().map(card => card.getRealId());
const selector = {
const ret = ReactiveCache.getCards(
{
boardId: this.selectedBoardId.get(),
swimlaneId: this.selectedSwimlaneId.get(),
listId: this.selectedListId.get(),
archived: false,
linkedId: { $nin: ownCardsIds },
_id: { $nin: ownCardsIds },
type: { $nin: ['template-card'] },
};
if (this.selectedBoardId.get()) selector.boardId = this.selectedBoardId.get();
if (this.selectedSwimlaneId.get()) selector.swimlaneId = this.selectedSwimlaneId.get();
if (this.selectedListId.get()) selector.listId = this.selectedListId.get();
const ret = ReactiveCache.getCards(selector, { sort: { sort: 1 } });
},
{
sort: { sort: 1 },
});
return ret;
},
@ -603,12 +545,8 @@ BlazeComponent.extendComponent({
return [
{
'change .js-select-boards'(evt) {
const val = $(evt.currentTarget).val();
subManager.subscribe('board', val, false);
// Clear selections to allow linking only board or re-choose swimlane/list
this.selectedSwimlaneId.set('');
this.selectedListId.set('');
this.selectedBoardId.set(val);
subManager.subscribe('board', $(evt.currentTarget).val(), false);
this.selectedBoardId.set($(evt.currentTarget).val());
},
'change .js-select-swimlanes'(evt) {
this.selectedSwimlaneId.set($(evt.currentTarget).val());
@ -817,7 +755,7 @@ BlazeComponent.extendComponent({
evt.preventDefault();
this.term.set(evt.target.searchTerm.value);
},
async 'click .js-minicard'(evt) {
'click .js-minicard'(evt) {
// 0. Common
const title = $('.js-element-title')
.val()
@ -835,7 +773,7 @@ BlazeComponent.extendComponent({
if (this.isTemplateSearch) {
element.type = 'cardType-card';
element.linkedId = '';
_id = await element.copy(this.boardId, this.swimlaneId, this.listId);
_id = element.copy(this.boardId, this.swimlaneId, this.listId);
// 1.B Linked card
} else {
_id = element.link(this.boardId, this.swimlaneId, this.listId);
@ -847,13 +785,13 @@ BlazeComponent.extendComponent({
.lists()
.length;
element.type = 'list';
_id = await element.copy(this.boardId, this.swimlaneId);
_id = element.copy(this.boardId, this.swimlaneId);
} else if (this.isSwimlaneTemplateSearch) {
element.sort = ReactiveCache.getBoard(this.boardId)
.swimlanes()
.length;
element.type = 'swimlane';
_id = await element.copy(this.boardId);
_id = element.copy(this.boardId);
} else if (this.isBoardTemplateSearch) {
Meteor.call(
'copyBoard',

View file

@ -8,7 +8,7 @@ template(name="listHeader")
if isMiniScreen
if currentList
a.list-header-left-icon.js-unselect-list
i.fa.fa-caret-left
| ◀️
else
if collapsed
if showCardsCountForList cards.length
@ -26,19 +26,15 @@ template(name="listHeader")
|/#{wipLimit.value})
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
else
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
if collapsed
i.fa.fa-caret-right
else
i.fa.fa-caret-down
if collapsed
a.js-collapse(title="{{_ 'uncollapse'}}")
| ⬅️
| ➡️
div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
if wipLimit.enabled
@ -48,54 +44,35 @@ template(name="listHeader")
unless collapsed
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
if isMiniScreen
if currentList
if isWatching
i.list-header-watch-icon i.fa.fa-eye
i.list-header-watch-icon | 👁️
div.list-header-menu
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
i.fa.fa-plus
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
else
a.list-header-menu-icon.js-select-list
i.fa.fa-caret-right
unless currentUser.isWorker
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle.handle.js-list-handle
i.fa.fa-arrows
a.list-header-menu-icon.js-select-list ▶️
a.list-header-handle.handle.js-list-handle ↕️
else if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon i.fa.fa-eye
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
i.fa.fa-arrows
i.list-header-watch-icon | 👁️
unless collapsed
div.list-header-menu
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
//if isBoardAdmin
//
a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
i.fa.fa-arrows
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
i.fa.fa-plus
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-collapse(title="{{_ 'collapse'}}")
| ⬅️
| ➡️
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
if currentUser.isBoardMember
unless currentUser.isCommentOnly
a.list-header-handle.handle.js-list-handle ↕️
template(name="editListTitleForm")
.list-composer
@ -103,78 +80,63 @@ template(name="editListTitleForm")
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
a.js-close-inlined-form
i.fa.fa-times-thin
| ❌
template(name="listActionPopup")
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
ul.pop-over-list
li
a.js-add-card.list-header-plus-top
i.fa.fa-plus
i.fa.fa-arrow-up
| {{_ 'add-card-to-top-of-list'}}
li
a.js-add-card.list-header-plus-bottom
i.fa.fa-plus
i.fa.fa-arrow-down
| {{_ 'add-card-to-bottom-of-list'}}
hr
ul.pop-over-list
li
a.js-add-list
i.fa.fa-plus
| {{_ 'add-list'}}
hr
ul.pop-over-list
li
a.js-set-list-width
i.fa.fa-arrows-h
| {{_ 'set-list-width'}}
ul.pop-over-list
li
a.js-add-card.list-header-plus-bottom
|
| ⬇️
| {{_ 'add-card-to-bottom-of-list'}}
hr
ul.pop-over-list
li
a.js-set-list-width
| ↔️
| {{_ 'set-list-width'}}
ul.pop-over-list
li
a.js-toggle-watch-list
if isWatching
i.fa.fa-eye
| 👁️
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| 🙈
| {{_ 'watch'}}
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
unless currentUser.isWorker
ul.pop-over-list
li
a.js-set-color-list
i.fa.fa-paint-brush
| {{_ 'set-color-list'}}
ul.pop-over-list
if cards.length
li
a.js-select-cards
i.fa.fa-select-square
| {{_ 'list-select-cards'}}
if currentUser.isBoardAdmin
ul.pop-over-list
li
a.js-set-wip-limit
i.fa.fa-ban
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a.js-close-list
i.fa.fa-arrow-right
i.fa.fa-archive
| {{_ 'archive-list'}}
hr
ul.pop-over-list
li
a.js-more
i.fa.fa-link
| {{_ 'listMorePopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-set-color-list
| 🎨
| {{_ 'set-color-list'}}
ul.pop-over-list
if cards.length
li
a.js-select-cards
| ☑️
| {{_ 'list-select-cards'}}
if currentUser.isBoardAdmin
ul.pop-over-list
li
a.js-set-wip-limit
| 🚫
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a.js-close-list
| ➡️
| 📦
| {{_ 'archive-list'}}
hr
ul.pop-over-list
li
a.js-more
| 🔗
| {{_ 'listMorePopup-title'}}
template(name="boardLists")
ul.pop-over-list
@ -190,15 +152,13 @@ template(name="listMorePopup")
span.clearfix
span {{_ 'link-list'}}
= ' '
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
//unless currentUser.isWorker
//
if currentUser.isBoardAdmin
//
a.js-delete {{_ 'delete'}}
// if currentUser.isBoardAdmin
// a.js-delete {{_ 'delete'}}
template(name="listDeletePopup")
p {{_ "list-delete-pop"}}
@ -212,7 +172,7 @@ template(name="setWipLimitPopup")
ul.pop-over-list
li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
if isWipLimitEnabled
i.fa.fa-check
| ✅
if isWipLimitEnabled
p
input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
@ -233,8 +193,8 @@ template(name="setListWidthPopup")
#js-list-width-edit
label {{_ 'set-list-width-value'}}
p
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270")
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="270")
input.list-width-value(type="number" value="{{ listWidthValue }}" min="100")
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="100")
input.list-width-apply(type="submit" value="{{_ 'apply'}}")
input.list-width-error
br
@ -245,7 +205,7 @@ template(name="setListWidthPopup")
template(name="listWidthErrorPopup")
.list-width-invalid
p {{_ 'list-width-error-message'}} '&gt;=270'
p {{_ 'list-width-error-message'}} '&gt;=100'
button.full.js-back-view(type="submit") {{_ 'cancel'}}
template(name="setListColorPopup")
@ -254,29 +214,6 @@ template(name="setListColorPopup")
// note: we use the swimlane palette to have more than just the border
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
| ✅
button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
template(name="addListPopup")
form.js-add-list-form
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}" autocomplete="off" autofocus)
if currentSwimlaneData
if swimlaneLists.length
label {{_ 'add-after-list'}}
select.list-position-input.full-line
each swimlaneLists
option(value="{{_id}}" selected="{{$eq _id currentListIdValue}}") {{increment @index}} {{title}}
else
if currentBoard.lists.length
label {{_ 'add-after-list'}}
select.list-position-input.full-line
each currentBoard.lists
option(value="{{_id}}" selected="{{$eq _id currentListIdValue}}") {{increment @index}} {{title}}
.edit-controls.clearfix
button.primary.confirm.js-submit-add-list(type="submit") {{_ 'save'}}
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-list-template {{_ 'template'}}

View file

@ -1,5 +1,4 @@
import { ReactiveCache } from '/imports/reactiveCache';
import Lists from '../../../models/lists';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
@ -22,37 +21,36 @@ BlazeComponent.extendComponent({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
async starred(check = undefined) {
starred(check = undefined) {
const list = Template.currentData();
const status = list.isStarred();
if (check === undefined) {
// just check
return status;
} else {
await list.star(!status);
list.star(!status);
return !status;
}
},
collapsed(check = undefined) {
const list = Template.currentData();
const status = Utils.getListCollapseState(list);
const status = list.isCollapsed();
if (check === undefined) {
// just check
return status;
} else {
const next = typeof check === 'boolean' ? check : !status;
Utils.setListCollapseState(list, next);
return next;
list.collapse(!status);
return !status;
}
},
async editTitle(event) {
editTitle(event) {
event.preventDefault();
const newTitle = this.childComponents('inlinedForm')[0]
.getValue()
.trim();
const list = this.currentData();
if (newTitle) {
await list.rename(newTitle.trim());
list.rename(newTitle.trim());
}
},
@ -144,48 +142,7 @@ BlazeComponent.extendComponent({
Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
numberFieldsSum() {
const list = Template.currentData();
if (!list) return 0;
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
if (!fields || !fields.length) return 0;
const cards = ReactiveCache.getCards({ listId: list._id, archived: false });
let total = 0;
if (cards && cards.length) {
cards.forEach(card => {
const cfs = (card.customFields || []);
fields.forEach(field => {
const cf = cfs.find(f => f && f._id === field._id);
if (!cf || cf.value === null || cf.value === undefined) return;
let v = cf.value;
if (typeof v === 'string') {
const parsed = parseFloat(v.replace(',', '.'));
if (isNaN(parsed)) return;
v = parsed;
}
if (typeof v === 'number' && isFinite(v)) {
total += v;
}
});
});
}
return total;
},
hasNumberFieldsSum() {
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
return !!(fields && fields.length);
},
}
});
Template.listActionPopup.helpers({
@ -204,27 +161,14 @@ Template.listActionPopup.helpers({
Template.listActionPopup.events({
'click .js-list-subscribe'() {},
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(`#js-list-${this._id}`)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
if (listComponent) {
listComponent.openForm({
position: 'top',
});
}
Popup.back();
},
'click .js-add-card.list-header-plus-bottom'(event) {
const listDom = $(`#js-list-${this._id}`)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
if (listComponent) {
listComponent.openForm({
position: 'bottom',
});
}
listComponent.openForm({
position: 'bottom',
});
Popup.back();
},
'click .js-add-list': Popup.open('addList'),
'click .js-set-list-width': Popup.open('setListWidth'),
'click .js-set-color-list': Popup.open('setListColor'),
'click .js-select-cards'() {
@ -239,9 +183,9 @@ Template.listActionPopup.events({
if (!err && ret) Popup.back();
});
},
async 'click .js-close-list'(event) {
'click .js-close-list'(event) {
event.preventDefault();
await this.archive();
this.archive();
Popup.back();
},
'click .js-set-wip-limit': Popup.open('setWipLimit'),
@ -268,26 +212,26 @@ BlazeComponent.extendComponent({
}
},
async enableSoftLimit() {
enableSoftLimit() {
const list = Template.currentData();
if (
list.getWipLimit('soft') &&
list.getWipLimit('value') < list.cards().length
) {
await list.setWipLimit(list.cards().length);
list.setWipLimit(list.cards().length);
}
Meteor.call('enableSoftLimit', Template.currentData()._id);
},
async enableWipLimit() {
enableWipLimit() {
const list = Template.currentData();
// Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
if (
!list.getWipLimit('enabled') &&
list.getWipLimit('value') < list.cards().length
) {
await list.setWipLimit(list.cards().length);
list.setWipLimit(list.cards().length);
}
Meteor.call('enableWipLimit', list._id);
},
@ -381,12 +325,12 @@ BlazeComponent.extendComponent({
'click .js-palette-color'() {
this.currentColor.set(this.currentData().color);
},
async 'click .js-submit'() {
await this.currentList.setColor(this.currentColor.get());
'click .js-submit'() {
this.currentList.setColor(this.currentColor.get());
Popup.close();
},
async 'click .js-remove-color'() {
await this.currentList.setColor(null);
'click .js-remove-color'() {
this.currentList.setColor(null);
Popup.close();
},
},
@ -412,7 +356,7 @@ BlazeComponent.extendComponent({
);
// FIXME(mark-i-m): where do we put constants?
if (width < 270 || !width || constraint < 270 || !constraint) {
if (width < 100 || !width || constraint < 100 || !constraint) {
Template.instance()
.$('.list-width-error')
.click();
@ -453,108 +397,3 @@ BlazeComponent.extendComponent({
];
},
}).register('setListWidthPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Utils.getCurrentBoard();
this.currentSwimlaneId = new ReactiveVar(null);
this.currentListId = new ReactiveVar(null);
// Get the swimlane context from opener
const openerData = Popup.getOpenerComponent()?.data();
// If opened from swimlane menu, openerData is the swimlane
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
this.currentSwimlane = openerData;
this.currentSwimlaneId.set(openerData._id);
} else if (openerData?._id) {
// If opened from list menu, get swimlane from the list
const list = ReactiveCache.getList({ _id: openerData._id });
this.currentSwimlane = list?.swimlaneId ? ReactiveCache.getSwimlane({ _id: list.swimlaneId }) : null;
this.currentSwimlaneId.set(this.currentSwimlane?._id || null);
this.currentListId.set(openerData._id);
}
},
currentSwimlaneData() {
const swimlaneId = this.currentSwimlaneId.get();
return swimlaneId ? ReactiveCache.getSwimlane({ _id: swimlaneId }) : null;
},
currentListIdValue() {
return this.currentListId.get();
},
swimlaneLists() {
const swimlaneId = this.currentSwimlaneId.get();
if (swimlaneId) {
return ReactiveCache.getLists({ swimlaneId, archived: false }).sort((a, b) => a.sort - b.sort);
}
return this.currentBoard.lists;
},
events() {
return [
{
'submit .js-add-list-form'(evt) {
evt.preventDefault();
const titleInput = this.find('.list-name-input');
const title = titleInput?.value.trim();
if (!title) return;
let sortIndex = 0;
const boardId = Utils.getCurrentBoardId();
let swimlaneId = this.currentSwimlane?._id;
const positionInput = this.find('.list-position-input');
if (positionInput && positionInput.value) {
const positionId = positionInput.value.trim();
const selectedList = ReactiveCache.getList({ boardId, _id: positionId, archived: false });
if (selectedList) {
sortIndex = selectedList.sort + 1;
// Use the swimlane ID from the selected list to ensure the new list
// is added to the same swimlane as the selected list
swimlaneId = selectedList.swimlaneId;
} else {
// No specific position, add at end of swimlane
if (swimlaneId) {
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
} else {
const lastList = this.currentBoard.getLastList();
sortIndex = Utils.calculateIndexData(lastList, null).base;
}
}
} else {
// No position input, add at end of swimlane
if (swimlaneId) {
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
} else {
const lastList = this.currentBoard.getLastList();
sortIndex = Utils.calculateIndexData(lastList, null).base;
}
}
Lists.insert({
title,
boardId: Session.get('currentBoard'),
sort: sortIndex,
type: 'list',
swimlaneId: swimlaneId,
});
Popup.back();
},
'click .js-list-template': Popup.open('searchElement'),
},
];
},
}).register('addListPopup');

View file

@ -3,7 +3,6 @@ template(name="minilist")
class="minicard-{{colorClass}}")
.minicard-title
.handle
span.drag-handle(title="{{_ 'dragList'}}")
i.fa.fa-arrows
.fa.fa-arrows
+viewer
= title

View file

@ -15,12 +15,12 @@ Template.bookmarks.helpers({
});
Template.bookmarks.events({
async 'click .js-toggle-star'(e) {
'click .js-toggle-star'(e) {
e.preventDefault();
const boardId = this._id;
const user = ReactiveCache.getCurrentUser();
if (user && boardId) {
await user.toggleBoardStar(boardId);
user.toggleBoardStar(boardId);
}
},
});
@ -42,12 +42,12 @@ Template.bookmarksPopup.helpers({
});
Template.bookmarksPopup.events({
async 'click .js-toggle-star'(e) {
'click .js-toggle-star'(e) {
e.preventDefault();
const boardId = this._id;
const user = ReactiveCache.getCurrentUser();
if (user && boardId) {
await user.toggleBoardStar(boardId);
user.toggleBoardStar(boardId);
}
},
});

View file

@ -1,23 +1,23 @@
template(name="dueCardsHeaderBar")
if currentUser
h1
i.fa.fa-calendar
| 📅
| {{_ 'dueCards-title'}}
.board-header-btns.left
a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}")
i.fa.fa-caret-down
| ▼
if $eq dueCardsView 'me'
i.fa.fa-user
| 👤
| {{_ 'dueCardsViewChange-choice-me'}}
if $eq dueCardsView 'all'
i.fa.fa-users
| 👥
| {{_ 'dueCardsViewChange-choice-all'}}
template(name="dueCardsModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'dueCards-title'}}
template(name="dueCards")
@ -49,18 +49,18 @@ template(name="dueCardsViewChangePopup")
li
with "dueCardsViewChange-choice-me"
a.js-due-cards-view-me
i.fa.fa-user
| 👤
| {{_ 'dueCardsViewChange-choice-me'}}
if $eq Utils.dueCardsView "me"
i.fa.fa-check
| ✅
hr
li
with "dueCardsViewChange-choice-all"
a.js-due-cards-view-all
i.fa.fa-users
| 👥
| {{_ 'dueCardsViewChange-choice-all'}}
span.sub-name
+viewer
| {{_ 'dueCardsViewChange-choice-all-description' }}
if $eq Utils.dueCardsView "all"
i.fa.fa-check
| ✅

View file

@ -92,14 +92,14 @@ BlazeComponent.extendComponent({
class DueCardsComponent extends BlazeComponent {
onCreated() {
super.onCreated();
this._cachedCards = null;
this._cachedTimestamp = null;
this.subscriptionHandle = null;
this.isLoading = new ReactiveVar(true);
this.hasResults = new ReactiveVar(false);
this.searching = new ReactiveVar(false);
// Subscribe to the optimized due cards publication
this.autorun(() => {
const allUsers = this.dueCardsView() === 'all';
@ -107,7 +107,7 @@ class DueCardsComponent extends BlazeComponent {
this.subscriptionHandle.stop();
}
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
// Update loading state based on subscription
this.autorun(() => {
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
@ -162,7 +162,7 @@ class DueCardsComponent extends BlazeComponent {
// Get the translated text and manually replace %s with the count
const baseText = TAPi18n.__('n-cards-found');
const result = baseText.replace('%s', count);
if (process.env.DEBUG === 'true') {
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
}
@ -196,10 +196,10 @@ class DueCardsComponent extends BlazeComponent {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: found', cards.length, 'cards with due dates');
console.log('dueCards client: cards details:', cards.map(c => ({
id: c._id,
title: c.title,
dueAt: c.dueAt,
console.log('dueCards client: cards details:', cards.map(c => ({
id: c._id,
title: c.title,
dueAt: c.dueAt,
boardId: c.boardId,
members: c.members,
assignees: c.assignees,
@ -223,33 +223,15 @@ class DueCardsComponent extends BlazeComponent {
const isAssignee = card.assignees && card.assignees.includes(currentUser._id);
const isAuthor = card.userId === currentUser._id;
const matches = isMember || isAssignee || isAuthor;
if (process.env.DEBUG === 'true' && matches) {
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
}
return matches;
});
}
// Normalize dueAt to timestamps for stable client-side ordering
const future = new Date('2100-12-31').getTime();
const toTime = v => {
if (v === null || v === undefined || v === '') return future;
if (v instanceof Date) return v.getTime();
const t = new Date(v);
if (!isNaN(t.getTime())) return t.getTime();
return future;
};
filteredCards.sort((a, b) => {
const x = toTime(a.dueAt);
const y = toTime(b.dueAt);
if (x > y) return 1;
if (x < y) return -1;
return 0;
});
if (process.env.DEBUG === 'true') {
console.log('dueCards client: filtered to', filteredCards.length, 'cards');
}

View file

@ -1,6 +1,8 @@
template(name="editor")
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
a(title="{{_ 'convert-to-markdown'}}")
| 📝
a(title="{{_ 'copy-text-to-clipboard'}}")
| 📋
span.copied-tooltip {{_ 'copied'}}
textarea.editor(
dir="auto"

View file

@ -4,17 +4,7 @@ var converter = require('@wekanteam/html-to-markdown');
const specialHandles = [
{userId: 'board_members', username: 'board_members'},
{userId: 'card_members', username: 'card_members'},
{userId: 'board_assignees', username: 'board_assignees'},
{userId: 'card_assignees', username: 'card_assignees'}
];
const cardSpecialHandles = [
{userId: 'card_members', username: 'card_members'},
{userId: 'card_assignees', username: 'card_assignees'}
];
const boardSpecialHandles = [
{userId: 'board_members', username: 'board_members'},
{userId: 'board_assignees', username: 'board_assignees'}
{userId: 'card_members', username: 'card_members'}
];
const specialHandleNames = specialHandles.map(m => m.username);
@ -55,27 +45,23 @@ BlazeComponent.extendComponent({
match: /\B@([\w.-]*)$/,
search(term, callback) {
const currentBoard = Utils.getCurrentBoard();
const searchTerm = term.toLowerCase();
const users = currentBoard
.activeMembers()
.map(member => {
const user = ReactiveCache.getUser(member.userId);
const username = user.username.toLowerCase();
const fullName = user.profile && user.profile !== undefined && user.profile.fullname ? user.profile.fullname.toLowerCase() : "";
return username.includes(searchTerm) || fullName.includes(searchTerm) ? user : null;
})
.filter(Boolean);
// Order: 1. Users, 2. Card-specific options, 3. Board-wide options
callback(_.union(users, cardSpecialHandles, boardSpecialHandles));
callback(
_.union(
currentBoard
.activeMembers()
.map(member => {
const user = ReactiveCache.getUser(member.userId);
const username = user.username;
const fullName = user.profile && user.profile !== undefined && user.profile.fullname ? user.profile.fullname : "";
return username.includes(term) || fullName.includes(term) ? user : null;
})
.filter(Boolean), [...specialHandles])
);
},
template(user) {
if (user.profile && user.profile.fullname) {
return (user.profile.fullname + " (" + user.username + ")");
}
// Translate special group mentions
if (specialHandleNames.includes(user.username)) {
return TAPi18n.__(user.username);
}
return user.username;
},
replace(user) {
@ -387,15 +373,13 @@ Blaze.Template.registerHelper(
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard)
return HTML.Raw(sanitizeHTML(content));
const knowedUsers = _.union(currentBoard.members
.filter(member => member.isActive)
.map(member => {
const u = ReactiveCache.getUser(member.userId);
if (u) {
member.username = u.username;
}
return member;
}), [...specialHandles]);
const knowedUsers = _.union(currentBoard.members.map(member => {
const u = ReactiveCache.getUser(member.userId);
if (u) {
member.username = u.username;
}
return member;
}), [...specialHandles]);
const mentionRegex = /\B@([\w.-]*)/gi;
let currentMention;
@ -412,14 +396,6 @@ Blaze.Template.registerHelper(
if (knowedUser.userId === Meteor.userId()) {
linkClass += ' me';
}
// For special group mentions, display translated text
let displayText = knowedUser.username;
if (specialHandleNames.includes(knowedUser.username)) {
displayText = TAPi18n.__(knowedUser.username);
linkClass = 'atMention'; // Remove js-open-member for special handles
}
// This @user mention link generation did open same Wekan
// window in new tab, so now A is changed to U so it's
// underlined and there is no link popup. This way also
@ -434,7 +410,7 @@ Blaze.Template.registerHelper(
// using a data attribute.
'data-userId': knowedUser.userId,
},
[' ', at, displayText],
linkValue,
);
content = content.replace(fullMention, Blaze.toHTML(link));

View file

@ -1,13 +1,13 @@
template(name="globalSearchHeaderBar")
if currentUser
h1
i.fa.fa-search
| 🔍
| {{_ 'globalSearch-title'}}
template(name="globalSearchModalTitle")
if currentUser
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'globalSearch-title'}}
template(name="resultsPaged")

View file

@ -78,26 +78,12 @@
#header #header-main-bar .board-header-btn .board-header-btn-close i.fa {
margin: 0 6px;
}
#header #header-main-bar .board-header-btn .board-header-btn-icon {
float: left;
display: block;
line-height: 28px;
color: #27ae60;
margin: 0 10px;
cursor: pointer;
}
#header #header-main-bar .board-header-btn.is-active,
#header #header-main-bar h1.is-clickable.is-active,
#header #header-main-bar .board-header-btn:hover:not(.is-disabled),
#header #header-main-bar h1.is-clickable:hover:not(.is-disabled) {
background: rgba(0,0,0,0.15);
}
#header #header-main-bar .board-header-btn.js-multiselection-active {
background: #1a5080;
}
#header #header-main-bar .board-header-btn.js-multiselection-active:hover {
background: #0f3a5f;
}
#header #header-main-bar .separator {
margin: 2px 4px;
border-left: 1px solid rgba(255,255,255,0.3);
@ -177,7 +163,8 @@
}
#header-quick-access ul.header-quick-access-list {
transition: opacity 0.2s;
overflow: hidden;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 10px;
margin: -10px;
@ -185,16 +172,26 @@
min-width: 0; /* Allow shrinking below content size */
display: flex; /* Use flexbox for better control */
align-items: center;
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(255, 255, 255, 0.3) transparent; /* Firefox */
}
/* Hide scrollbar completely */
/* Webkit scrollbar styling for better UX */
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar {
display: none;
height: 4px;
}
#header-quick-access ul.header-quick-access-list {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-track {
background: transparent;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
#header-quick-access ul.header-quick-access-list li {
display: inline-block; /* Keep inline-block for proper spacing */
@ -222,13 +219,6 @@
}
#header-quick-access ul.header-quick-access-list li.current.empty {
padding: 12px 10px 12px 10px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
opacity: 0.85;
font-style: italic;
}
#header-quick-access ul.header-quick-access-list li:first-child .fa-home,
#header-quick-access ul.header-quick-access-list li:nth-child(3) .fa-globe {
@ -349,20 +339,15 @@
width: 100%;
min-width: 3vw;
font-size: clamp(12px, 2vw, 14px);
box-sizing: border-box;
-webkit-appearance: none;
appearance: none;
flex: 0 0 auto;
}
/* Make zoom input wider on all mobile screens */
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
#header-quick-access .zoom-controls .zoom-input {
min-width: 80px !important; /* Wider on mobile to show 3 digits */
width: 80px !important; /* Fixed width to show 100 fully */
font-size: 16px !important; /* Slightly larger text */
flex: 0 0 80px !important; /* Prevent shrinking in flex */
min-width: 50px !important; /* Wider on mobile */
width: 50px !important; /* Fixed width to show all numbers */
font-size: 14px !important; /* Slightly larger text */
}
}
@ -865,9 +850,8 @@
#header-quick-access .zoom-controls .zoom-input {
font-size: 16px !important; /* Larger input text */
padding: 0.5vh 0.8vw !important;
min-width: 80px !important; /* Wider to fit 100 */
width: 80px !important; /* Fixed width to show 100 fully */
flex: 0 0 80px !important; /* Prevent shrinking in flex */
min-width: 6vw !important; /* Much wider for mobile */
width: 60px !important; /* Fixed width to show all numbers */
}
/* Make mobile mode toggle larger */

View file

@ -9,7 +9,7 @@ template(name="header")
// Home icon - always at left side of logo
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
i.fa.fa-home
| 🏠
| {{_ 'all-boards'}}
// Logo - visible; on mobile constrained by CSS
@ -30,14 +30,6 @@ template(name="header")
span.zoom-display {{zoomLevel}}%
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
// Drag handles toggle - between zoom and mobile mode toggle
a.board-header-btn.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}")
i.fa.fa-arrows
if isShowDesktopDragHandles
i.fa.fa-check
unless isShowDesktopDragHandles
i.fa.fa-ban
if isMiniScreen
ul.header-quick-access-list
if currentList
@ -52,42 +44,48 @@ template(name="header")
a(href="{{pathFor 'board' id=_id slug=slug}}")
+viewer
= title
//a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
// i.fa.fa-arrows
// if isShowDesktopDragHandles
// i.fa.fa-check-square-o
// unless isShowDesktopDragHandles
// i.fa.fa-ban
#header-new-board-icon
else
ul.header-quick-access-list
//li
//
a(href="{{pathFor 'public'}}")
//
span.fa.fa-globe
//
| {{_ 'public'}}
// a(href="{{pathFor 'public'}}")
// span.fa.fa-globe
// | {{_ 'public'}}
each currentUser.starredBoards
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
a(href="{{pathFor 'board' id=_id slug=slug}}")
+viewer
= title
else
li.current.empty(title="{{_ 'quick-access-description'}}")
| {{_ 'quick-access-description'}}
#header-new-board-icon
li.current.empty {{_ 'quick-access-description'}}
//a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
// i.fa.fa-arrows
// if isShowDesktopDragHandles
// i.fa.fa-check-square-o
// unless isShowDesktopDragHandles
// i.fa.fa-ban
// Next line is used only for spacing at header,
// there is no visible clickable icon.
#header-new-board-icon
//
Hide duplicate create board button,
//
because it did not show board templates correctly.
// Hide duplicate create board button,
// because it did not show board templates correctly.
//a#header-new-board-icon.js-create-board
//
i.fa.fa-plus(title="Create a new 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.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-mobile
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.fa.fa-desktop
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
// Bookmarks button - desktop opens popup, mobile routes to page
a.board-header-btn.js-open-bookmarks(title="{{_ 'bookmarks'}}")
| 🔖
// Notifications
+notifications
@ -95,7 +93,7 @@ template(name="header")
if currentSetting.customHelpLinkUrl
#header-help
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
i.fa.fa-question-circle
| ❓
+headerUserBar
@ -114,17 +112,15 @@ template(name="header")
if hasAnnouncement
.announcement
p
i.fa.fa-bullhorn
| 📢
+viewer
| #{announcement}
a
.js-close-announcement
i.fa.fa-times-thin
| ❌
template(name="offlineWarning")
.offline-warning
p
i.fa.fa-warning
| ⚠️
| {{_ 'app-is-offline'}}
a.app-try-reconnect {{_ 'app-try-reconnect'}}

View file

@ -1,11 +1,10 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
Meteor.subscribe('user-admin');
Meteor.subscribe('boards');
Meteor.subscribe('setting');
Meteor.subscribe('announcements');
Template.header.onCreated(function () {
Template.header.onCreated(function(){
const templateInstance = this;
templateInstance.currentSetting = new ReactiveVar();
templateInstance.isLoading = new ReactiveVar(false);
@ -14,21 +13,10 @@ Template.header.onCreated(function () {
onReady() {
templateInstance.currentSetting.set(ReactiveCache.getCurrentSetting());
let currSetting = templateInstance.currentSetting.curValue;
if (
currSetting &&
currSetting !== undefined &&
currSetting.customLoginLogoImageUrl !== undefined &&
document.getElementById('headerIsSettingDatabaseCallDone') != null
)
document.getElementById(
'headerIsSettingDatabaseCallDone',
).style.display = 'none';
else if (
document.getElementById('headerIsSettingDatabaseCallDone') != null
)
document.getElementById(
'headerIsSettingDatabaseCallDone',
).style.display = 'block';
if(currSetting && currSetting !== undefined && currSetting.customLoginLogoImageUrl !== undefined && document.getElementById("headerIsSettingDatabaseCallDone") != null)
document.getElementById("headerIsSettingDatabaseCallDone").style.display = 'none';
else if(document.getElementById("headerIsSettingDatabaseCallDone") != null)
document.getElementById("headerIsSettingDatabaseCallDone").style.display = 'block';
return this.stop();
},
});
@ -86,15 +74,10 @@ Template.header.events({
},
'keypress .js-zoom-input'(evt) {
if (evt.which === 13) {
// Enter key
if (evt.which === 13) { // Enter key
const newZoomPercent = parseInt(evt.target.value);
if (
!isNaN(newZoomPercent) &&
newZoomPercent >= 50 &&
newZoomPercent <= 300
) {
if (!isNaN(newZoomPercent) && newZoomPercent >= 50 && newZoomPercent <= 300) {
const newZoom = newZoomPercent / 100;
Utils.setZoomLevel(newZoom);
@ -132,10 +115,11 @@ Template.header.events({
Session.set('currentCard', null);
},
'click .js-toggle-desktop-drag-handles'() {
currentUser = Meteor.user();
if (currentUser) {
Meteor.call('toggleDesktopDragHandles');
} else if (window.localStorage.getItem('showDesktopDragHandles')) {
//currentUser = Meteor.user();
//if (currentUser) {
// Meteor.call('toggleDesktopDragHandles');
//} else if (window.localStorage.getItem('showDesktopDragHandles')) {
if (window.localStorage.getItem('showDesktopDragHandles')) {
window.localStorage.removeItem('showDesktopDragHandles');
location.reload();
} else {

View file

@ -6,7 +6,7 @@ template(name="shortcutsHeaderBar")
template(name="shortcutsModalTitle")
h2
i.fa.fa-keyboard-o
| ⌨️
| {{_ 'keyboard-shortcuts'}}
template(name="keyboardShortcuts")

View file

@ -81,27 +81,6 @@ body {
display: flex;
flex-direction: column;
height: 100vh;
/* iOS Safari fixes */
-webkit-overflow-scrolling: touch;
}
/* Mobile mode specific fixes for iOS Safari */
body.mobile-mode {
overflow-x: hidden;
position: fixed;
width: 100%;
height: 100vh;
/* Prevent iOS Safari bounce scroll */
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
}
/* Ensure content area is scrollable in mobile mode */
body.mobile-mode #content {
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
height: calc(100vh - 48px);
}
#content {
position: relative;
@ -515,7 +494,7 @@ a:not(.disabled).is-active i.fa {
max-width: 95vw;
margin: 0 auto;
}
/* Improve touch targets */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 44px;
@ -524,7 +503,7 @@ a:not(.disabled).is-active i.fa {
font-size: 16px; /* Prevent zoom on iOS */
touch-action: manipulation;
}
/* Form elements */
input, select, textarea {
font-size: 16px; /* Prevent zoom on iOS */
@ -532,7 +511,7 @@ a:not(.disabled).is-active i.fa {
min-height: 44px;
touch-action: manipulation;
}
/* Cards and lists */
.minicard {
min-height: 48px;
@ -540,19 +519,19 @@ a:not(.disabled).is-active i.fa {
margin-bottom: 8px;
touch-action: manipulation;
}
.list {
margin: 0 8px;
min-width: 280px;
}
/* Board canvas */
.board-canvas {
padding: 0 8px 8px 0;
padding: 8px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Header mobile layout */
#header {
padding: 8px;
@ -561,7 +540,7 @@ a:not(.disabled).is-active i.fa {
align-items: center;
gap: 8px;
}
#header-quick-access {
/* Keep quick-access items in one row */
display: flex;
@ -585,43 +564,43 @@ a:not(.disabled).is-active i.fa {
overflow: hidden;
white-space: nowrap;
}
/* Hide text in home icon on mobile, show only icon */
#header-quick-access .home-icon a span:not(.fa) {
display: none !important;
}
/* Ensure proper spacing for mobile header elements */
#header-quick-access .zoom-controls {
margin-left: auto;
margin-right: 8px;
}
.mobile-mode-toggle {
margin-right: 8px;
}
#header-user-bar {
margin-left: auto;
}
/* Ensure header elements don't wrap on very small screens */
#header-quick-access {
min-width: 0; /* Allow flexbox to shrink */
}
/* Make sure logo doesn't take too much space on mobile */
#header-quick-access img {
max-height: 24px;
max-width: 120px;
}
/* Ensure zoom controls are compact on mobile */
.zoom-controls .zoom-level {
padding: 4px 8px;
font-size: 12px;
}
/* Modal mobile optimization */
#modal .modal-content,
#modal .modal-content-wide {
@ -632,7 +611,7 @@ a:not(.disabled).is-active i.fa {
max-height: 90vh;
overflow-y: auto;
}
/* Table mobile optimization */
table {
font-size: 14px;
@ -642,19 +621,19 @@ a:not(.disabled).is-active i.fa {
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
/* Admin panel mobile optimization */
.setting-content .content-body {
flex-direction: column;
gap: 16px;
padding: 8px;
}
.setting-content .content-body .side-menu {
width: 100%;
order: 2;
}
.setting-content .content-body .main-body {
order: 1;
min-height: 60vh;
@ -668,63 +647,58 @@ a:not(.disabled).is-active i.fa {
#content > .wrapper {
padding: 12px;
}
.wrapper {
padding: 12px;
}
.panel-default {
width: 90vw;
max-width: 90vw;
}
/* Touch-friendly but more compact */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 48px;
min-width: 48px;
padding: 10px 14px;
}
.minicard {
min-height: 40px;
padding: 10px;
}
.list {
margin: 0 12px;
min-width: 300px;
}
.board-canvas {
padding: 0 12px 12px 0;
padding: 12px;
}
#header {
padding: 12px 16px;
}
#modal .modal-content {
width: 80vw;
max-width: 600px;
}
#modal .modal-content-wide {
width: 90vw;
max-width: 800px;
}
.setting-content .content-body {
gap: 20px;
}
.setting-content .content-body .side-menu {
width: 250px;
}
/* Responsive handling for quick-access description on tablets */
#header-quick-access ul.header-quick-access-list li.current.empty {
max-width: 300px;
}
}
/* Large displays and digital signage (1920px+) */
@ -732,49 +706,49 @@ a:not(.disabled).is-active i.fa {
body {
font-size: 18px;
}
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 56px;
min-width: 56px;
padding: 16px 20px;
font-size: 18px;
}
.minicard {
min-height: 56px;
padding: 16px;
font-size: 18px;
}
.list {
margin: 0 8px;
min-width: 360px;
}
.board-canvas {
padding: 0;
}
#header {
padding: 0 8px;
}
#content > .wrapper {
padding: 0;
}
#modal .modal-content {
width: 600px;
}
#modal .modal-content-wide {
width: 1000px;
}
.setting-content .content-body {
gap: 32px;
}
.setting-content .content-body .side-menu {
width: 320px;
}
@ -782,7 +756,7 @@ a:not(.disabled).is-active i.fa {
.inline-input {
height: 37px;
margin: 8px 10px 0 0;
width: 100px;
width: 50px;
}
.select-authentication {
width: 100%;
@ -925,40 +899,6 @@ a:not(.disabled).is-active i.fa {
height: 100%;
}
}
/* iOS Safari Mobile Mode Fixes */
@media screen and (max-width: 800px) {
/* Prevent scrolling issues on iOS Safari when card popup is open */
body.mobile-mode {
overflow: hidden;
position: fixed;
width: 100%;
height: 100vh;
}
/* Fix z-index stacking for mobile Safari */
body.mobile-mode .board-wrapper {
z-index: 1;
}
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
body.mobile-mode .card-details {
z-index: 100 !important;
}
body.mobile-mode .pop-over {
z-index: 999;
}
/* Ensure smooth scrolling on iOS */
body.mobile-mode .card-details,
body.mobile-mode .pop-over .content-wrapper {
-webkit-overflow-scrolling: touch;
}
}
@-moz-keyframes lds-roller {
0% {
transform: rotate(0deg);

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