Compare commits

..

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

621 changed files with 31535 additions and 80677 deletions

View file

@ -96,19 +96,23 @@
"autosize": false,
"Avatar": true,
"Avatars": true,
"BlazeComponent": false,
"BlazeLayout": false,
"CollectionHooks": false,
"DocHead": false,
"ESSearchResults": false,
"FastRender": false,
"FlowRouter": false,
"FS": false,
"getSlug": false,
"Migrations": false,
"moment": false,
"Mousetrap": false,
"Picker": false,
"Presence": true,
"presences": true,
"Ps": true,
"ReactiveTabs": false,
"Restivus": false,
"SimpleSchema": false,
"SubsManager": false,
@ -129,11 +133,13 @@
"CSSEvents": true,
"EscapeActions": true,
"Filter": true,
"Mixins": true,
"Modal": true,
"MultiSelection": true,
"Popup": true,
"Sidebar": true,
"Utils": true,
"InlinedForm": true,
"UnsavedEdits": true,
"Notifications": true,
"allowIsBoardAdmin": true,

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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@ -48,14 +48,14 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

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
@ -125,7 +125,7 @@ jobs:
v1-meteor_build_cache-
- name: Setup meteor
uses: meteorengineer/setup-meteor@v3
uses: meteorengineer/setup-meteor@v2
with:
meteor-release: '2.2'
@ -136,7 +136,7 @@ jobs:
run: sh ./test-wekan.sh -cv
- name: Upload coverage
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v5
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@v8
uses: actions/download-artifact@v6
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,10 @@ 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
email@2.2.5
@ -59,23 +71,26 @@ msavin:usercache
meteorhacks:subs-manager
meteorhacks:aggregate@1.3.0
wekan-markdown
quave:synced-cron
konecty:mongo-counter
percolate:synced-cron
ostrio:cookies
ostrio:files@2.3.0
pascoual:pdfkit
lmieulet:meteor-coverage
meteortesting:mocha@2.0.3
aldeed:simple-schema
matb33:collection-hooks
simple:json-routes
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
wekan-fontawesome
useraccounts:flow-routing-extra
ostrio:flow-router-extra
momentjs:moment@2.29.3
# 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,11 +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
@ -64,32 +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
mongo@1.16.10
momentjs:moment@2.29.3
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
@ -99,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
@ -114,6 +136,7 @@ socket-stream-client@0.5.2
spacebars@1.4.1
spacebars-compiler@1.3.1
standard-minifier-js@2.8.1
templates:tabs@2.3.0
templating@1.4.1
templating-compiler@1.4.1
templating-runtime@1.5.0
@ -121,20 +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.2.0
wekan-accounts-lockout@1.1.0
wekan-accounts-cas@0.1.0
wekan-accounts-lockout@1.0.0
wekan-accounts-oidc@1.0.10
wekan-accounts-sandstorm@0.9.0
wekan-fontawesome@6.4.2
wekan-fullcalendar@5.11.5
wekan-ldap@0.1.0
wekan-accounts-sandstorm@0.8.0
wekan-fullcalendar@3.10.5
wekan-ldap@0.0.2
wekan-markdown@1.0.9
wekan-oidc@1.1.0
zodern:types@1.0.13
wekan-oidc@1.0.12
yasaricli:slugify@0.0.7
zimme:active-route@2.3.2
zodern:types@1.0.10

View file

@ -1 +0,0 @@
npm-packages/

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,660 +8,23 @@ 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.
Upgrading to Meteor 3 progress:
- https://harryadel.com/dev-diary-24/
- https://harryadel.com/dev-diary-25/
# Upcoming WeKan ® release
This release adds the following updates:
- [Bump docker/build-push-action from 6.19.2 to 7.0.0](https://github.com/wekan/wekan/pull/6181).
Thanks to dependabot.
- [Bump docker/metadata-action from 5.10.0 to 6.0.0](https://github.com/wekan/wekan/pull/6182).
Thanks to dependabot.
- [Bump docker/login-action from 3.7.0 to 4.0.0](https://github.com/wekan/wekan/pull/6183).
Thanks to dependabot.
- [Remove peerlibrary:blaze-components](https://github.com/wekan/wekan/pull/6178).
Thanks to harryadel.
and fixes the following bugs:
- [Fixed linked card swimlane routing](https://github.com/wekan/wekan/pull/6179).
Thanks to KhaoulaMaleh.
- [Replaced incompatible file-type with mime-type](https://github.com/wekan/wekan/commit/89f86caf69db0600a207aee075361f8a6801253b).
Thanks to xet7.
- [Fix Add List popup to not open after adding new board or there being no lists at all](https://github.com/wekan/wekan/commit/7e378be1d87280b8fb3f63eea3c0374e12054984).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.35 2026-03-05 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES of [IntegrationBleed](https://wekan.fi/hall-of-fame/integrationBleed/):
- [Fix IntegrationBleed](https://github.com/wekan/wekan/commit/2cd702f48df2b8aef0e7381685f8e089986a18a4).
Thanks to Rodolphe GHIO and xet7.
and adds the following updates:
- [Bump minimatch from 3.1.3 to 3.1.5](https://github.com/wekan/wekan/pull/6167).
Thanks to dependabot.
- [Bump actions/download-artifact from 7 to 8](https://github.com/wekan/wekan/pull/6170).
Thanks to dependabot.
- [Bump meteorengineer/setup-meteor from 2 to 3](https://github.com/wekan/wekan/pull/6171).
Thanks to dependabot.
- [Bump actions/upload-artifact from 6 to 7](https://github.com/wekan/wekan/pull/6172).
Thanks to dependabot.
- [Replace konecty:mongo-counter with inline implementation](https://github.com/wekan/wekan/pull/6174).
Thanks to harryadel.
- [Replace templates:tabs package with inline Blaze implementation](https://github.com/wekan/wekan/pull/6175).
Thanks to harryadel.
- [Updated dompurify](https://github.com/wekan/wekan/commit/9f79a8b6edc161f95c7362f45597b8c6ec777088).
Thanks to xet7.
and fixes the following bugs:
- [Commented out Admin Panel/Settings/Migrations related menu option and code to speed up WeKan](https://github.com/wekan/wekan/commit/9b3ecd795fffaf012911d0d36cea0ee362e2fc27).
Thanks to xet7.
- [Optimized board loading](https://github.com/wekan/wekan/commit/7127862bea34ab84ebf8ef00727e3f7633ca8b69).
Thanks to xet7.
- [Fix FilesCollection findOneAsync errors for Avatars and Attachments](https://github.com/wekan/wekan/pull/6173).
Thanks to harryadel.
- [Fixed unable to delete Avatar, with Meteor 3 compatible avatar and attachments fixes](https://github.com/wekan/wekan/commit/274f1309c389221915b40508faffdfc361d48bbf).
Thanks to inDane and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.34 2026-02-20 WeKan ® release
This release adds the following CRITICAL SECURITY FIXES of [AnchorBleed](https://wekan.fi/hall-of-fame/anchorBleed/):
- [Fix GHSL-2026-035_Wekan CursorBleed of AnchorBleed](https://github.com/wekan/wekan/commit/1c8667eae8b28739e43569b612ffdb2693c6b1ce).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-036_Wekan WatchBleed of AnchorBleed](https://github.com/wekan/wekan/commit/8c00adc6b865653bd717a946dd646eb54ac78c9c).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-037_Wekan GlobalBleed of AnchorBleed](https://github.com/wekan/wekan/commit/1ee9b2e917104f54c035f6426169a28fedecbdb6).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-044_Wekan CustomFieldBleed of AnchorBleed](https://github.com/wekan/wekan/commit/73eb98c57afd3d72377a1f7160a52450ab0eeb8b).
Thanks to GHSL and xet7.
- [Fix GHSL-2026-045_Wekan ImportBleed of AnchorBleed](https://github.com/wekan/wekan/commit/62216e36c15f55d4ef6cb97313db3aa54fc77fe0).
Thanks to GHSL and xet7.
and adds the following new features:
- [Helm Chart: Feat(ingress): add ingressClassName support](https://github.com/wekan/charts/pull/50).
Thanks to Rohmilchkaese.
and adds the following updates:
- [Migrate @wekanteam/meteor-reactive-cache](https://github.com/wekan/wekan/pull/6139).
Thanks to harryadel.
- [Fix unhandled Promise rejection in cron migration job callback](https://github.com/wekan/wekan/pull/6153).
Thanks to harryadel.
- [Bump docker/build-push-action from 6.18.0 to 6.19.2](https://github.com/wekan/wekan/pull/6149).
Thanks to dependabot.
- [Bump ajv from 6.12.6 to 8.18.0](https://github.com/wekan/wekan/pull/6151).
Thanks to dependabot.
- [Bump tar from 7.5.7 to 7.5.9](https://github.com/wekan/wekan/pull/6156).
Thanks to dependabot.
- [Updated dependencies](https://github.com/wekan/wekan/commit/f463198e40f9802c0a30f2d713d831e905678162).
Thanks to developers of dependencies.
- [Moved meteor-reactive-cache to npmjs.com @wekanteam/meteor-reactive-cache https://github.com/wekan/meteor-reactive-cache](https://github.com/wekan/wekan/commit/8816c886cf740ec43c4c00c946730e6c2f3a8237).
Thanks to xet7.
and fixes the following bugs:
- [Fix calendar](https://github.com/wekan/wekan/pull/6155).
Thanks to KhaoulaMaleh.
- [Removed duplicate code](https://github.com/wekan/wekan/commit/ed907f8c61f59763a87cc738f94bff418de77701).
Thanks to xet7.
- [Fix createWorkspace Meteor method fails with "Expected string, got undefined"](https://github.com/wekan/wekan/commit/06d418b12b5de6392dab12c2d3b262813b92e730).
Thanks to TheBoysenBuilds and xet7.
- [Fix Notifications from not allowed Boards](https://github.com/wekan/wekan/commit/a34c2f35a6c4ae64b97af0a930fb768b2d781938).
Thanks to FK-PATZ3 and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.33 2026-02-15 WeKan ® release
This release adds the following new features:
- [Admin Panel/Settings/Layout, for PWA: Custom head meta, link, icons, assetlinks.json, site.webmanifest](https://github.com/wekan/wekan/commit/b5a13f0206ff9b44329a1cf8d4f2b84ca1c7bd91).
Thanks to xet7.
and adds the following updates:
- [Migrate wekan-ldap to async API for Meteor 3.0](https://github.com/wekan/wekan/pull/6115).
Thanks to harryadel.
- [Updated dependencies](https://github.com/wekan/wekan/commit/bebea9efeab098f7f5faca3f75019fd9efbcb5ac).
Thanks to developers of dependencies.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.32 2026-02-13 WeKan ® release
This release adds the following updates:
- [Migrate wekan-oidc to async API for Meteor 3.0](https://github.com/wekan/wekan/pull/6111).
Thanks to harryadel.
- [Migrate wekan-accounts-sandstorm to async API for Meteor 3.0](https://github.com/wekan/wekan/pull/6112).
Thanks to harryadel.
- [Migrate wekan-accounts-cas to async API for Meteor 3.0](https://github.com/wekan/wekan/pull/6114).
Thanks to harryadel.
- [Updated to MongoDB 7.0.30 at Snap Candidate](https://github.com/wekan/wekan/commit/fed2e9dd4e3c571795af24f60c6643a33bb5ecf9).
Thanks to MongoDB developers.
- [Updated MongoDB to 7.0.30 at Helm Chart](https://github.com/wekan/wekan/commit/commit/98f66a2b92f7a2c199135e8239133ef431c332b9).
Thanks to MongoDB developers.
Thanks to above GitHub users for their contributions and translators for their translations.
# 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/):
This release fixes SpaceBleed that is the following CRITICAL SECURITY ISSUES:
- [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.
@ -3880,7 +3243,7 @@ Thanks to above GitHub users for their contributions and translators for their t
This release fixes the following CRITICAL SECURITY ISSUES:
- Security Fix of FileBleed in WeKan. That is XSS in filename.
- Security Fix of Filebleed in WeKan. That is XSS in filename.
[Part 1](https://github.com/wekan/wekan/commit/ff993e7c917b5650a790238e95c78001e4f0e039),
[Part 2](https://github.com/wekan/wekan/commit/382168a5b428a7124d368c4fcb37e7e140e7ec8b).
Thanks to responsible security disclosure contributors and xet7.

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.35/wekan-8.35-${WEKAN_ARCH}.zip"
unzip "wekan-8.35-${WEKAN_ARCH}.zip"
rm "wekan-8.35-${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.16/wekan-8.16-amd64.zip"
unzip wekan-8.16-amd64.zip
rm wekan-8.16-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,203 +0,0 @@
# Meteor 3.0 Migration Guide
Reference document capturing patterns, constraints, and lessons learned during the async migration of WeKan from Meteor 2.16 toward Meteor 3.0 readiness.
---
## 1. Dual-Compatibility Strategy
WeKan runs on **Meteor 2.16 with Blaze 2.x**. The goal is dual compatibility: changes must work on 2.16 now and remain compatible with a future Meteor 3.0 upgrade.
**Key constraint:** Blaze 2.x does NOT support async template helpers. Client-side code must receive synchronous data.
---
## 2. ReactiveCache Facade Pattern
`ReactiveCache` dispatches to `ReactiveCacheServer` (async MongoDB) or `ReactiveCacheClient` (sync Minimongo).
**Rule:** Facade methods must NOT be `async`. They return a Promise on the server and data on the client. Server callers `await`; client code uses the return value directly.
```javascript
// CORRECT:
getBoard(boardId) {
if (Meteor.isServer) {
return ReactiveCacheServer.getBoard(boardId); // Returns Promise
} else {
return ReactiveCacheClient.getBoard(boardId); // Returns data
}
}
// WRONG:
async getBoard(boardId) { ... } // Wraps client return in Promise too!
```
---
## 3. Model Helpers (Collection.helpers)
Model helpers defined via `Collection.helpers({})` are used by Blaze templates. They must NOT be `async`.
```javascript
// CORRECT:
Cards.helpers({
board() {
return ReactiveCache.getBoard(this.boardId); // Promise on server, data on client
},
});
// WRONG:
Cards.helpers({
async board() { // Blaze gets Promise instead of data
return await ReactiveCache.getBoard(this.boardId);
},
});
```
**Server-side callers** of these helpers must `await` the result:
```javascript
// In a Meteor method or hook (server-only):
const board = await card.board();
```
---
## 4. Allow/Deny Callbacks Must Be Synchronous
Meteor 2.x evaluates allow/deny callbacks synchronously. An `async` callback returns a Promise:
- **allow** callback returning Promise (truthy) → always passes
- **deny** callback returning Promise (truthy) → always denies
**Rule:** Never use `async` in allow/deny. Replace `ReactiveCache` calls with direct sync Mongo calls.
```javascript
// CORRECT:
Cards.allow({
insert(userId, doc) {
return allowIsBoardMemberWithWriteAccess(userId, Boards.findOne(doc.boardId));
},
fetch: ['boardId'],
});
// WRONG:
Cards.allow({
async insert(userId, doc) {
return allowIsBoardMemberWithWriteAccess(userId, await ReactiveCache.getBoard(doc.boardId));
},
});
```
### Sync alternatives for common patterns:
| Async (broken in allow/deny) | Sync replacement |
|------------------------------|------------------|
| `await ReactiveCache.getBoard(id)` | `Boards.findOne(id)` |
| `await ReactiveCache.getCard(id)` | `Cards.findOne(id)` |
| `await ReactiveCache.getCurrentUser()` | `Meteor.users.findOne(userId)` |
| `await ReactiveCache.getBoards({...})` | `Boards.find({...}).fetch()` |
| `await card.board()` | `Boards.findOne(card.boardId)` |
**Note:** These sync Mongo calls (`findOne`, `find().fetch()`) are available in Meteor 2.x. In Meteor 3.0, they will be replaced by `findOneAsync` / `find().fetchAsync()`, which will require allow/deny callbacks to be reworked again (or replaced by Meteor 3.0's new permission model).
---
## 5. Server-Only Code CAN Be Async
Code that runs exclusively on the server can safely use `async`/`await`:
- `Meteor.methods({})` — method bodies
- `Meteor.publish()` — publication functions
- `JsonRoutes.add()` — REST API handlers
- `Collection.before.*` / `Collection.after.*` — collection hooks (via `matb33:collection-hooks`)
- Standalone server functions
```javascript
Meteor.methods({
async createCard(data) {
const board = await ReactiveCache.getBoard(data.boardId); // OK
// ...
},
});
```
---
## 6. forEach with await Anti-Pattern
`Array.forEach()` does not handle async callbacks — iterations run concurrently without awaiting.
```javascript
// WRONG:
items.forEach(async (item) => {
await processItem(item); // Runs all in parallel, not sequentially
});
// CORRECT:
for (const item of items) {
await processItem(item); // Runs sequentially
}
```
---
## 7. Client-Side Collection Updates
Meteor requires client-side collection updates to use `_id` as the selector:
```javascript
// CORRECT:
Lists.updateAsync(listId, { $set: { title: newTitle } });
// WRONG - fails with "Untrusted code may only update documents by ID":
Lists.updateAsync({ _id: listId, boardId: boardId }, { $set: { title: newTitle } });
```
---
## 8. Sync Meteor 2.x APIs to Convert for 3.0
These Meteor 2.x sync APIs will need conversion when upgrading to Meteor 3.0:
| Meteor 2.x (sync) | Meteor 3.0 (async) |
|--------------------|--------------------|
| `Collection.findOne()` | `Collection.findOneAsync()` |
| `Collection.find().fetch()` | `Collection.find().fetchAsync()` |
| `Collection.insert()` | `Collection.insertAsync()` |
| `Collection.update()` | `Collection.updateAsync()` |
| `Collection.remove()` | `Collection.removeAsync()` |
| `Collection.upsert()` | `Collection.upsertAsync()` |
| `Meteor.user()` | `Meteor.userAsync()` |
| `Meteor.userId()` | Remains sync |
**Current status:** Server-side code already uses async patterns via `ReactiveCache`. The sync `findOne()` calls in allow/deny callbacks will need to be addressed when Meteor 3.0's allow/deny system supports async (or is replaced).
---
## 9. Files Reference
Key files involved in the async migration:
| File | Role |
|------|------|
| `imports/reactiveCache.js` | ReactiveCache facade + Server/Client/Index implementations |
| `server/lib/utils.js` | Permission helper functions (`allowIsBoardMember*`) |
| `models/*.js` | Collection schemas, helpers, allow/deny, hooks, methods |
| `server/publications/*.js` | Meteor publications |
| `server/rulesHelper.js` | Rule trigger/action evaluation |
| `server/cronMigrationManager.js` | Cron-based migration jobs |
---
## 10. FullCalendar Versioning Note (Post-3.0 Follow-Up)
`wekan-fullcalendar` is currently migrated from legacy Meteor package globals to npm-based **FullCalendar 5.11.5** to keep Meteor 2.16 and 3.0 dual compatibility stable.
**Why pinned for now:**
- Avoids introducing additional breaking changes during core Meteor async migration.
- Keeps compatibility with current Blaze/jQuery-era integration points while removing `momentjs:moment` Meteor package dependency.
**After Meteor 3.0 lands (recommended follow-up):**
1. Re-evaluate upgrading FullCalendar to latest stable major.
2. Re-test plugin API differences (especially view names, callback signatures, locale/time formatting, CSS entry points).
3. Verify Node/runtime compatibility and bundle behavior under Meteor 3's final toolchain.
4. Keep migration isolated in a dedicated PR (separate from async data-layer work) to reduce rollback risk.

View file

@ -1,33 +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.
1. To send email, if possible, use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
2. Send info about security issue ONLY to security@wekan.fi . NOT TO ANYWHERE ELSE. NO CC, NO BCC.
3. Wait for new WeKan release that fixes security issue to appear to top of
https://github.com/wekan/wekan/blob/main/CHANGELOG.md
4. We will thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
5. All vulnerability details will be private to security@wekan.fi ,
unless you help all WeKan platforms to have a way to upgrade, like sending
database migrations code to security@wekan.fi or PRs to https://github.com/wekan/wekan/pulls .
There is no benefit to Wordwide Security Community to have more details about vulnerabilities,
if Worldwide Security Community does not help to make upgrades possible.
6. If there some day becomes available a way to upgrade all WeKan platforms,
this page will be updated to add permission for security researchers
to request new GHSA or CVE ID and publish your vulnerability details at your blog, talks, etc,
and send that info also to security@wekan.fi to be added to
Hall of Fame: https://wekan.fi/hall-of-fame/ to get Upgrade Bonus Point Stars.
In that case, it will become possible for security@wekan.fi to publish all
remaining private security details, and publicly thank Worldwide Security Community.
## 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?
@ -47,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?
@ -84,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
@ -285,4 +269,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.35.0"
appVersion: "v8.16.0"
files:
userUploads:
- README.md

View file

@ -9,13 +9,12 @@ if ('serviceWorker' in navigator) {
import '/client/lib/boardConverter';
import '/client/components/boardConversionProgress';
// Import migration manager and progress UI - COMMENTED OUT
// import '/client/lib/attachmentMigrationManager';
// import '/client/components/settings/migrationProgress';
// Import migration manager and progress UI
import '/client/lib/migrationManager';
import '/client/components/migrationProgress';
// Import cron settings - COMMENTED OUT
// import '/client/components/settings/cronSettings';
// Custom head tags
// 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
@ -63,21 +62,3 @@ Meteor.startup(() => {
}
});
});
// 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

@ -189,15 +189,14 @@ template(name="activity")
if(currentData.timeKey)
| {{_ activity.activityType }}
= ' '
i(title=currentData.timeValue).activity-meta {{ displayDate currentData.timeValue 'LLL' }}
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
if (currentData.timeOldValue)
= ' '
| {{{_ "previous_as" }}}
= ' '
i(title=currentData.timeOldValue).activity-meta {{ displayDate currentData.timeOldValue 'LLL' }}
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
= ' @'
else if(currentData.timeValue)
| {{_ activity.activityType currentData.timeValue}}
if($neq mode 'none')
div(title=activity.createdAt).activity-meta {{ displayDate activity.createdAt }}
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}

View file

@ -5,158 +5,164 @@ import { TAPi18n } from '/imports/i18n';
const activitiesPerPage = 500;
Template.activities.onCreated(function () {
// Register with sidebar so it can call loadNextPage on us
if (Sidebar) {
Sidebar.activitiesInstance = this;
}
BlazeComponent.extendComponent({
onCreated() {
// XXX Should we use ReactiveNumber?
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
// TODO is sidebar always available? E.g. on small screens/mobile devices
const sidebar = Sidebar;
sidebar && sidebar.callFirstWith(null, 'resetNextPeak');
this.autorun(() => {
let mode = this.data()?.mode;
if (mode) {
const capitalizedMode = Utils.capitalize(mode);
let searchId;
const showActivities = this.showActivities();
if (mode === 'linkedcard' || mode === 'linkedboard') {
const currentCard = Utils.getCurrentCard();
searchId = currentCard.linkedId;
mode = mode.replace('linked', '');
} else if (mode === 'card') {
searchId = Utils.getCurrentCardId();
} else {
searchId = Session.get(`current${capitalizedMode}`);
}
const limit = this.page.get() * activitiesPerPage;
if (searchId === null) return;
// XXX Should we use ReactiveNumber?
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
this.loadNextPage = () => {
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
this.loadNextPageLocked = false;
// TODO the guard can be removed as soon as the TODO above is resolved
if (!sidebar) return;
// If the sibear peak hasn't increased, that mean that there are no more
// activities, and we can stop calling new subscriptions.
// XXX This is hacky! We need to know excatly and reactively how many
// activities there are, we probably want to denormalize this number
// dirrectly into card and board documents.
const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
sidebar.calculateNextPeak();
const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
if (nextPeakBefore === nextPeakAfter) {
sidebar.callFirstWith(null, 'resetNextPeak');
}
});
}
});
},
loadNextPage() {
if (this.loadNextPageLocked === false) {
this.page.set(this.page.get() + 1);
this.loadNextPageLocked = true;
}
};
// TODO is sidebar always available? E.g. on small screens/mobile devices
const sidebar = Sidebar;
if (sidebar && sidebar.infiniteScrolling) {
sidebar.infiniteScrolling.resetNextPeak();
}
this.autorun(() => {
const data = Template.currentData();
let mode = data?.mode;
},
showActivities() {
let ret = false;
let mode = this.data()?.mode;
if (mode) {
const capitalizedMode = Utils.capitalize(mode);
let searchId;
const showActivities = _showActivities(data);
if (mode === 'linkedcard' || mode === 'linkedboard') {
const currentCard = Utils.getCurrentCard();
searchId = currentCard.linkedId;
mode = mode.replace('linked', '');
ret = currentCard.showActivities ?? false;
} else if (mode === 'card') {
searchId = Utils.getCurrentCardId();
ret = this.data()?.card?.showActivities ?? false;
} else {
searchId = Session.get(`current${capitalizedMode}`);
ret = Utils.getCurrentBoard().showActivities ?? false;
}
const limit = this.page.get() * activitiesPerPage;
if (searchId === null) return;
this.subscribe('activities', mode, searchId, limit, showActivities, () => {
this.loadNextPageLocked = false;
// TODO the guard can be removed as soon as the TODO above is resolved
if (!sidebar || !sidebar.infiniteScrolling) return;
// If the sidebar peak hasn't increased, that means that there are no more
// activities, and we can stop calling new subscriptions.
const nextPeakBefore = sidebar.infiniteScrolling.getNextPeak();
sidebar.calculateNextPeak();
const nextPeakAfter = sidebar.infiniteScrolling.getNextPeak();
if (nextPeakBefore === nextPeakAfter) {
sidebar.infiniteScrolling.resetNextPeak();
}
});
}
});
});
function _showActivities(data) {
let ret = false;
let mode = data?.mode;
if (mode) {
if (mode === 'linkedcard' || mode === 'linkedboard') {
const currentCard = Utils.getCurrentCard();
ret = currentCard.showActivities ?? false;
} else if (mode === 'card') {
ret = data?.card?.showActivities ?? false;
} else {
ret = Utils.getCurrentBoard().showActivities ?? false;
}
}
return ret;
}
Template.activities.helpers({
activities() {
return this.card.activities();
return ret;
},
});
activities() {
const ret = this.data().card.activities();
return ret;
},
}).register('activities');
Template.activity.helpers({
BlazeComponent.extendComponent({
checkItem() {
const checkItemId = this.activity.checklistItemId;
const checkItemId = this.currentData().activity.checklistItemId;
const checkItem = ReactiveCache.getChecklistItem(checkItemId);
return checkItem && checkItem.title;
},
boardLabelLink() {
const data = this.currentData();
const currentBoardId = Session.get('currentBoard');
if (this.mode !== 'board') {
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
if (data.mode !== 'board') {
// data.mode: card, linkedcard, linkedboard
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
}
else if (currentBoardId != this.activity.boardId) {
return createBoardLink(this.activity.board(), this.activity.listName ? this.activity.listName : null);
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createBoardLink(data.activity.board(), data.activity.listName ? data.activity.listName : null);
}
return TAPi18n.__('this-board');
},
cardLabelLink() {
const data = this.currentData();
const currentBoardId = Session.get('currentBoard');
if (this.mode == 'card') {
if (data.mode == 'card') {
// data.mode: card
return TAPi18n.__('this-card');
}
else if (this.mode !== 'board') {
return createCardLink(this.activity.card(), null);
else if (data.mode !== 'board') {
// data.mode: linkedcard, linkedboard
return createCardLink(data.activity.card(), null);
}
else if (currentBoardId != this.activity.boardId) {
return createCardLink(this.activity.card(), this.activity.board().title);
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createCardLink(data.activity.card(), data.activity.board().title);
}
return createCardLink(this.activity.card(), null);
return createCardLink(this.currentData().activity.card(), null);
},
cardLink() {
const data = this.currentData();
const currentBoardId = Session.get('currentBoard');
if (this.mode !== 'board') {
return createCardLink(this.activity.card(), null);
if (data.mode !== 'board') {
// data.mode: card, linkedcard, linkedboard
return createCardLink(data.activity.card(), null);
}
else if (currentBoardId != this.activity.boardId) {
return createCardLink(this.activity.card(), this.activity.board().title);
else if (currentBoardId != data.activity.boardId) {
// data.mode: board
// current activitie is linked
return createCardLink(data.activity.card(), data.activity.board().title);
}
return createCardLink(this.activity.card(), null);
return createCardLink(this.currentData().activity.card(), null);
},
receivedDate() {
const card = this.activity.card();
if (!card) return null;
return card.receivedAt;
const receivedDate = this.currentData().activity.card();
if (!receivedDate) return null;
return receivedDate.receivedAt;
},
startDate() {
const card = this.activity.card();
if (!card) return null;
return card.startAt;
const startDate = this.currentData().activity.card();
if (!startDate) return null;
return startDate.startAt;
},
dueDate() {
const card = this.activity.card();
if (!card) return null;
return card.dueAt;
const dueDate = this.currentData().activity.card();
if (!dueDate) return null;
return dueDate.dueAt;
},
endDate() {
const card = this.activity.card();
if (!card) return null;
return card.endAt;
const endDate = this.currentData().activity.card();
if (!endDate) return null;
return endDate.endAt;
},
lastLabel() {
const lastLabelId = this.activity.labelId;
const lastLabelId = this.currentData().activity.labelId;
if (!lastLabelId) return null;
const lastLabel = ReactiveCache.getBoard(
this.activity.boardId,
this.currentData().activity.boardId,
).getLabelById(lastLabelId);
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
return lastLabel.color;
@ -169,7 +175,7 @@ Template.activity.helpers({
lastCustomField() {
const lastCustomField = ReactiveCache.getCustomField(
this.activity.customFieldId,
this.currentData().activity.customFieldId,
);
if (!lastCustomField) return null;
return lastCustomField.name;
@ -177,10 +183,10 @@ Template.activity.helpers({
lastCustomFieldValue() {
const lastCustomField = ReactiveCache.getCustomField(
this.activity.customFieldId,
this.currentData().activity.customFieldId,
);
if (!lastCustomField) return null;
const value = this.activity.value;
const value = this.currentData().activity.value;
if (
lastCustomField.settings.dropdownItems &&
lastCustomField.settings.dropdownItems.length > 0
@ -197,13 +203,13 @@ Template.activity.helpers({
},
listLabel() {
const activity = this.activity;
const activity = this.currentData().activity;
const list = activity.list();
return (list && list.title) || activity.title;
},
sourceLink() {
const source = this.activity.source;
const source = this.currentData().activity.source;
if (source) {
if (source.url) {
return Blaze.toHTML(
@ -223,12 +229,12 @@ Template.activity.helpers({
memberLink() {
return Blaze.toHTMLWithData(Template.memberName, {
user: this.activity.member(),
user: this.currentData().activity.member(),
});
},
attachmentLink() {
const attachment = this.activity.attachment();
const attachment = this.currentData().activity.attachment();
// trying to display url before file is stored generates js errors
return (
(attachment &&
@ -242,16 +248,17 @@ Template.activity.helpers({
sanitizeText(attachment.name),
),
)) ||
sanitizeText(this.activity.attachmentName)
sanitizeText(this.currentData().activity.attachmentName)
);
},
customField() {
const customField = this.activity.customField();
const customField = this.currentData().activity.customField();
if (!customField) return null;
return customField.name;
},
});
}).register('activity');
Template.activity.helpers({
sanitize(value) {

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,14 +25,13 @@ 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
= text
+commentReactions(reactions=reactions commentId=_id)
span(title=createdAt).comment-meta {{ displayDate createdAt }}
span(title=createdAt).comment-meta {{ moment createdAt }}
if($eq currentUser._id userId)
+editOrDeleteComment
else if currentUser.isBoardAdmin
@ -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

@ -2,79 +2,93 @@ import { ReactiveCache } from '/imports/reactiveCache';
const commentFormIsOpen = new ReactiveVar(false);
Template.commentForm.onDestroyed(function () {
commentFormIsOpen.set(false);
$('.note-popover').hide();
});
BlazeComponent.extendComponent({
onDestroyed() {
commentFormIsOpen.set(false);
$('.note-popover').hide();
},
Template.commentForm.helpers({
commentFormIsOpen() {
return commentFormIsOpen.get();
},
});
Template.commentForm.events({
'submit .js-new-comment-form'(evt, tpl) {
const input = tpl.$('.js-new-comment-input');
const text = input.val().trim();
const card = Template.currentData();
let boardId = card.boardId;
let cardId = card._id;
if (card.isLinkedCard()) {
boardId = ReactiveCache.getCard(card.linkedId).boardId;
cardId = card.linkedId;
} else if (card.isLinkedBoard()) {
boardId = card.linkedId;
}
if (text) {
CardComments.insert({
text,
boardId,
cardId,
});
resetCommentInput(input);
Tracker.flush();
autosize.update(input);
input.trigger('submitted');
}
evt.preventDefault();
getInput() {
return this.$('.js-new-comment-input');
},
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
tpl.find('button[type=submit]').click();
}
},
});
Template.comments.helpers({
getComments() {
const data = Template.currentData();
if (!data || typeof data.comments !== 'function') return [];
return data.comments();
},
});
Template.comment.events({
'click .js-delete-comment': Popup.afterConfirm('deleteComment', function () {
const commentId = this._id;
CardComments.remove(commentId);
Popup.back();
}),
'submit .js-edit-comment'(evt, tpl) {
evt.preventDefault();
const textarea = tpl.find('.js-edit-comment textarea,input[type=text]');
const commentText = textarea && textarea.value ? textarea.value.trim() : '';
const commentId = this._id;
if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
events() {
return [
{
'submit .js-new-comment-form'(evt) {
const input = this.getInput();
const text = input.val().trim();
const card = this.currentData();
let boardId = card.boardId;
let cardId = card._id;
if (card.isLinkedCard()) {
boardId = ReactiveCache.getCard(card.linkedId).boardId;
cardId = card.linkedId;
} else if (card.isLinkedBoard()) {
boardId = card.linkedId;
}
if (text) {
CardComments.insert({
text,
boardId,
cardId,
});
resetCommentInput(input);
Tracker.flush();
autosize.update(input);
input.trigger('submitted');
}
evt.preventDefault();
},
});
}
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
this.find('button[type=submit]').click();
}
},
},
];
},
});
}).register('commentForm');
BlazeComponent.extendComponent({
getComments() {
const ret = this.data().comments();
return ret;
},
}).register("comments");
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-delete-comment': Popup.afterConfirm('deleteComment', () => {
const commentId = this.data()._id;
CardComments.remove(commentId);
Popup.back();
}),
'submit .js-edit-comment'(evt) {
evt.preventDefault();
const commentText = this.currentComponent()
.getValue()
.trim();
const commentId = this.data()._id;
if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
},
});
}
},
},
];
},
}).register("comment");
// XXX This should be a static method of the `commentForm` component
function resetCommentInput(input) {

View file

@ -3,7 +3,7 @@ template(name="boardConversionProgress")
.board-conversion-modal
.board-conversion-header
h3
i.fa.fa-cog
| ⚙️
| {{_ 'converting-board'}}
p {{_ 'converting-board-description'}}
@ -14,14 +14,14 @@ template(name="boardConversionProgress")
.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,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,13 +8,13 @@ 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
| {{_ 'restore-board'}}
= title
span {{ displayDate archivedAt 'LLL' }}
span {{ moment archivedAt 'LLL' }}
else
li.no-items-message {{_ 'no-archived-boards'}}

View file

@ -1,11 +1,10 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
Template.archivedBoards.onCreated(function () {
this.subscribe('archivedBoards');
});
BlazeComponent.extendComponent({
onCreated() {
this.subscribe('archivedBoards');
},
Template.archivedBoards.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
@ -19,34 +18,38 @@ Template.archivedBoards.helpers({
);
return ret;
},
});
Template.archivedBoards.events({
async 'click .js-restore-board'() {
// TODO : Make isSandstorm variable global
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await currentBoard.archive();
}
const board = this;
await board.restore();
Utils.goBoardId(board._id);
events() {
return [
{
'click .js-restore-board'() {
// TODO : Make isSandstorm variable global
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
currentBoard.archive();
}
const board = this.currentData();
board.restore();
Utils.goBoardId(board._id);
},
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
Popup.back();
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
Boards.remove(currentBoard._id);
}
Boards.remove(this._id);
FlowRouter.go('home');
}),
},
];
},
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
Popup.back();
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Utils.getCurrentBoardId()) {
const currentBoard = Utils.getCurrentBoard();
await Boards.removeAsync(currentBoard._id);
}
await Boards.removeAsync(this._id);
FlowRouter.go('home');
}),
});
}).register('archivedBoards');

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,35 +263,6 @@ 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;

View file

@ -1,8 +1,10 @@
template(name="board")
if isConverting
if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress
else if isBoardReady
else if isBoardReady.get
if currentBoard
if onlyShowCurrentCard
+cardDetails(currentCard)
@ -22,16 +24,16 @@ 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}}"
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
class="{{#if draggingActive}}is-dragging-active{{/if}}"
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
class="{{#if isMiniScreen}}mobile-view{{/if}}")
if showOverlay
if showOverlay.get
.board-overlay
if currentBoard.isTemplatesBoard
each currentBoard.swimlanes
@ -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")

File diff suppressed because it is too large Load diff

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

@ -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,47 +189,40 @@ 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
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen
if visibilityMenuIsOpen.get
+boardVisibilityList
else
p.quiet
if $eq visibility 'public'
span.fa.fa-globe.colorful
if $eq visibility.get 'public'
span 🌐
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
@ -262,75 +242,16 @@ template(name="createBoardPopup")
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen
if visibilityMenuIsOpen.get
+boardVisibilityList
else
p.quiet
if $eq visibility 'public'
span.fa.fa-globe.colorful
if $eq visibility.get 'public'
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'}}
template(name="headerBarCreateBoardPopup")
form
label
| {{_ 'title'}}
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
if visibilityMenuIsOpen
+boardVisibilityList
else
p.quiet
if $eq visibility 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ '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
+boardVisibilityList
else
p.quiet
if $eq visibility 'public'
span.fa.fa-globe.colorful
= " "
| {{{_ 'board-public-info'}}}
else
span.fa.fa-lock.colorful
span 🔒
= " "
| {{{_ 'board-private-info'}}}
a.js-change-visibility {{_ 'change'}}.
@ -346,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
@ -389,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,23 +19,22 @@ Template.boardChangeTitlePopup.events({
.val()
.trim();
if (newTitle) {
const board = Utils.getCurrentBoard();
if (board) {
await board.rename(newTitle);
await board.setDescription(newDesc);
}
this.rename(newTitle);
this.setDescription(newDesc);
Popup.back();
}
event.preventDefault();
},
});
Template.boardHeaderBar.helpers({
BlazeComponent.extendComponent({
watchLevel() {
const currentBoard = Utils.getCurrentBoard();
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
},
isStarred() {
const boardId = Session.get('currentBoard');
const user = ReactiveCache.getCurrentUser();
@ -48,7 +46,127 @@ Template.boardHeaderBar.helpers({
const currentBoard = Utils.getCurrentBoard();
return currentBoard && currentBoard.stars >= 2;
},
/*
showSort() {
return ReactiveCache.getCurrentUser().hasSortBy();
},
directionClass() {
return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
},
changeDirection() {
const direction = 0 - this.currentDirection() === -1 ? '-' : '';
Meteor.call('setListSortBy', direction + this.currentListSortBy());
},
currentDirection() {
return ReactiveCache.getCurrentUser().getListSortByDirection();
},
currentListSortBy() {
return ReactiveCache.getCurrentUser().getListSortBy();
},
listSortShortDesc() {
return `list-label-short-${this.currentListSortBy()}`;
},
*/
events() {
return [
{
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() {
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
if (process.env.DEBUG === 'true') {
console.log('Hamburger menu clicked');
}
// Use the same approach as keyboard shortcuts
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Sidebar.toggle()');
}
Sidebar.toggle();
} else {
if (process.env.DEBUG === 'true') {
console.warn('Sidebar not available, trying alternative approach');
}
// Try to trigger the sidebar through the global Blaze helper
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
const sidebar = Blaze._globalHelpers.Sidebar();
if (sidebar && typeof sidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Blaze helper Sidebar.toggle()');
}
sidebar.toggle();
}
}
}
},
'click .js-open-filter-view'() {
if (Sidebar) {
Sidebar.setView('filter');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-sort-cards': Popup.open('cardsSort'),
/*
'click .js-open-sort-view'(evt) {
const target = evt.target;
if (target.tagName === 'I') {
// click on the text, popup choices
this.changeDirection();
} else {
// change the sort order
Popup.open('listsort')(evt);
}
},
*/
'click .js-filter-reset'(event) {
event.stopPropagation();
if (Sidebar) {
Sidebar.setView();
} else {
console.warn('Sidebar not available for setView');
}
Filter.reset();
},
'click .js-sort-reset'() {
Session.set('sortBy', '');
},
'click .js-open-search-view'() {
if (Sidebar) {
Sidebar.setView('search');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId();
MultiSelection.activate();
if (currentCard) {
MultiSelection.add(currentCard);
}
},
'click .js-multiselection-reset'(event) {
event.stopPropagation();
MultiSelection.disable();
},
'click .js-log-in'() {
FlowRouter.go('atSignIn');
},
},
];
},
}).register('boardHeaderBar');
Template.boardHeaderBar.helpers({
boardView() {
return Utils.boardView();
},
@ -74,102 +192,6 @@ Template.boardHeaderBar.helpers({
},
});
Template.boardHeaderBar.events({
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board'() {
const boardId = Session.get('currentBoard');
if (boardId) {
Meteor.call('toggleBoardStar', boardId);
}
},
'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-watch-board': Popup.open('boardChangeWatch'),
'click .js-open-archived-board'() {
Modal.open('archivedBoards');
},
'click .js-toggle-board-view': Popup.open('boardChangeView'),
'click .js-toggle-sidebar'() {
if (process.env.DEBUG === 'true') {
console.log('Hamburger menu clicked');
}
// Use the same approach as keyboard shortcuts
if (typeof Sidebar !== 'undefined' && Sidebar && typeof Sidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Sidebar.toggle()');
}
Sidebar.toggle();
} else {
if (process.env.DEBUG === 'true') {
console.warn('Sidebar not available, trying alternative approach');
}
// Try to trigger the sidebar through the global Blaze helper
if (typeof Blaze !== 'undefined' && Blaze._globalHelpers && Blaze._globalHelpers.Sidebar) {
const sidebar = Blaze._globalHelpers.Sidebar();
if (sidebar && typeof sidebar.toggle === 'function') {
if (process.env.DEBUG === 'true') {
console.log('Using Blaze helper Sidebar.toggle()');
}
sidebar.toggle();
}
}
}
},
'click .js-open-filter-view'() {
if (Sidebar) {
Sidebar.setView('filter');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-sort-cards': Popup.open('cardsSort'),
/*
'click .js-open-sort-view'(evt) {
const target = evt.target;
if (target.tagName === 'I') {
// click on the text, popup choices
this.changeDirection();
} else {
// change the sort order
Popup.open('listsort')(evt);
}
},
*/
'click .js-filter-reset'(event) {
event.stopPropagation();
if (Sidebar) {
Sidebar.setView();
} else {
console.warn('Sidebar not available for setView');
}
Filter.reset();
},
'click .js-sort-reset'() {
Session.set('sortBy', '');
},
'click .js-open-search-view'() {
if (Sidebar) {
Sidebar.setView('search');
} else {
console.warn('Sidebar not available for setView');
}
},
'click .js-multiselection-activate'() {
const currentCard = Utils.getCurrentCardId();
MultiSelection.activate();
if (currentCard) {
MultiSelection.add(currentCard);
}
},
'click .js-multiselection-reset'(event) {
event.stopPropagation();
MultiSelection.disable();
},
'click .js-log-in'() {
FlowRouter.go('atSignIn');
},
});
Template.boardChangeViewPopup.events({
'click .js-open-lists-view'() {
Utils.setBoardView('board-view-lists');
@ -183,263 +205,196 @@ 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({
template() {
return 'createBoard';
},
});
// Shared setup for all create board popups
function setupCreateBoardState(tpl) {
tpl.visibilityMenuIsOpen = new ReactiveVar(false);
tpl.visibility = new ReactiveVar('private');
tpl.boardId = new ReactiveVar('');
Meteor.subscribe('tableVisibilityModeSettings');
}
function createBoardHelpers() {
return {
visibilityMenuIsOpen() {
return Template.instance().visibilityMenuIsOpen.get();
},
visibility() {
return Template.instance().visibility.get();
},
notAllowPrivateVisibilityOnly() {
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
},
visibilityCheck() {
return Template.currentData() === Template.instance().visibility.get();
},
};
}
function createBoardSubmit(tpl, event) {
event.preventDefault();
const title = tpl.find('.js-new-board-title').value;
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
if (addTemplateContainer) {
//const templateContainerId = Meteor.call('setCreateTemplateContainer');
//Utils.goBoardId(templateContainerId);
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
tpl.boardId.set(
Boards.insert({
// title: TAPi18n.__('templates'),
title: title,
permission: 'private',
type: 'template-container',
migrationVersion: 1, // Latest version - no migration needed
}),
);
// Insert the card templates swimlane
Swimlanes.insert({
// title: TAPi18n.__('card-templates-swimlane'),
title: 'Card Templates',
boardId: tpl.boardId.get(),
sort: 1,
type: 'template-container',
}),
// Insert the list templates swimlane
Swimlanes.insert(
{
// title: TAPi18n.__('list-templates-swimlane'),
title: 'List Templates',
boardId: tpl.boardId.get(),
sort: 2,
type: 'template-container',
},
);
// Insert the board templates swimlane
Swimlanes.insert(
{
//title: TAPi18n.__('board-templates-swimlane'),
title: 'Board Templates',
boardId: tpl.boardId.get(),
sort: 3,
type: 'template-container',
},
);
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(tpl.boardId.get());
} else {
const visibility = tpl.visibility.get();
tpl.boardId.set(
Boards.insert({
title,
permission: visibility,
migrationVersion: 1, // Latest version - no migration needed
}),
);
Swimlanes.insert({
title: 'Default',
boardId: tpl.boardId.get(),
});
// Assign to space if one was selected
const spaceId = Session.get('createBoardInWorkspace');
if (spaceId) {
Meteor.call('assignBoardToWorkspace', tpl.boardId.get(), spaceId, (err) => {
if (err) console.error('Error assigning board to space:', err);
});
Session.set('createBoardInWorkspace', null); // Clear after use
}
Utils.goBoardId(tpl.boardId.get());
}
}
function createBoardEvents() {
return {
'click .js-select-visibility'(event, tpl) {
tpl.visibility.set(Template.currentData());
tpl.visibilityMenuIsOpen.set(false);
},
'click .js-change-visibility'(event, tpl) {
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
},
'click .js-import': Popup.open('boardImportBoard'),
'submit'(event, tpl) {
createBoardSubmit(tpl, event);
},
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-board-template': Popup.open('searchElement'),
'click .js-toggle-add-template-container'() {
$('#add-template-container').toggleClass('is-checked');
},
};
}
// createBoard (non-popup version)
Template.createBoard.onCreated(function () {
setupCreateBoardState(this);
});
Template.createBoard.helpers(createBoardHelpers());
Template.createBoard.events(createBoardEvents());
// createBoardPopup
Template.createBoardPopup.onCreated(function () {
setupCreateBoardState(this);
});
Template.createBoardPopup.helpers(createBoardHelpers());
Template.createBoardPopup.events(createBoardEvents());
// createTemplateContainerPopup
Template.createTemplateContainerPopup.onCreated(function () {
setupCreateBoardState(this);
});
Template.createTemplateContainerPopup.onRendered(function () {
// Always pre-check the template container checkbox for this popup
$('#add-template-container').addClass('is-checked');
});
Template.createTemplateContainerPopup.helpers(createBoardHelpers());
Template.createTemplateContainerPopup.events(createBoardEvents());
// headerBarCreateBoardPopup
Template.headerBarCreateBoardPopup.onCreated(function () {
setupCreateBoardState(this);
});
Template.headerBarCreateBoardPopup.helpers(createBoardHelpers());
Template.headerBarCreateBoardPopup.events({
'click .js-select-visibility'(event, tpl) {
tpl.visibility.set(Template.currentData());
tpl.visibilityMenuIsOpen.set(false);
onCreated() {
this.visibilityMenuIsOpen = new ReactiveVar(false);
this.visibility = new ReactiveVar('private');
this.boardId = new ReactiveVar('');
Meteor.subscribe('tableVisibilityModeSettings');
},
'click .js-change-visibility'(event, tpl) {
tpl.visibilityMenuIsOpen.set(!tpl.visibilityMenuIsOpen.get());
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
},
'click .js-import': Popup.open('boardImportBoard'),
async submit(event, tpl) {
createBoardSubmit(tpl, event);
// Immediately star boards created with the headerbar popup.
await ReactiveCache.getCurrentUser().toggleBoardStar(tpl.boardId.get());
visibilityCheck() {
return this.currentData() === this.visibility.get();
},
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-board-template': Popup.open('searchElement'),
'click .js-toggle-add-template-container'() {
setVisibility(visibility) {
this.visibility.set(visibility);
this.visibilityMenuIsOpen.set(false);
},
toggleVisibilityMenu() {
this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
},
toggleAddTemplateContainer() {
$('#add-template-container').toggleClass('is-checked');
},
});
Template.boardChangeVisibilityPopup.onCreated(function () {
Meteor.subscribe('tableVisibilityModeSettings');
});
onSubmit(event) {
event.preventDefault();
const title = this.find('.js-new-board-title').value;
Template.boardChangeVisibilityPopup.helpers({
const addTemplateContainer = $('#add-template-container.is-checked').length > 0;
if (addTemplateContainer) {
//const templateContainerId = Meteor.call('setCreateTemplateContainer');
//Utils.goBoardId(templateContainerId);
//alert('niinku template ' + Meteor.call('setCreateTemplateContainer'));
this.boardId.set(
Boards.insert({
// title: TAPi18n.__('templates'),
title: title,
permission: 'private',
type: 'template-container',
migrationVersion: 1, // Latest version - no migration needed
}),
);
// Insert the card templates swimlane
Swimlanes.insert({
// title: TAPi18n.__('card-templates-swimlane'),
title: 'Card Templates',
boardId: this.boardId.get(),
sort: 1,
type: 'template-container',
}),
// Insert the list templates swimlane
Swimlanes.insert(
{
// title: TAPi18n.__('list-templates-swimlane'),
title: 'List Templates',
boardId: this.boardId.get(),
sort: 2,
type: 'template-container',
},
);
// Insert the board templates swimlane
Swimlanes.insert(
{
//title: TAPi18n.__('board-templates-swimlane'),
title: 'Board Templates',
boardId: this.boardId.get(),
sort: 3,
type: 'template-container',
},
);
Utils.goBoardId(this.boardId.get());
} else {
const visibility = this.visibility.get();
this.boardId.set(
Boards.insert({
title,
permission: visibility,
migrationVersion: 1, // Latest version - no migration needed
}),
);
Swimlanes.insert({
title: 'Default',
boardId: this.boardId.get(),
});
Utils.goBoardId(this.boardId.get());
}
},
events() {
return [
{
'click .js-select-visibility'() {
this.setVisibility(this.currentData());
},
'click .js-change-visibility': this.toggleVisibilityMenu,
'click .js-import': Popup.open('boardImportBoard'),
submit: this.onSubmit,
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-board-template': Popup.open('searchElement'),
'click .js-toggle-add-template-container': this.toggleAddTemplateContainer,
},
];
},
}).register('createBoardPopup');
(class HeaderBarCreateBoard extends CreateBoard {
onSubmit(event) {
super.onSubmit(event);
// Immediately star boards crated with the headerbar popup.
ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
}
}.register('headerBarCreateBoardPopup'));
BlazeComponent.extendComponent({
notAllowPrivateVisibilityOnly(){
return !TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
},
visibilityCheck() {
const currentBoard = Utils.getCurrentBoard();
return this === currentBoard.permission;
return this.currentData() === currentBoard.permission;
},
});
Template.boardChangeVisibilityPopup.events({
'click .js-select-visibility'() {
selectBoardVisibility() {
const currentBoard = Utils.getCurrentBoard();
const visibility = String(this);
const visibility = this.currentData();
currentBoard.setVisibility(visibility);
Popup.back();
},
});
Template.boardChangeWatchPopup.helpers({
events() {
return [
{
'click .js-select-visibility': this.selectBoardVisibility,
},
];
},
}).register('boardChangeVisibilityPopup');
BlazeComponent.extendComponent({
watchLevel() {
const currentBoard = Utils.getCurrentBoard();
return currentBoard.getWatchLevel(Meteor.userId());
},
watchCheck() {
const currentBoard = Utils.getCurrentBoard();
return this === currentBoard.getWatchLevel(Meteor.userId());
return this.currentData() === this.watchLevel();
},
});
Template.boardChangeWatchPopup.events({
'click .js-select-watch'() {
const level = String(this);
Meteor.call(
'watch',
'board',
Session.get('currentBoard'),
level,
(err, ret) => {
if (!err && ret) Popup.back();
events() {
return [
{
'click .js-select-watch'() {
const level = this.currentData();
Meteor.call(
'watch',
'board',
Session.get('currentBoard'),
level,
(err, ret) => {
if (!err && ret) Popup.back();
},
);
},
},
);
];
},
});
}).register('boardChangeWatchPopup');
/*
// BlazeComponent.extendComponent was removed - this code is unused.
// Original listsortPopup component:
// {
BlazeComponent.extendComponent({
onCreated() {
//this.sortBy = new ReactiveVar();
////this.sortDirection = new ReactiveVar();
@ -507,40 +462,46 @@ Template.boardChangeWatchPopup.events({
},
];
},
// }.register('listsortPopup');
}).register('listsortPopup');
*/
Template.cardsSortPopup.events({
'click .js-sort-due'() {
const sortBy = {
dueAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('due-date'));
Popup.back();
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-sort-due'() {
const sortBy = {
dueAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('due-date'));
Popup.back();
},
'click .js-sort-title'() {
const sortBy = {
title: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('title'));
Popup.back();
},
'click .js-sort-created-asc'() {
const sortBy = {
createdAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
Popup.back();
},
'click .js-sort-created-desc'() {
const sortBy = {
createdAt: -1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
Popup.back();
},
},
];
},
'click .js-sort-title'() {
const sortBy = {
title: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('title'));
Popup.back();
},
'click .js-sort-created-asc'() {
const sortBy = {
createdAt: 1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-newest-first'));
Popup.back();
},
'click .js-sort-created-desc'() {
const sortBy = {
createdAt: -1,
};
Session.set('sortBy', sortBy);
sortCardsBy.set(TAPi18n.__('date-created-oldest-first'));
Popup.back();
},
});
}).register('cardsSortPopup');

View file

@ -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'}}

File diff suppressed because it is too large Load diff

View file

@ -1,43 +0,0 @@
.calendar-view .fc {
--fc-button-text-color: #333;
--fc-button-bg-color: #f5f5f5;
--fc-button-border-color: rgba(0, 0, 0, 0.2);
--fc-button-hover-bg-color: #e6e6e6;
--fc-button-hover-border-color: rgba(0, 0, 0, 0.25);
--fc-button-active-bg-color: #d9d9d9;
--fc-button-active-border-color: rgba(0, 0, 0, 0.3);
}
.calendar-view .fc .fc-button-primary {
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
background-image: linear-gradient(to bottom, #fff 0%, #e6e6e6 100%);
}
.calendar-view .fc .fc-button-primary:focus,
.calendar-view .fc .fc-button-primary:not(:disabled).fc-button-active,
.calendar-view .fc .fc-button-primary:not(:disabled):active {
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),
0 1px 2px rgba(0, 0, 0, 0.05);
}
.calendar-create-close {
min-height: auto !important;
min-width: auto !important;
width: 32px;
height: 32px;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
color: #666 !important;
opacity: 0.8;
}
.calendar-create-close:hover,
.calendar-create-close:focus {
background: transparent !important;
color: #111 !important;
opacity: 1;
}

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

@ -0,0 +1,82 @@
<template name="originalPositionsView">
<div class="original-positions-view">
<div class="original-positions-header">
<button class="btn btn-sm btn-outline-secondary" onclick="{{toggleOriginalPositions}}">
<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
</button>
{{/if}}
</div>
{{#if isShowingOriginalPositions}}
<div class="original-positions-content">
{{#if isLoading}}
<div class="original-positions-loading">
<i class="fa fa-spinner fa-spin"></i> Loading original positions...
</div>
{{else}}
<div class="original-positions-filters">
<div class="btn-group btn-group-sm" role="group">
<button type="button"
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'all'}}">
All
</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"
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"
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'card'}}">
<i class="fa fa-sticky-note"></i> Cards
</button>
</div>
</div>
<div class="original-positions-list">
{{#each getFilteredHistory}}
<div class="original-position-item">
<div class="original-position-item-header">
<i class="fa {{getEntityTypeIcon entityType}}"></i>
<span class="entity-type">{{getEntityTypeLabel entityType}}</span>
<span class="entity-name">{{getEntityDisplayName this}}</span>
<span class="entity-id">({{entityId}})</span>
</div>
<div class="original-position-item-details">
<div class="original-position-description">
{{getEntityOriginalPositionDescription this}}
</div>
{{#if originalTitle}}
<div class="original-title">
<strong>Original title:</strong> {{originalTitle}}
</div>
{{/if}}
<div class="original-position-date">
<small class="text-muted">Created: {{formatDate createdAt}}</small>
</div>
</div>
</div>
{{else}}
<div class="no-original-positions">
<i class="fa fa-info-circle"></i>
No original position data available for this board.
</div>
{{/each}}
</div>
{{/if}}
</div>
{{/if}}
</div>
</template>

View file

@ -1,57 +0,0 @@
template(name="originalPositionsView")
.original-positions-view
.original-positions-header
button.btn.btn-sm.btn-outline-secondary.js-toggle-original-positions
i.fa.fa-history
if isShowingOriginalPositions
| Hide Original Positions
else
| Show Original Positions
if isShowingOriginalPositions
button.btn.btn-sm.btn-outline-primary.js-refresh-history
i.fa.fa-refresh
| Refresh
if isShowingOriginalPositions
.original-positions-content
if isLoading
.original-positions-loading
i.fa.fa-spinner.fa-spin
| Loading original positions...
else
.original-positions-filters
.btn-group.btn-group-sm(role="group")
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="all")
| All
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="swimlane")
i.fa.fa-bars
| Swimlanes
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="list")
i.fa.fa-columns
| Lists
button.btn.js-filter-type(type="button" class="{{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}" data-filter-type="card")
i.fa.fa-sticky-note
| Cards
.original-positions-list
each getFilteredHistory
.original-position-item
.original-position-item-header
i.fa(class="{{getEntityTypeIcon entityType}}")
span.entity-type {{getEntityTypeLabel entityType}}
span.entity-name {{getEntityDisplayName this}}
span.entity-id ({{entityId}})
.original-position-item-details
.original-position-description
| {{getEntityOriginalPositionDescription this}}
if originalTitle
.original-title
strong Original title:
| {{originalTitle}}
.original-position-date
small.text-muted Created: {{formatDate createdAt}}
else
.no-original-positions
i.fa.fa-info-circle
| No original position data available for this board.

View file

@ -1,73 +1,94 @@
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import './originalPositionsView.html';
/**
* Component to display original positions for all entities on a board
*/
class OriginalPositionsViewComponent extends BlazeComponent {
onCreated() {
super.onCreated();
this.showOriginalPositions = new ReactiveVar(false);
this.boardHistory = new ReactiveVar([]);
this.isLoading = new ReactiveVar(false);
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
}
Template.originalPositionsView.onCreated(function () {
this.showOriginalPositions = new ReactiveVar(false);
this.boardHistory = new ReactiveVar([]);
this.isLoading = new ReactiveVar(false);
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
onRendered() {
super.onRendered();
this.loadBoardHistory();
}
const tpl = this;
this.loadBoardHistory = function () {
loadBoardHistory() {
const boardId = Session.get('currentBoard');
if (!boardId) return;
tpl.isLoading.set(true);
this.isLoading.set(true);
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
tpl.isLoading.set(false);
this.isLoading.set(false);
if (error) {
console.error('Error loading board history:', error);
tpl.boardHistory.set([]);
this.boardHistory.set([]);
} else {
tpl.boardHistory.set(result);
this.boardHistory.set(result);
}
});
};
});
}
Template.originalPositionsView.onRendered(function () {
this.loadBoardHistory();
});
toggleOriginalPositions() {
this.showOriginalPositions.set(!this.showOriginalPositions.get());
}
Template.originalPositionsView.helpers({
isShowingOriginalPositions() {
return Template.instance().showOriginalPositions.get();
},
return this.showOriginalPositions.get();
}
isLoading() {
return Template.instance().isLoading.get();
},
return this.isLoading.get();
}
getBoardHistory() {
return Template.instance().boardHistory.get();
},
return this.boardHistory.get();
}
getFilteredHistory() {
const tpl = Template.instance();
const history = tpl.boardHistory.get();
const filterType = tpl.filterType.get();
const history = this.getBoardHistory();
const filterType = this.filterType.get();
if (filterType === 'all') {
return history;
}
return history.filter(item => item.entityType === filterType);
},
}
isFilterType(type) {
return Template.instance().filterType.get() === type;
},
getSwimlanesHistory() {
return this.getBoardHistory().filter(item => item.entityType === 'swimlane');
}
getListsHistory() {
return this.getBoardHistory().filter(item => item.entityType === 'list');
}
getCardsHistory() {
return this.getBoardHistory().filter(item => item.entityType === 'card');
}
setFilterType(type) {
this.filterType.set(type);
}
getFilterType() {
return this.filterType.get();
}
getEntityDisplayName(entity) {
const position = entity.originalPosition || {};
return position.title || `Entity ${entity.entityId}`;
},
}
getEntityOriginalPositionDescription(entity) {
const position = entity.originalPosition || {};
@ -85,7 +106,7 @@ Template.originalPositionsView.helpers({
}
return description;
},
}
getEntityTypeIcon(entityType) {
switch (entityType) {
@ -98,7 +119,7 @@ Template.originalPositionsView.helpers({
default:
return 'fa-question';
}
},
}
getEntityTypeLabel(entityType) {
switch (entityType) {
@ -111,24 +132,17 @@ Template.originalPositionsView.helpers({
default:
return 'Unknown';
}
},
}
formatDate(date) {
return new Date(date).toLocaleString();
},
});
}
Template.originalPositionsView.events({
'click .js-toggle-original-positions'(evt, tpl) {
tpl.showOriginalPositions.set(!tpl.showOriginalPositions.get());
},
refreshHistory() {
this.loadBoardHistory();
}
}
'click .js-refresh-history'(evt, tpl) {
tpl.loadBoardHistory();
},
OriginalPositionsViewComponent.register('originalPositionsView');
'click .js-filter-type'(evt, tpl) {
const type = evt.currentTarget.dataset.filterType;
tpl.filterType.set(type);
},
});
export default OriginalPositionsViewComponent;

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

@ -495,9 +495,9 @@ Template.previewClipboardImagePopup.events({
},
});
Template.attachmentActionsPopup.helpers({
BlazeComponent.extendComponent({
isCover() {
const ret = ReactiveCache.getCard(this.meta.cardId).coverId == this._id;
const ret = ReactiveCache.getCard(this.data().meta.cardId).coverId == this.data()._id;
return ret;
},
isBackgroundImage() {
@ -505,72 +505,78 @@ Template.attachmentActionsPopup.helpers({
//return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src");
return false;
},
});
events() {
return [
{
'click .js-add-cover'() {
ReactiveCache.getCard(this.data().meta.cardId).setCover(this.data()._id);
Popup.back();
},
'click .js-remove-cover'() {
ReactiveCache.getCard(this.data().meta.cardId).unsetCover();
Popup.back();
},
'click .js-add-background-image'() {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL(attachmentActionsLink);
Utils.setBackgroundImage(attachmentActionsLink);
Popup.back();
event.preventDefault();
},
'click .js-remove-background-image'() {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL("");
Utils.setBackgroundImage("");
Popup.back();
Utils.reload();
event.preventDefault();
},
'click .js-move-storage-fs'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "fs");
Popup.back();
},
'click .js-move-storage-gridfs'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs");
Popup.back();
},
'click .js-move-storage-s3'() {
Meteor.call('moveAttachmentToStorage', this.data()._id, "s3");
Popup.back();
},
}
]
}
}).register('attachmentActionsPopup');
Template.attachmentActionsPopup.events({
'click .js-add-cover'() {
ReactiveCache.getCard(this.meta.cardId).setCover(this._id);
Popup.back();
},
'click .js-remove-cover'() {
ReactiveCache.getCard(this.meta.cardId).unsetCover();
Popup.back();
},
'click .js-add-background-image'(event) {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL(attachmentActionsLink);
Utils.setBackgroundImage(attachmentActionsLink);
Popup.back();
event.preventDefault();
},
'click .js-remove-background-image'(event) {
const currentBoard = Utils.getCurrentBoard();
currentBoard.setBackgroundImageURL("");
Utils.setBackgroundImage("");
Popup.back();
Utils.reload();
event.preventDefault();
},
'click .js-move-storage-fs'() {
Meteor.call('moveAttachmentToStorage', this._id, "fs");
Popup.back();
},
'click .js-move-storage-gridfs'() {
Meteor.call('moveAttachmentToStorage', this._id, "gridfs");
Popup.back();
},
'click .js-move-storage-s3'() {
Meteor.call('moveAttachmentToStorage', this._id, "s3");
Popup.back();
},
});
Template.attachmentRenamePopup.helpers({
BlazeComponent.extendComponent({
getNameWithoutExtension() {
const ret = this.name.replace(new RegExp("\." + this.extension + "$"), "");
const ret = this.data().name.replace(new RegExp("\." + this.data().extension + "$"), "");
return ret;
},
});
Template.attachmentRenamePopup.events({
'keydown input.js-edit-attachment-name'(evt, tpl) {
// enter = save
if (evt.keyCode === 13) {
tpl.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-attachment-name'(event, tpl) {
// save button pressed
event.preventDefault();
const name = tpl.$('.js-edit-attachment-name')[0]
.value
.trim() + this.extensionWithDot;
if (name === sanitizeText(name)) {
Meteor.call('renameAttachment', this._id, name);
}
Popup.back();
},
});
events() {
return [
{
'keydown input.js-edit-attachment-name'(evt) {
// enter = save
if (evt.keyCode === 13) {
this.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-attachment-name'(event) {
// save button pressed
event.preventDefault();
const name = this.$('.js-edit-attachment-name')[0]
.value
.trim() + this.data().extensionWithDot;
if (name === sanitizeText(name)) {
Meteor.call('renameAttachment', this.data()._id, name);
}
Popup.back();
},
}
]
}
}).register('attachmentRenamePopup');
// Template helpers for attachment migration status
Template.registerHelper('attachmentMigrationStatus', function(attachmentId) {

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
@ -99,21 +98,6 @@ template(name="cardCustomField-date")
b
| {{showWeek}}
template(name="cardCustomField-datePopup")
.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'}}
template(name="cardCustomField-dropdown")
if canModifyCard
+inlinedForm(classNames="js-card-customfield-dropdown")

View file

@ -1,10 +1,5 @@
import { TAPi18n } from '/imports/i18n';
import {
setupDatePicker,
datePickerRendered,
datePickerHelpers,
datePickerEvents,
} from '/client/lib/datepicker';
import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
formatDateTime,
@ -27,13 +22,12 @@ import {
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import { CustomFieldStringTemplate } from '/client/lib/customFields'
import { getCurrentCardFromContext } from '/client/lib/currentCard';
Template.cardCustomFieldsPopup.helpers({
hasCustomField() {
const card = getCurrentCardFromContext();
if (!card) return false;
const card = Utils.getCurrentCard();
const customFieldId = this._id;
return card.customFieldIndex(customFieldId) > -1;
},
@ -41,8 +35,7 @@ Template.cardCustomFieldsPopup.helpers({
Template.cardCustomFieldsPopup.events({
'click .js-select-field'(event) {
const card = getCurrentCardFromContext();
if (!card) return;
const card = Utils.getCurrentCard();
const customFieldId = this._id;
card.toggleCustomField(customFieldId);
event.preventDefault();
@ -55,280 +48,304 @@ Template.cardCustomFieldsPopup.events({
});
// cardCustomField
Template.cardCustomField.helpers({
const CardCustomField = BlazeComponent.extendComponent({
getTemplate() {
return `cardCustomField-${this.definition.type}`;
return `cardCustomField-${this.data().definition.type}`;
},
onCreated() {
const self = this;
self.card = Utils.getCurrentCard();
self.customFieldId = this.data()._id;
},
});
Template.cardCustomField.onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
CardCustomField.register('cardCustomField');
// cardCustomField-text
Template['cardCustomField-text'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
(class extends CardCustomField {
onCreated() {
super.onCreated();
}
Template['cardCustomField-text'].events({
'submit .js-card-customfield-text'(event, tpl) {
event.preventDefault();
const value = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
events() {
return [
{
'submit .js-card-customfield-text'(event) {
event.preventDefault();
const value = this.currentComponent().getValue();
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-text'));
// cardCustomField-number
Template['cardCustomField-number'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
(class extends CardCustomField {
onCreated() {
super.onCreated();
}
Template['cardCustomField-number'].events({
'submit .js-card-customfield-number'(event, tpl) {
event.preventDefault();
const value = parseInt(tpl.find('input').value, 10);
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
events() {
return [
{
'submit .js-card-customfield-number'(event) {
event.preventDefault();
const value = parseInt(this.find('input').value, 10);
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-number'));
// cardCustomField-checkbox
Template['cardCustomField-checkbox'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
});
(class extends CardCustomField {
onCreated() {
super.onCreated();
}
Template['cardCustomField-checkbox'].events({
'click .js-checklist-item .check-box-unicode'(event, tpl) {
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
},
'click .js-checklist-item .check-box-container'(event, tpl) {
tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value);
},
});
toggleItem() {
this.card.setCustomField(this.customFieldId, !this.data().value);
}
events() {
return [
{
'click .js-checklist-item .check-box-container': this.toggleItem,
},
];
}
}.register('cardCustomField-checkbox'));
// cardCustomField-currency
Template['cardCustomField-currency'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
this.currencyCode = Template.currentData().definition.settings.currencyCode;
});
(class extends CardCustomField {
onCreated() {
super.onCreated();
this.currencyCode = this.data().definition.settings.currencyCode;
}
Template['cardCustomField-currency'].helpers({
formattedValue() {
const locale = TAPi18n.getLanguage();
const tpl = Template.instance();
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: tpl.currencyCode,
}).format(this.value);
},
});
currency: this.currencyCode,
}).format(this.data().value);
}
Template['cardCustomField-currency'].events({
'submit .js-card-customfield-currency'(event, tpl) {
event.preventDefault();
// To allow input separated by comma, the comma is replaced by a period.
const value = Number(tpl.find('input').value.replace(/,/i, '.'), 10);
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
events() {
return [
{
'submit .js-card-customfield-currency'(event) {
event.preventDefault();
// To allow input separated by comma, the comma is replaced by a period.
const value = Number(this.find('input').value.replace(/,/i, '.'), 10);
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-currency'));
// cardCustomField-date
Template['cardCustomField-date'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(now());
}, 60000);
(class extends CardCustomField {
onCreated() {
super.onCreated();
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(now());
}, 60000);
self.autorun(() => {
self.date.set(new Date(Template.currentData().value));
});
});
self.autorun(() => {
self.date.set(new Date(self.data().value));
});
}
Template['cardCustomField-date'].helpers({
showWeek() {
return getISOWeek(Template.instance().date.get()).toString();
},
return getISOWeek(this.date.get()).toString();
}
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
},
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showISODate() {
return Template.instance().date.get().toISOString();
},
return this.date.get().toISOString();
}
classes() {
const tpl = Template.instance();
if (
isBefore(tpl.date.get(), tpl.now.get(), 'minute') &&
isBefore(tpl.now.get(), this.value, 'minute')
isBefore(this.date.get(), this.now.get(), 'minute') &&
isBefore(this.now.get(), this.data().value, 'minute')
) {
return 'current';
}
return '';
},
showTitle() {
return `${TAPi18n.__('card-start-on')} ${Template.instance().date.get().toLocaleString()}`;
},
});
}
Template['cardCustomField-date'].events({
'click .js-edit-date': Popup.open('cardCustomField-date'),
});
showTitle() {
return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
}
events() {
return [
{
'click .js-edit-date': Popup.open('cardCustomField-date'),
},
];
}
}.register('cardCustomField-date'));
// cardCustomField-datePopup
Template['cardCustomField-datePopup'].onCreated(function () {
const data = Template.currentData();
setupDatePicker(this, {
initialDate: data.value ? data.value : undefined,
});
// Override card and store customFieldId for store/delete callbacks
this.datePicker.card = getCurrentCardFromContext();
this.customFieldId = data._id;
});
(class extends DatePicker {
onCreated() {
super.onCreated();
const self = this;
self.card = Utils.getCurrentCard();
self.customFieldId = this.data()._id;
this.data().value && this.date.set(new Date(this.data().value));
}
Template['cardCustomField-datePopup'].onRendered(function () {
datePickerRendered(this);
});
_storeDate(date) {
this.card.setCustomField(this.customFieldId, date);
}
Template['cardCustomField-datePopup'].helpers(datePickerHelpers());
Template['cardCustomField-datePopup'].events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setCustomField(this.customFieldId, date);
},
deleteDate() {
this.datePicker.card.setCustomField(this.customFieldId, '');
},
}));
_deleteDate() {
this.card.setCustomField(this.customFieldId, '');
}
}.register('cardCustomField-datePopup'));
// cardCustomField-dropdown
Template['cardCustomField-dropdown'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
this._items = Template.currentData().definition.settings.dropdownItems;
this.items = this._items.slice(0);
this.items.unshift({
_id: '',
name: TAPi18n.__('custom-field-dropdown-none'),
});
});
(class extends CardCustomField {
onCreated() {
super.onCreated();
this._items = this.data().definition.settings.dropdownItems;
this.items = this._items.slice(0);
this.items.unshift({
_id: '',
name: TAPi18n.__('custom-field-dropdown-none'),
});
}
Template['cardCustomField-dropdown'].helpers({
items() {
return Template.instance().items;
},
selectedItem() {
const tpl = Template.instance();
const selected = tpl._items.find(item => {
return item._id === this.value;
const selected = this._items.find(item => {
return item._id === this.data().value;
});
return selected
? selected.name
: TAPi18n.__('custom-field-dropdown-unknown');
},
});
}
Template['cardCustomField-dropdown'].events({
'submit .js-card-customfield-dropdown'(event, tpl) {
event.preventDefault();
const value = tpl.find('select').value;
tpl.card.setCustomField(tpl.customFieldId, value);
},
});
events() {
return [
{
'submit .js-card-customfield-dropdown'(event) {
event.preventDefault();
const value = this.find('select').value;
this.card.setCustomField(this.customFieldId, value);
},
},
];
}
}.register('cardCustomField-dropdown'));
// cardCustomField-stringtemplate
Template['cardCustomField-stringtemplate'].onCreated(function () {
this.card = getCurrentCardFromContext();
this.customFieldId = Template.currentData()._id;
this.customField = new CustomFieldStringTemplate(Template.currentData().definition);
this.stringtemplateItems = new ReactiveVar(Template.currentData().value ?? []);
});
class CardCustomFieldStringTemplate extends CardCustomField {
onCreated() {
super.onCreated();
this.customField = new CustomFieldStringTemplate(this.data().definition);
this.stringtemplateItems = new ReactiveVar(this.data().value ?? []);
}
Template['cardCustomField-stringtemplate'].helpers({
formattedValue() {
const tpl = Template.instance();
const ret = tpl.customField.getFormattedValue(this.value);
const ret = this.customField.getFormattedValue(this.data().value);
return ret;
},
stringtemplateItems() {
return Template.instance().stringtemplateItems.get();
},
});
}
Template['cardCustomField-stringtemplate'].events({
'submit .js-card-customfield-stringtemplate'(event, tpl) {
event.preventDefault();
const items = tpl.stringtemplateItems.get();
tpl.card.setCustomField(tpl.customFieldId, items);
},
getItems() {
return Array.from(this.findAll('input'))
.map(input => input.value)
.filter(value => !!value.trim());
}
'keydown .js-card-customfield-stringtemplate-item'(event, tpl) {
if (event.keyCode === 13) {
event.preventDefault();
events() {
return [
{
'submit .js-card-customfield-stringtemplate'(event) {
event.preventDefault();
const items = this.stringtemplateItems.get();
this.card.setCustomField(this.customFieldId, items);
},
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
const inputLast = tpl.find('input.last');
'keydown .js-card-customfield-stringtemplate-item'(event) {
if (event.keyCode === 13) {
event.preventDefault();
let items = Array.from(tpl.findAll('input'))
.map(input => input.value)
.filter(value => !!value.trim());
if (event.target.value.trim() || event.metaKey || event.ctrlKey) {
const inputLast = this.find('input.last');
if (event.target === inputLast) {
inputLast.value = '';
} else if (event.target.nextSibling === inputLast) {
inputLast.focus();
} else {
event.target.blur();
let items = this.getItems();
const idx = Array.from(tpl.findAll('input')).indexOf(
event.target,
);
items.splice(idx + 1, 0, '');
if (event.target === inputLast) {
inputLast.value = '';
} else if (event.target.nextSibling === inputLast) {
inputLast.focus();
} else {
event.target.blur();
Tracker.afterFlush(() => {
const element = tpl.findAll('input')[idx + 1];
element.focus();
element.value = '';
});
}
const idx = Array.from(this.findAll('input')).indexOf(
event.target,
);
items.splice(idx + 1, 0, '');
tpl.stringtemplateItems.set(items);
}
if (event.metaKey || event.ctrlKey) {
tpl.find('button[type=submit]').click();
}
}
},
Tracker.afterFlush(() => {
const element = this.findAll('input')[idx + 1];
element.focus();
element.value = '';
});
}
'blur .js-card-customfield-stringtemplate-item'(event, tpl) {
if (
!event.target.value.trim() ||
event.target === tpl.find('input.last')
) {
const items = Array.from(tpl.findAll('input'))
.map(input => input.value)
.filter(value => !!value.trim());
tpl.stringtemplateItems.set(items);
tpl.find('input.last').value = '';
}
},
this.stringtemplateItems.set(items);
}
if (event.metaKey || event.ctrlKey) {
this.find('button[type=submit]').click();
}
}
},
'click .js-close-inlined-form'(event, tpl) {
tpl.stringtemplateItems.set(Template.currentData().value ?? []);
},
});
'blur .js-card-customfield-stringtemplate-item'(event) {
if (
!event.target.value.trim() ||
event.target === this.find('input.last')
) {
const items = this.getItems();
this.stringtemplateItems.set(items);
this.find('input.last').value = '';
}
},
'click .js-close-inlined-form'(event) {
this.stringtemplateItems.set(this.data().value ?? []);
},
},
];
}
}
CardCustomFieldStringTemplate.register('cardCustomField-stringtemplate');

View file

@ -14,70 +14,6 @@ template(name="dateBadge")
b
| {{showWeek}}
template(name="cardReceivedDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="cardStartDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="cardDueDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="cardEndDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="dateCustomField")
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
@ -86,46 +22,6 @@ template(name="dateCustomField")
b
| {{showWeek}}
template(name="cardCustomFieldDate")
a(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="voteEndDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="pokerEndDate")
if canModifyCard
a.js-edit-date.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
else
a.card-date(title="{{showTitle}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
time(datetime="{{showISODate}}")
| {{showDate}}
if showWeekOfYear
b
| {{showWeek}}
template(name="minicardReceivedDate")
if canModifyCard
a.js-edit-date.card-date.received-date(title="{{_ 'card-received'}} {{_ 'predicate-week'}} {{#if showWeekOfYear}}{{showWeek}}{{/if}}" class="{{classes}}")
@ -199,91 +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'}}
template(name="editVoteEndDatePopup")
.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'}}
template(name="editPokerEndDatePopup")
.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,11 +1,5 @@
import { TAPi18n } from '/imports/i18n';
import { ReactiveCache } from '/imports/reactiveCache';
import {
setupDatePicker,
datePickerRendered,
datePickerHelpers,
datePickerEvents,
} from '/client/lib/datepicker';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
@ -29,159 +23,143 @@ import {
diff
} from '/imports/lib/dateUtils';
// --- DatePicker popups (edit date forms) ---
// editCardReceivedDatePopup
Template.editCardReceivedDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: formatDateTime(now()),
initialDate: card.getReceived() ? card.getReceived() : undefined,
});
});
(class extends DatePicker {
onCreated() {
super.onCreated(formatDateTime(now()));
this.data().getReceived() &&
this.date.set(new Date(this.data().getReceived()));
}
Template.editCardReceivedDatePopup.onRendered(function () {
datePickerRendered(this);
});
_storeDate(date) {
this.card.setReceived(formatDateTime(date));
}
Template.editCardReceivedDatePopup.helpers(datePickerHelpers());
Template.editCardReceivedDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setReceived(formatDateTime(date));
},
deleteDate() {
this.datePicker.card.unsetReceived();
},
}));
_deleteDate() {
this.card.unsetReceived();
}
}.register('editCardReceivedDatePopup'));
// editCardStartDatePopup
Template.editCardStartDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: formatDateTime(now()),
initialDate: card.getStart() ? card.getStart() : undefined,
});
});
(class extends DatePicker {
onCreated() {
super.onCreated(formatDateTime(now()));
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
}
Template.editCardStartDatePopup.onRendered(function () {
datePickerRendered(this);
});
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
Template.editCardStartDatePopup.helpers(datePickerHelpers());
_storeDate(date) {
this.card.setStart(formatDateTime(date));
}
Template.editCardStartDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setStart(formatDateTime(date));
},
deleteDate() {
this.datePicker.card.unsetStart();
},
}));
_deleteDate() {
this.card.unsetStart();
}
}.register('editCardStartDatePopup'));
// editCardDueDatePopup
Template.editCardDueDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: '1970-01-01 17:00:00',
initialDate: card.getDue() ? card.getDue() : undefined,
});
});
(class extends DatePicker {
onCreated() {
super.onCreated('1970-01-01 17:00:00');
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
}
Template.editCardDueDatePopup.onRendered(function () {
datePickerRendered(this);
});
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
Template.editCardDueDatePopup.helpers(datePickerHelpers());
_storeDate(date) {
this.card.setDue(formatDateTime(date));
}
Template.editCardDueDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setDue(formatDateTime(date));
},
deleteDate() {
this.datePicker.card.unsetDue();
},
}));
_deleteDate() {
this.card.unsetDue();
}
}.register('editCardDueDatePopup'));
// editCardEndDatePopup
Template.editCardEndDatePopup.onCreated(function () {
const card = Template.currentData();
setupDatePicker(this, {
defaultTime: formatDateTime(now()),
initialDate: card.getEnd() ? card.getEnd() : undefined,
});
});
(class extends DatePicker {
onCreated() {
super.onCreated(formatDateTime(now()));
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
}
Template.editCardEndDatePopup.onRendered(function () {
datePickerRendered(this);
});
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
Template.editCardEndDatePopup.helpers(datePickerHelpers());
_storeDate(date) {
this.card.setEnd(formatDateTime(date));
}
Template.editCardEndDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setEnd(formatDateTime(date));
_deleteDate() {
this.card.unsetEnd();
}
}.register('editCardEndDatePopup'));
// Display received, start, due & end dates
const CardDate = BlazeComponent.extendComponent({
template() {
return 'dateBadge';
},
deleteDate() {
this.datePicker.card.unsetEnd();
onCreated() {
const self = this;
self.date = ReactiveVar();
self.now = ReactiveVar(now());
window.setInterval(() => {
self.now.set(now());
}, 60000);
},
}));
// --- Card date badge display helpers ---
showWeek() {
return getISOWeek(this.date.get()).toString();
},
// Shared onCreated logic for card date badge templates
function cardDateOnCreated(tpl) {
tpl.date = new ReactiveVar();
tpl.now = new ReactiveVar(now());
window.setInterval(() => {
tpl.now.set(now());
}, 60000);
}
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
},
// Shared helpers for card date badge templates
function cardDateHelpers(extraHelpers) {
const base = {
showWeek() {
return getISOWeek(Template.instance().date.get()).toString();
},
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
return false;
}
return user.isShowWeekOfYear();
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showISODate() {
return Template.instance().date.get().toISOString();
},
};
return Object.assign(base, extraHelpers);
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
},
// cardReceivedDate
Template.cardReceivedDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getReceived()));
});
showISODate() {
return this.date.get().toISOString();
},
});
Template.cardReceivedDate.helpers(cardDateHelpers({
class CardReceivedDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getReceived()));
});
}
classes() {
const tpl = Template.instance();
let classes = 'received-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const startAt = data.getStart();
const theDate = tpl.date.get();
const dueAt = this.data().getDue();
const endAt = this.data().getEnd();
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)) ||
(endAt && isAfter(theDate, endAt)) ||
@ -192,453 +170,332 @@ Template.cardReceivedDate.helpers(cardDateHelpers({
classes += 'not-due';
}
return classes;
},
}
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
},
}));
}
Template.cardReceivedDate.events({
'click .js-edit-date': Popup.open('editCardReceivedDate'),
});
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardReceivedDate'),
});
}
}
CardReceivedDate.register('cardReceivedDate');
// cardStartDate
Template.cardStartDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getStart()));
});
});
class CardStartDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getStart()));
});
}
Template.cardStartDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'start-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
const dueAt = this.data().getDue();
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';
} else if (isAfter(theDate, nowVal)) {
} else if (isAfter(theDate, now)) {
// Start date is in the future - not due yet
classes += 'not-due';
} else {
// Start date is today or in the past - current/active
classes += 'current';
}
return classes;
},
}
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
},
}));
}
Template.cardStartDate.events({
'click .js-edit-date': Popup.open('editCardStartDate'),
});
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardStartDate'),
});
}
}
CardStartDate.register('cardStartDate');
// cardDueDate
Template.cardDueDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getDue()));
});
});
class CardDueDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getDue()));
});
}
Template.cardDueDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'due-date ';
const data = Template.currentData();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
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';
} else if (endAt) {
}
// If there's an end date, don't show due date status since task is completed
else if (endAt) {
classes += 'completed';
} else {
const daysDiff = diff(theDate, nowVal, 'days');
}
// 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';
} else if (daysDiff <= 1) {
// Due today or tomorrow - due soon
classes += 'due-soon';
} else {
// Due date is more than 1 day away - not due yet
classes += 'not-due';
}
}
return classes;
},
}
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
},
}));
}
Template.cardDueDate.events({
'click .js-edit-date': Popup.open('editCardDueDate'),
});
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardDueDate'),
});
}
}
CardDueDate.register('cardDueDate');
// cardEndDate
Template.cardEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getEnd()));
});
});
class CardEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getEnd()));
});
}
Template.cardEndDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'end-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const theDate = tpl.date.get();
const dueAt = this.data().getDue();
const theDate = this.date.get();
if (!dueAt) {
// No due date set - just show as completed
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
// End date is before due date - completed early
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
// End date is after due date - completed late
classes += 'completed-late';
} else {
// End date equals due date - completed on time
classes += 'completed-on-time';
}
return classes;
},
}
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
},
}));
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
}
Template.cardEndDate.events({
'click .js-edit-date': Popup.open('editCardEndDate'),
});
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editCardEndDate'),
});
}
}
CardEndDate.register('cardEndDate');
// cardCustomFieldDate
Template.cardCustomFieldDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().value));
});
});
class CardCustomFieldDate extends CardDate {
template() {
return 'dateCustomField';
}
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().value));
});
}
showWeek() {
return getISOWeek(this.date.get()).toString();
}
showWeekOfYear() {
const user = ReactiveCache.getCurrentUser();
if (!user) {
// For non-logged-in users, week of year is not shown
return false;
}
return user.isShowWeekOfYear();
}
Template.cardCustomFieldDate.helpers(cardDateHelpers({
showDate() {
const tpl = Template.instance();
// this will start working once mquandalle:moment
// is updated to at least moment.js 2.10.5
// until then, the date is displayed in the "L" format
return tpl.date.get().calendar(null, {
return this.date.get().calendar(null, {
sameElse: 'llll',
});
},
}
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${formattedDate}`;
},
}
classes() {
return 'customfield-date';
},
}));
}
// --- Minicard date templates ---
events() {
return [];
}
}
CardCustomFieldDate.register('cardCustomFieldDate');
// minicardReceivedDate
Template.minicardReceivedDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getReceived()));
});
});
(class extends CardReceivedDate {
template() {
return 'minicardReceivedDate';
}
Template.minicardReceivedDate.helpers(cardDateHelpers({
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardReceivedDate'));
(class extends CardStartDate {
template() {
return 'minicardStartDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardStartDate'));
(class extends CardDueDate {
template() {
return 'minicardDueDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardDueDate'));
(class extends CardEndDate {
template() {
return 'minicardEndDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardEndDate'));
(class extends CardCustomFieldDate {
template() {
return 'minicardCustomFieldDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
}.register('minicardCustomFieldDate'));
class VoteEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getVoteEnd()));
});
}
classes() {
const tpl = Template.instance();
let classes = 'received-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const startAt = data.getStart();
const theDate = tpl.date.get();
if (
(startAt && isAfter(theDate, startAt)) ||
(endAt && isAfter(theDate, endAt)) ||
(dueAt && isAfter(theDate, dueAt))
) {
classes += 'overdue';
} else {
classes += 'not-due';
}
const classes = 'end-date' + ' ';
return classes;
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
},
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
}
Template.minicardReceivedDate.events({
'click .js-edit-date': Popup.open('editCardReceivedDate'),
});
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editVoteEndDate'),
});
}
}
VoteEndDate.register('voteEndDate');
// minicardStartDate
Template.minicardStartDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getStart()));
});
});
Template.minicardStartDate.helpers(cardDateHelpers({
class PokerEndDate extends CardDate {
onCreated() {
super.onCreated();
const self = this;
self.autorun(() => {
self.date.set(new Date(self.data().getPokerEnd()));
});
}
classes() {
const tpl = Template.instance();
let classes = 'start-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
} else if (isAfter(theDate, nowVal)) {
classes += 'not-due';
} else {
classes += 'current';
}
const classes = 'end-date' + ' ';
return classes;
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
},
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
Template.minicardStartDate.events({
'click .js-edit-date': Popup.open('editCardStartDate'),
});
// minicardDueDate
Template.minicardDueDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getDue()));
});
});
Template.minicardDueDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'due-date ';
const data = Template.currentData();
const endAt = data.getEnd();
const theDate = tpl.date.get();
const nowVal = tpl.now.get();
if (endAt && isBefore(endAt, theDate)) {
classes += 'completed-early';
} else if (endAt) {
classes += 'completed';
} else {
const daysDiff = diff(theDate, nowVal, 'days');
if (daysDiff < 0) {
classes += 'overdue';
} else if (daysDiff <= 1) {
classes += 'due-soon';
} else {
classes += 'not-due';
}
}
return classes;
},
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;
}
Template.minicardDueDate.events({
'click .js-edit-date': Popup.open('editCardDueDate'),
});
// minicardEndDate
Template.minicardEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getEnd()));
});
});
Template.minicardEndDate.helpers(cardDateHelpers({
classes() {
const tpl = Template.instance();
let classes = 'end-date ';
const data = Template.currentData();
const dueAt = data.getDue();
const theDate = tpl.date.get();
if (!dueAt) {
classes += 'completed';
} else if (isBefore(theDate, dueAt)) {
classes += 'completed-early';
} else if (isAfter(theDate, dueAt)) {
classes += 'completed-late';
} else {
classes += 'completed-on-time';
}
return classes;
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
}));
Template.minicardEndDate.events({
'click .js-edit-date': Popup.open('editCardEndDate'),
});
// minicardCustomFieldDate
Template.minicardCustomFieldDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().value));
});
});
Template.minicardCustomFieldDate.helpers(cardDateHelpers({
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
const tpl = Template.instance();
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true);
return `${formattedDate}`;
},
classes() {
return 'customfield-date';
},
}));
// --- Vote and Poker end date badge templates ---
// voteEndDate
Template.voteEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getVoteEnd()));
});
});
Template.voteEndDate.helpers(cardDateHelpers({
classes() {
return 'end-date ';
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${tpl.date.get().toLocaleString()}`;
},
}));
Template.voteEndDate.events({
'click .js-edit-date': Popup.open('editVoteEndDate'),
});
// pokerEndDate
Template.pokerEndDate.onCreated(function () {
cardDateOnCreated(this);
const self = this;
self.autorun(() => {
self.date.set(new Date(Template.currentData().getPokerEnd()));
});
});
Template.pokerEndDate.helpers(cardDateHelpers({
classes() {
return 'end-date ';
},
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true);
},
showTitle() {
const tpl = Template.instance();
return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`;
},
}));
Template.pokerEndDate.events({
'click .js-edit-date': Popup.open('editPokerEndDate'),
});
events() {
return super.events().concat({
'click .js-edit-date': Popup.open('editPokerEndDate'),
});
}
}
PokerEndDate.register('pokerEndDate');

View file

@ -1,29 +1,37 @@
const descriptionFormIsOpen = new ReactiveVar(false);
Template.descriptionForm.onDestroyed(function () {
descriptionFormIsOpen.set(false);
$('.note-popover').hide();
});
BlazeComponent.extendComponent({
onDestroyed() {
descriptionFormIsOpen.set(false);
$('.note-popover').hide();
},
Template.descriptionForm.helpers({
descriptionFormIsOpen() {
return descriptionFormIsOpen.get();
},
});
Template.descriptionForm.events({
async 'submit .js-card-description'(event, tpl) {
event.preventDefault();
const description = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val();
await this.setDescription(description);
getInput() {
return this.$('.js-new-description-input');
},
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt, tpl) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
const submitButton = tpl.find('button[type=submit]');
if (submitButton) {
submitButton.click();
}
}
events() {
return [
{
'submit .js-card-description'(event) {
event.preventDefault();
const description = this.currentComponent().getValue();
this.data().setDescription(description);
},
// Pressing Ctrl+Enter should submit the form
'keydown form textarea'(evt) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
const submitButton = this.find('button[type=submit]');
if (submitButton) {
submitButton.click();
}
}
},
},
];
},
});
}).register('descriptionForm');

View file

@ -1,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
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
| 📤
| {{_ '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'}}
@ -1077,20 +909,19 @@ template(name="cardMorePopup")
option(value="{{_id}}") {{title}}
br
| {{_ 'added'}}
span.date(title=card.createdAt) {{ displayDate createdAt 'LLL' }}
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
if currentUser.isBoardAdmin
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}
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

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
template(name="editCardSpentTimePopup")
template(name="editCardSpentTime")
.edit-card-time
form.edit-time
.fields
@ -13,7 +13,7 @@ template(name="editCardSpentTimePopup")
button.primary.wide.left.js-submit-time(type="submit") {{_ 'save'}}
button.js-delete-time.negate.wide.right {{_ 'delete'}}
template(name="cardSpentTime")
template(name="timeBadge")
if canModifyCard
a.js-edit-time.card-time(title="{{_ 'time'}}" class="{{#if getIsOvertime}}card-label-red{{else}}card-label-green{{/if}}")
| ⏱️ {{showTime}}

View file

@ -1,91 +1,85 @@
import { TAPi18n } from '/imports/i18n';
import Cards from '/models/cards';
import { getCurrentCardIdFromContext } from '/client/lib/currentCard';
function getCardId() {
return getCurrentCardIdFromContext();
}
Template.editCardSpentTimePopup.onCreated(function () {
this.error = new ReactiveVar('');
this.card = Cards.findOne(getCardId());
});
Template.editCardSpentTimePopup.helpers({
error() {
return Template.instance().error;
BlazeComponent.extendComponent({
template() {
return 'editCardSpentTime';
},
card() {
return Cards.findOne(getCardId());
onCreated() {
this.error = new ReactiveVar('');
this.card = this.data();
},
getIsOvertime() {
const card = Cards.findOne(getCardId());
return card?.getIsOvertime ? card.getIsOvertime() : false;
},
});
Template.editCardSpentTimePopup.events({
//TODO : need checking this portion
'submit .edit-time'(evt, tpl) {
evt.preventDefault();
const card = Cards.findOne(getCardId());
if (!card) return;
const spentTime = parseFloat(evt.target.time.value);
let isOvertime = false;
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
isOvertime = true;
}
if (spentTime >= 0) {
card.setSpentTime(spentTime);
card.setIsOvertime(isOvertime);
Popup.back();
} else {
tpl.error.set('invalid-time');
evt.target.time.focus();
}
},
'click .js-delete-time'(evt) {
evt.preventDefault();
const card = Cards.findOne(getCardId());
if (!card) return;
card.setSpentTime(null);
card.setIsOvertime(false);
Popup.back();
},
'click a.js-toggle-overtime'(evt) {
const card = Cards.findOne(getCardId());
if (!card) return;
card.setIsOvertime(!card.getIsOvertime());
toggleOvertime() {
this.card.setIsOvertime(!this.card.getIsOvertime());
$('#overtime .materialCheckBox').toggleClass('is-checked');
$('#overtime').toggleClass('is-checked');
},
});
storeTime(spentTime, isOvertime) {
this.card.setSpentTime(spentTime);
this.card.setIsOvertime(isOvertime);
},
deleteTime() {
this.card.setSpentTime(null);
this.card.setIsOvertime(false);
},
events() {
return [
{
//TODO : need checking this portion
'submit .edit-time'(evt) {
evt.preventDefault();
Template.cardSpentTime.helpers({
const spentTime = parseFloat(evt.target.time.value);
//const isOvertime = this.card.getIsOvertime();
let isOvertime = false;
if ($('#overtime').attr('class').indexOf('is-checked') >= 0) {
isOvertime = true;
}
if (spentTime >= 0) {
this.storeTime(spentTime, isOvertime);
Popup.back();
} else {
this.error.set('invalid-time');
evt.target.time.focus();
}
},
'click .js-delete-time'(evt) {
evt.preventDefault();
this.deleteTime();
Popup.back();
},
'click a.js-toggle-overtime': this.toggleOvertime,
},
];
},
}).register('editCardSpentTimePopup');
BlazeComponent.extendComponent({
template() {
return 'timeBadge';
},
onCreated() {
const self = this;
self.time = ReactiveVar();
},
showTitle() {
const card = Cards.findOne(this._id) || this;
if (card.getIsOvertime && card.getIsOvertime()) {
if (this.data().getIsOvertime()) {
return `${TAPi18n.__(
'overtime',
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
} else if (card.getSpentTime) {
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
} else {
return `${TAPi18n.__(
'card-spent',
)} ${card.getSpentTime()} ${TAPi18n.__('hours')}`;
)} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`;
}
return '';
},
showTime() {
const card = Cards.findOne(this._id) || this;
return card.getSpentTime ? card.getSpentTime() : '';
return this.data().getSpentTime();
},
getIsOvertime() {
const card = Cards.findOne(this._id) || this;
return card.getIsOvertime ? card.getIsOvertime() : false;
events() {
return [
{
'click .js-edit-time': Popup.open('editCardSpentTime'),
},
];
},
});
Template.cardSpentTime.events({
'click .js-edit-time': Popup.open('editCardSpentTime'),
});
}).register('cardSpentTime');

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'}}")
span(title=createdAt) {{ displayDate createdAt }}
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

@ -2,7 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import Cards from '/models/cards';
import Boards from '/models/boards';
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
const subManager = new SubsManager();
const { calculateIndexData, capitalize } = Utils;
@ -45,63 +45,55 @@ function initSorting(items) {
});
}
Template.checklistDetail.onRendered(function () {
const tpl = this;
tpl.itemsDom = this.$('.js-checklist-items');
initSorting(tpl.itemsDom);
tpl.itemsDom.mousedown(function (evt) {
evt.stopPropagation();
});
BlazeComponent.extendComponent({
onRendered() {
const self = this;
self.itemsDom = this.$('.js-checklist-items');
initSorting(self.itemsDom);
self.itemsDom.mousedown(function (evt) {
evt.stopPropagation();
});
function userIsMember() {
return ReactiveCache.getCurrentUser()?.isBoardMember();
}
// Disable sorting if the current user is not a board member
tpl.autorun(() => {
const $itemsDom = $(tpl.itemsDom);
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
$(tpl.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(tpl.itemsDom).sortable({
handle: 'span.fa.checklistitem-handle',
});
}
function userIsMember() {
return ReactiveCache.getCurrentUser()?.isBoardMember();
}
});
});
Template.checklistDetail.helpers({
// Disable sorting if the current user is not a board member
self.autorun(() => {
const $itemsDom = $(self.itemsDom);
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(self.itemsDom).sortable({
handle: 'span.fa.checklistitem-handle',
});
}
}
});
},
/** returns the finished percent of the checklist */
finishedPercent() {
const ret = this.checklist.finishedPercent();
const ret = this.data().checklist.finishedPercent();
return ret;
},
});
}).register('checklistDetail');
Template.checklists.helpers({
checklists() {
const card = ReactiveCache.getCard(this.cardId);
const ret = card.checklists();
return ret;
},
});
Template.checklists.events({
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
'submit .js-add-checklist'(event, tpl) {
BlazeComponent.extendComponent({
addChecklist(event) {
event.preventDefault();
const textarea = tpl.find('textarea.js-add-checklist-item');
const textarea = this.find('textarea.js-add-checklist-item');
const title = textarea.value.trim();
let cardId = Template.currentData().cardId;
let cardId = this.currentData().cardId;
const card = ReactiveCache.getCard(cardId);
//if (card.isLinked()) cardId = card.linkedId;
if (card.isLinkedCard()) {
cardId = card.linkedId;
}
let sortIndex;
let checklistItemIndex;
if (Template.currentData().position === 'top') {
if (this.currentData().position === 'top') {
sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base;
checklistItemIndex = 0;
} else {
@ -115,34 +107,27 @@ Template.checklists.events({
title,
sort: sortIndex,
});
tpl.$('.js-close-inlined-form').click();
this.closeAllInlinedForms();
setTimeout(() => {
tpl.$('.add-checklist-item')
this.$('.add-checklist-item')
.eq(checklistItemIndex)
.click();
}, 100);
}
},
'submit .js-edit-checklist-title'(event, tpl) {
addChecklistItem(event) {
event.preventDefault();
const textarea = tpl.find('textarea.js-edit-checklist-item');
const textarea = this.find('textarea.js-add-checklist-item');
const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
const newlineBecomesNewChecklistItemOriginOrder = this.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
const title = textarea.value.trim();
const checklist = Template.currentData().checklist;
checklist.setTitle(title);
},
'submit .js-add-checklist-item'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-add-checklist-item');
const newlineBecomesNewChecklistItem = tpl.find('input#toggleNewlineBecomesNewChecklistItem');
const newlineBecomesNewChecklistItemOriginOrder = tpl.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder');
const title = textarea.value.trim();
const checklist = Template.currentData().checklist;
const checklist = this.currentData().checklist;
if (title) {
let checklistItems = [title];
if (newlineBecomesNewChecklistItem.checked) {
checklistItems = title.split('\n').map(_value => _value.trim());
if (Template.currentData().position === 'top') {
if (this.currentData().position === 'top') {
if (newlineBecomesNewChecklistItemOriginOrder.checked === false) {
checklistItems = checklistItems.reverse();
}
@ -150,7 +135,7 @@ Template.checklists.events({
}
let addIndex;
let sortIndex;
if (Template.currentData().position === 'top') {
if (this.currentData().position === 'top') {
sortIndex = Utils.calculateIndexData(null, checklist.firstItem()).base;
addIndex = -1;
} else {
@ -171,39 +156,33 @@ Template.checklists.events({
textarea.value = '';
textarea.focus();
},
'submit .js-edit-checklist-item'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const item = Template.currentData().item;
item.setTitle(title);
},
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
async 'click .js-delete-checklist-item'() {
const checklist = Template.currentData().checklist;
const item = Template.currentData().item;
deleteItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
ChecklistItems.remove(item._id);
}
},
'focus .js-add-checklist-item'(event) {
// If a new checklist is created, pre-fill the title and select it.
const checklist = Template.currentData().checklist;
if (!checklist) {
const textarea = event.target;
textarea.value = capitalize(TAPi18n.__('r-checklist'));
textarea.select();
}
},
// add and delete checklist / checklist-item
'click .js-open-inlined-form'(event, tpl) {
tpl.$('.js-close-inlined-form').click();
},
'click #toggleHideFinishedChecklist'(event) {
editChecklist(event) {
event.preventDefault();
Template.currentData().card.toggleHideFinishedChecklist();
const textarea = this.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const checklist = this.currentData().checklist;
checklist.setTitle(title);
},
keydown(event) {
editChecklistItem(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-checklist-item');
const title = textarea.value.trim();
const item = this.currentData().item;
item.setTitle(title);
},
pressKey(event) {
//If user press enter key inside a form, submit it
//Unless the user is also holding down the 'shift' key
if (event.keyCode === 13 && !event.shiftKey) {
@ -212,201 +191,201 @@ Template.checklists.events({
$form.find('button[type=submit]').click();
}
},
});
// NOTE: boardsSwimlanesAndLists template was removed from jade but JS was left behind.
// This is dead code — the template no longer exists in any jade file.
Template.addChecklistItemForm.onRendered(function () {
autosize(this.$('textarea.js-add-checklist-item'));
});
Template.addChecklistItemForm.events({
'click a.fa.fa-copy'(event, tpl) {
const $editor = tpl.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = tpl.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
});
Template.checklistActionsPopup.events({
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
const checklist = this.checklist;
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
focusChecklistItem(event) {
// If a new checklist is created, pre-fill the title and select it.
const checklist = this.currentData().checklist;
if (!checklist) {
const textarea = event.target;
textarea.value = capitalize(TAPi18n.__('r-checklist'));
textarea.select();
}
}),
'click .js-move-checklist': Popup.open('moveChecklist'),
'click .js-copy-checklist': Popup.open('copyChecklist'),
'click .js-hide-checked-checklist-items'(event) {
event.preventDefault();
Template.currentData().checklist.toggleHideCheckedChecklistItems();
Popup.back();
},
'click .js-hide-all-checklist-items'(event) {
event.preventDefault();
Template.currentData().checklist.toggleHideAllChecklistItems();
Popup.back();
/** closes all inlined forms (checklist and checklist-item input fields) */
closeAllInlinedForms() {
this.$('.js-close-inlined-form').click();
},
events() {
return [
{
'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
'submit .js-add-checklist': this.addChecklist,
'submit .js-edit-checklist-title': this.editChecklist,
'submit .js-add-checklist-item': this.addChecklistItem,
'submit .js-edit-checklist-item': this.editChecklistItem,
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
'click .js-delete-checklist-item': this.deleteItem,
'focus .js-add-checklist-item': this.focusChecklistItem,
// add and delete checklist / checklist-item
'click .js-open-inlined-form': this.closeAllInlinedForms,
'click #toggleHideFinishedChecklist'(event) {
event.preventDefault();
this.data().card.toggleHideFinishedChecklist();
},
keydown: this.pressKey,
},
];
},
}).register('checklists');
BlazeComponent.extendComponent({
onCreated() {
subManager.subscribe('board', Session.get('currentBoard'), false);
this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
},
boards() {
const ret = ReactiveCache.getBoards(
{
archived: false,
'members.userId': Meteor.userId(),
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
},
{
sort: { sort: 1 /* boards default sorting */ },
},
);
return ret;
},
swimlanes() {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
return board.swimlanes();
},
aBoardLists() {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
return board.lists();
},
events() {
return [
{
'change .js-select-boards'(event) {
this.selectedBoardId.set($(event.currentTarget).val());
subManager.subscribe('board', this.selectedBoardId.get(), false);
},
},
];
},
}).register('boardsSwimlanesAndLists');
Template.checklists.helpers({
checklists() {
const card = ReactiveCache.getCard(this.cardId);
const ret = card.checklists();
return ret;
},
});
Template.editChecklistItemForm.onRendered(function () {
autosize(this.$('textarea.js-edit-checklist-item'));
});
Template.editChecklistItemForm.events({
'click a.fa.fa-copy'(event, tpl) {
const $editor = tpl.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = tpl.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
BlazeComponent.extendComponent({
onRendered() {
autosize(this.$('textarea.js-add-checklist-item'));
},
});
events() {
return [
{
'click a.fa.fa-copy'(event) {
const $editor = this.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = this.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
}
];
}
}).register('addChecklistItemForm');
BlazeComponent.extendComponent({
events() {
return [
{
'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () {
Popup.back(2);
const checklist = this.checklist;
if (checklist && checklist._id) {
Checklists.remove(checklist._id);
}
}),
'click .js-move-checklist': Popup.open('moveChecklist'),
'click .js-copy-checklist': Popup.open('copyChecklist'),
'click .js-hide-checked-checklist-items'(event) {
event.preventDefault();
this.data().checklist.toggleHideCheckedChecklistItems();
Popup.back();
},
'click .js-hide-all-checklist-items'(event) {
event.preventDefault();
this.data().checklist.toggleHideAllChecklistItems();
Popup.back();
},
}
]
}
}).register('checklistActionsPopup');
BlazeComponent.extendComponent({
onRendered() {
autosize(this.$('textarea.js-edit-checklist-item'));
},
events() {
return [
{
'click a.fa.fa-copy'(event) {
const $editor = this.$('textarea');
const promise = Utils.copyTextToClipboard($editor[0].value);
const $tooltip = this.$('.copied-tooltip');
Utils.showCopied(promise, $tooltip);
},
}
];
}
}).register('editChecklistItemForm');
Template.checklistItemDetail.helpers({
});
Template.checklistItemDetail.events({
'click .js-checklist-item .check-box-container'() {
const checklist = Template.currentData().checklist;
const item = Template.currentData().item;
BlazeComponent.extendComponent({
toggleItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
item.toggleItem();
}
},
});
/**
* Helper to find the dialog instance from a parent popup template.
* copyAndMoveChecklist is included inside moveChecklistPopup / copyChecklistPopup,
* so we traverse up the view hierarchy to find the parent template's dialog.
*/
function getParentDialog(tpl) {
let view = tpl.view.parentView;
while (view) {
if (view.templateInstance && view.templateInstance() && view.templateInstance().dialog) {
return view.templateInstance().dialog;
}
view = view.parentView;
}
return null;
}
/** Shared helpers for copyAndMoveChecklist sub-template */
Template.copyAndMoveChecklist.helpers({
boards() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.boards() : [];
events() {
return [
{
'click .js-checklist-item .check-box-container': this.toggleItem,
},
];
},
swimlanes() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.swimlanes() : [];
},
lists() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.lists() : [];
},
cards() {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.cards() : [];
},
isDialogOptionBoardId(boardId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionBoardId(boardId) : false;
},
isDialogOptionSwimlaneId(swimlaneId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionSwimlaneId(swimlaneId) : false;
},
isDialogOptionListId(listId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionListId(listId) : false;
},
isDialogOptionCardId(cardId) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isDialogOptionCardId(cardId) : false;
},
isTitleDefault(title) {
const dialog = getParentDialog(Template.instance());
return dialog ? dialog.isTitleDefault(title) : title;
},
});
/**
* Helper: register standard card dialog events on a checklist popup template.
* Events bubble up from the copyAndMoveChecklist sub-template to the parent popup.
*/
function registerChecklistDialogEvents(templateName) {
Template[templateName].events({
async 'click .js-done'(event, tpl) {
const dialog = tpl.dialog;
const boardSelect = tpl.$('.js-select-boards')[0];
const boardId = boardSelect.options[boardSelect.selectedIndex].value;
const listSelect = tpl.$('.js-select-lists')[0];
const listId = listSelect.options[listSelect.selectedIndex].value;
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
const cardSelect = tpl.$('.js-select-cards')[0];
const cardId = cardSelect.options.length > 0
? cardSelect.options[cardSelect.selectedIndex].value
: null;
const options = { boardId, swimlaneId, listId, cardId };
try {
await dialog.setDone(cardId, options);
} catch (e) {
console.error('Error in card dialog operation:', e);
}
Popup.back(2);
},
'change .js-select-boards'(event, tpl) {
tpl.dialog.getBoardData($(event.currentTarget).val());
},
'change .js-select-swimlanes'(event, tpl) {
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
tpl.dialog.setFirstListId();
},
'change .js-select-lists'(event, tpl) {
tpl.dialog.selectedListId.set($(event.currentTarget).val());
tpl.dialog.selectedCardId.set('');
},
'change .js-select-cards'(event, tpl) {
tpl.dialog.selectedCardId.set($(event.currentTarget).val());
},
});
}
}).register('checklistItemDetail');
/** Move Checklist Dialog */
Template.moveChecklistPopup.onCreated(function () {
this.dialog = new BoardSwimlaneListCardDialog(this, {
getDialogOptions() {
return ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
},
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
await Template.currentData().checklist.move(cardId);
},
});
});
registerChecklistDialogEvents('moveChecklistPopup');
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
return ret;
}
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
this.data().checklist.move(cardId);
}
}).register('moveChecklistPopup');
/** Copy Checklist Dialog */
Template.copyChecklistPopup.onCreated(function () {
this.dialog = new BoardSwimlaneListCardDialog(this, {
getDialogOptions() {
return ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
},
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
await Template.currentData().checklist.copy(cardId);
},
});
});
registerChecklistDialogEvents('copyChecklistPopup');
(class extends DialogWithBoardSwimlaneListCard {
getDialogOptions() {
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
return ret;
}
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
this.data().checklist.copy(cardId);
}
}).register('copyChecklistPopup');

View file

@ -1,6 +0,0 @@
template(name='inlinedCardDescription')
if isOpen.get
form.inlined-form.js-inlined-form(id=id class=classNames)
+Template.contentBlock
else
+Template.elseBlock

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

@ -5,32 +5,29 @@ Meteor.startup(() => {
labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
});
Template.formLabel.onCreated(function () {
this.currentColor = new ReactiveVar(this.data.color);
});
BlazeComponent.extendComponent({
onCreated() {
this.currentColor = new ReactiveVar(this.data().color);
},
Template.formLabel.helpers({
labels() {
return labelColors.map(color => ({ color, name: '' }));
},
isSelected(color) {
return Template.instance().currentColor.get() === color;
return this.currentColor.get() === color;
},
});
Template.formLabel.events({
'click .js-palette-color'(event, tpl) {
tpl.currentColor.set(Template.currentData().color);
const $this = $(event.currentTarget);
// hide selected ll colors
$('.js-palette-select').addClass('hide');
// show select color
$this.find('.js-palette-select').removeClass('hide');
events() {
return [
{
'click .js-palette-color'() {
this.currentColor.set(this.currentData().color);
},
},
];
},
});
}).register('formLabel');
Template.createLabelPopup.helpers({
// This is the default color for a new label. We search the first color that
@ -44,66 +41,79 @@ Template.createLabelPopup.helpers({
},
});
Template.cardLabelsPopup.onRendered(function () {
const tpl = this;
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
const $labels = tpl.$('.edit-labels-pop-over');
BlazeComponent.extendComponent({
onRendered() {
const itemsSelector = 'li.js-card-label-item:not(.placeholder)';
const $labels = this.$('.edit-labels-pop-over');
$labels.sortable({
connectWith: '.edit-labels-pop-over',
tolerance: 'pointer',
appendTo: '.edit-labels-pop-over',
helper(element, currentItem) {
let ret = currentItem.clone();
if (currentItem.closest('.popup-container-depth-0').length == 0)
{ // only set css transform at every sub-popup, not at the main popup
const content = currentItem.closest('.content')[0]
const offsetLeft = content.offsetLeft;
const offsetTop = $('.pop-over > .header').height() * -1;
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
$labels.sortable({
connectWith: '.edit-labels-pop-over',
tolerance: 'pointer',
appendTo: '.edit-labels-pop-over',
helper(element, currentItem) {
let ret = currentItem.clone();
if (currentItem.closest('.popup-container-depth-0').length == 0)
{ // only set css transform at every sub-popup, not at the main popup
const content = currentItem.closest('.content')[0]
const offsetLeft = content.offsetLeft;
const offsetTop = $('.pop-over > .header').height() * -1;
ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`);
}
return ret;
},
distance: 7,
items: itemsSelector,
placeholder: 'card-label-wrapper placeholder',
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.clickExecute(evt.target, 'inlinedForm');
},
stop(evt, ui) {
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
const card = Blaze.getData(this);
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
},
});
// Disable drag-dropping if the current user is not a board member or is comment only
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$labels.sortable({
handle: '.label-handle',
});
}
return ret;
},
distance: 7,
items: itemsSelector,
placeholder: 'card-label-wrapper placeholder',
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.clickExecute(evt.target, 'inlinedForm');
},
stop(evt, ui) {
const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id)
const card = Blaze.getData(this);
card.board().setNewLabelOrder(newLabelOrderOnlyIds);
},
});
// Disable drag-dropping if the current user is not a board member or is comment only
tpl.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$labels.sortable({
handle: '.label-handle',
});
}
});
});
Template.cardLabelsPopup.helpers({
isLabelSelected(cardId) {
return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id);
});
},
});
events() {
return [
{
'click .js-select-label'(event) {
const card = this.data();
const labelId = this.currentData()._id;
card.toggleLabel(labelId);
event.preventDefault();
},
'click .js-edit-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel'),
}
];
}
}).register('cardLabelsPopup');
Template.cardLabelsPopup.events({
'click .js-select-label'(event) {
const card = Template.currentData();
const labelId = this._id;
card.toggleLabel(labelId);
event.preventDefault();
});
Template.formLabel.events({
'click .js-palette-color'(event) {
const $this = $(event.currentTarget);
// hide selected ll colors
$('.js-palette-select').addClass('hide');
// show select color
$this.find('.js-palette-select').removeClass('hide');
},
'click .js-edit-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel'),
});
Template.createLabelPopup.events({
@ -115,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();
},
});
@ -134,8 +155,25 @@ 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();
},
});
Template.cardLabelsPopup.helpers({
isLabelSelected(cardId) {
return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id);
},
});

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

@ -8,9 +8,13 @@ import uploadProgressManager from '../../lib/uploadProgressManager';
// 'click .member': Popup.open('cardMember')
// });
Template.minicard.helpers({
BlazeComponent.extendComponent({
template() {
return 'minicard';
},
formattedCurrencyCustomFieldValue(definition) {
const customField = this
const customField = this.data()
.customFieldsWD()
.find(f => f._id === definition._id);
const customFieldTrueValue =
@ -24,7 +28,7 @@ Template.minicard.helpers({
},
formattedStringtemplateCustomFieldValue(definition) {
const customField = this
const customField = this.data()
.customFieldsWD()
.find(f => f._id === definition._id);
@ -37,7 +41,7 @@ Template.minicard.helpers({
showCreatorOnMinicard() {
// cache "board" to reduce the mini-mongodb access
const board = this.board();
const board = this.data().board();
let ret = false;
if (board) {
ret = board.allowsCreatorOnMinicard ?? false;
@ -45,12 +49,13 @@ Template.minicard.helpers({
return ret;
},
isWatching() {
return this.findWatcher(Meteor.userId());
const card = this.currentData();
return card.findWatcher(Meteor.userId());
},
showMembers() {
// cache "board" to reduce the mini-mongodb access
const board = this.board();
const board = this.data().board();
let ret = false;
if (board) {
ret =
@ -64,7 +69,7 @@ Template.minicard.helpers({
showAssignee() {
// cache "board" to reduce the mini-mongodb access
const board = this.board();
const board = this.data().board();
let ret = false;
if (board) {
ret =
@ -76,6 +81,96 @@ Template.minicard.helpers({
return ret;
},
/** opens the card label popup only if clicked onto a label
* <li> this is necessary to have the data context of the minicard.
* if .js-card-label is used at click event, then only the data context of the label itself is available at this.currentData()
*/
cardLabelsPopup(event) {
if (this.find('.js-card-label:hover')) {
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
}
},
events() {
return [
{
'click .js-linked-link'() {
if (this.data().isLinkedCard()) Utils.goCardId(this.data().linkedId);
else if (this.data().isLinkedBoard())
Utils.goBoardId(this.data().linkedId);
},
'click .js-toggle-minicard-label-text'() {
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
} else {
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
}
},
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
'click .minicard-labels' : this.cardLabelsPopup,
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// Only prevent default for file drags to avoid interfering with sortable
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
},
'dragenter .minicard'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
const card = this.data();
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (Utils.canModifyCard() && board && board.allowsAttachments) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .minicard'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
},
'drop .minicard'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this.data();
const board = card.board();
// Check permissions
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a card reorder)
if (!dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
}
},
}
];
},
}).register('minicard');
Template.minicard.helpers({
hiddenMinicardLabelText() {
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
@ -92,6 +187,9 @@ Template.minicard.helpers({
? Meteor.connection._lastSessionId
: null;
},
isWatching() {
return this.findWatcher(Meteor.userId());
},
// Upload progress helpers
hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id);
@ -111,161 +209,68 @@ 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;
}
});
Template.minicard.events({
'click .js-linked-link'() {
if (this.isLinkedCard()) Utils.goCardId(this.linkedId);
else if (this.isLinkedBoard())
Utils.goBoardId(this.linkedId);
},
'click .js-toggle-minicard-label-text'() {
if (window.localStorage.getItem('hiddenMinicardLabelText')) {
window.localStorage.removeItem('hiddenMinicardLabelText'); //true
} else {
window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true
}
},
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
'click .minicard-labels'(event, tpl) {
if (tpl.find('.js-card-label:hover')) {
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: Template.currentData()});
}
},
'click .js-open-minicard-details-menu'(event, tpl) {
BlazeComponent.extendComponent({
events() {
return [
{
'keydown input.js-edit-card-sort-popup'(evt) {
// enter = save
if (evt.keyCode === 13) {
this.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-card-sort-popup'(event) {
// save button pressed
event.preventDefault();
const sort = this.$('.js-edit-card-sort-popup')[0]
.value
.trim();
if (!Number.isNaN(sort)) {
let card = this.data();
card.move(card.boardId, card.swimlaneId, card.listId, sort);
Popup.back();
}
},
}
]
}
}).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();
event.stopPropagation();
const card = Template.currentData();
Popup.open('cardDetailsActions').call({currentData: () => card}, event);
const minOrder = this.getMinSort();
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
Popup.back();
},
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// Only prevent default for file drags to avoid interfering with sortable
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
}
'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();
},
'dragenter .minicard'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
const card = this;
const board = card.board();
// Only allow drag-and-drop if user can modify card and board allows attachments
if (Utils.canModifyCard() && board && board.allowsAttachments) {
$(event.currentTarget).addClass('is-dragging-over');
}
}
},
'dragleave .minicard'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
}
},
'drop .minicard'(event) {
const dataTransfer = event.originalEvent.dataTransfer;
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
event.preventDefault();
event.stopPropagation();
$(event.currentTarget).removeClass('is-dragging-over');
const card = this;
const board = card.board();
// Check permissions
if (!Utils.canModifyCard() || !board || !board.allowsAttachments) {
return;
}
// Check if this is a file drop (not a card reorder)
if (!dataTransfer.files || dataTransfer.files.length === 0) {
return;
}
const files = dataTransfer.files;
if (files && files.length > 0) {
handleFileUpload(card, files);
}
}
},
});
Template.minicardChecklist.helpers({
visibleItems() {
const checklist = this.checklist || this;
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;
'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();
});
},
});
Template.minicardChecklist.events({
'click .js-open-checklist-menu'(event) {
const data = Template.currentData();
const checklist = data.checklist || data;
const card = data.card || this;
const context = { currentData: () => ({ checklist, card }) };
Popup.open('checklistActions').call(context, event);
},
});
Template.editCardSortOrderPopup.events({
'keydown input.js-edit-card-sort-popup'(evt, tpl) {
// enter = save
if (evt.keyCode === 13) {
tpl.find('button[type=submit]').click();
}
},
'click button.js-submit-edit-card-sort-popup'(event, tpl) {
// save button pressed
event.preventDefault();
const sort = tpl.$('.js-edit-card-sort-popup')[0]
.value
.trim();
if (!Number.isNaN(sort)) {
let card = this;
card.move(card.boardId, card.swimlaneId, card.listId, sort);
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

@ -4,19 +4,32 @@ Template.resultCard.helpers({
},
});
Template.resultCard.events({
'click .js-minicard'(event) {
event.preventDefault();
const cardId = Template.currentData()._id;
const boardId = Template.currentData().boardId;
BlazeComponent.extendComponent({
clickOnMiniCard(evt) {
evt.preventDefault();
const this_ = this;
const cardId = this.currentData()._id;
const boardId = this.currentData().boardId;
Meteor.subscribe('popupCardData', cardId, {
onReady() {
Session.set('popupCardId', cardId);
Session.set('popupCardBoardId', boardId);
if (!Popup.isOpen()) {
Popup.open("cardDetails")(event);
}
this_.cardDetailsPopup(evt);
},
});
},
});
cardDetailsPopup(event) {
if (!Popup.isOpen()) {
Popup.open("cardDetails")(event);
}
},
events() {
return [
{
'click .js-minicard': this.clickOnMiniCard,
},
];
},
}).register('resultCard');

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
@ -51,7 +51,7 @@ template(name="editSubtaskItemForm")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form
span(title=createdAt) {{ displayDate createdAt }}
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
if currentUser.isBoardAdmin
a.js-delete-subtask-item {{_ "delete"}}...
@ -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,13 +1,11 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
Template.subtasks.events({
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
'submit .js-add-subtask'(event, tpl) {
BlazeComponent.extendComponent({
addSubtask(event) {
event.preventDefault();
const textarea = tpl.find('textarea.js-add-subtask-item');
const textarea = this.find('textarea.js-add-subtask-item');
const title = textarea.value.trim();
const cardId = Template.currentData().cardId;
const cardId = this.currentData().cardId;
const card = ReactiveCache.getCard(cardId);
const sortIndex = -1;
const crtBoard = ReactiveCache.getBoard(card.boardId);
@ -54,7 +52,7 @@ Template.subtasks.events({
Filter.addException(_id);
setTimeout(() => {
tpl.$('.add-subtask-item')
this.$('.add-subtask-item')
.last()
.click();
}, 100);
@ -62,20 +60,27 @@ Template.subtasks.events({
textarea.value = '';
textarea.focus();
},
'submit .js-edit-subtask-title'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-edit-subtask-item');
const title = textarea.value.trim();
const subtask = Template.currentData().subtask;
subtask.setTitle(title);
},
async 'click .js-delete-subtask-item'() {
const subtask = Template.currentData().subtask;
deleteSubtask() {
const subtask = this.currentData().subtask;
if (subtask && subtask._id) {
await subtask.archive();
subtask.archive();
}
},
keydown(event) {
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
editSubtask(event) {
event.preventDefault();
const textarea = this.find('textarea.js-edit-subtask-item');
const title = textarea.value.trim();
const subtask = this.currentData().subtask;
subtask.setTitle(title);
},
pressKey(event) {
//If user press enter key inside a form, submit it
//Unless the user is also holding down the 'shift' key
if (event.keyCode === 13 && !event.shiftKey) {
@ -84,58 +89,53 @@ Template.subtasks.events({
$form.find('button[type=submit]').click();
}
},
});
Template.subtasks.onCreated(function () {
this.toggleDeleteDialog = new ReactiveVar(false);
});
events() {
return [
{
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
'submit .js-add-subtask': this.addSubtask,
'submit .js-edit-subtask-title': this.editSubtask,
'click .js-delete-subtask-item': this.deleteSubtask,
keydown: this.pressKey,
},
];
},
}).register('subtasks');
Template.subtasks.helpers({
BlazeComponent.extendComponent({
// ...
}).register('subtaskItemDetail');
BlazeComponent.extendComponent({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
toggleDeleteDialog() {
return Template.instance().toggleDeleteDialog;
},
});
Template.subtaskItemDetail.events({
async 'click .js-subtasks-item .check-box-unicode'() {
const item = Template.currentData().item;
if (item && item._id) {
await item.toggleItem();
}
},
});
Template.subtaskActionsPopup.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
});
Template.subtaskActionsPopup.events({
'click .js-view-subtask'(event) {
if ($(event.target).hasClass('js-view-subtask')) {
const subtask = Template.currentData().subtask;
const board = subtask.board();
FlowRouter.go('card', {
boardId: board._id,
slug: board.slug,
cardId: subtask._id,
swimlaneId: subtask.swimlaneId,
listId: subtask.listId,
});
}
},
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () {
Popup.back(2);
const subtask = this.subtask;
if (subtask && subtask._id) {
await subtask.archive();
}
}),
});
events() {
return [
{
'click .js-view-subtask'(event) {
if ($(event.target).hasClass('js-view-subtask')) {
const subtask = this.currentData().subtask;
const board = subtask.board();
FlowRouter.go('card', {
boardId: board._id,
slug: board.slug,
cardId: subtask._id,
});
}
},
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', function () {
Popup.back(2);
const subtask = this.subtask;
if (subtask && subtask._id) {
subtask.archive();
}
}),
}
]
}
}).register('subtaskActionsPopup');
Template.editSubtaskItemForm.helpers({
user() {
@ -145,3 +145,5 @@ Template.editSubtaskItemForm.helpers({
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
});

View file

@ -0,0 +1,29 @@
<template name="originalPosition">
<div class="original-position-info">
{{#if isLoading}}
<div class="original-position-loading">
<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">
<i class="fa fa-info-circle"></i>
<span class="original-position-text">{{getOriginalPositionDescription}}</span>
</div>
{{else}}
<div class="original-position-unchanged">
<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}}
</div>
{{/if}}
</div>
{{/if}}
</div>
</template>

View file

@ -1,19 +0,0 @@
template(name="originalPosition")
.original-position-info
if isLoading
.original-position-loading
| ⏳ Loading original position...
else
if showOriginalPosition
.original-position-details
if hasMovedFromOriginal
.original-position-moved
span.original-position-text {{getOriginalPositionDescription}}
else
.original-position-unchanged
span.original-position-text ✅ In original position
if getOriginalTitle
.original-title
strong Original title:
| {{getOriginalTitle}}

View file

@ -1,69 +1,69 @@
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import './originalPosition.html';
/**
* Component to display original position information for swimlanes, lists, and cards
*/
class OriginalPositionComponent extends BlazeComponent {
onCreated() {
super.onCreated();
this.originalPosition = new ReactiveVar(null);
this.isLoading = new ReactiveVar(false);
this.hasMoved = new ReactiveVar(false);
Template.originalPosition.onCreated(function () {
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) {
this.loadOriginalPosition(data.entityId, data.entityType);
}
});
}
const tpl = this;
function loadOriginalPosition(entityId, entityType) {
tpl.isLoading.set(true);
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) => {
tpl.isLoading.set(false);
this.isLoading.set(false);
if (error) {
console.error('Error loading original position:', error);
tpl.originalPosition.set(null);
this.originalPosition.set(null);
} else {
tpl.originalPosition.set(result);
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) => {
if (!movedError) {
tpl.hasMoved.set(movedResult);
this.hasMoved.set(movedResult);
}
});
}
});
}
this.autorun(() => {
const data = Template.currentData();
if (data && data.entityId && data.entityType) {
loadOriginalPosition(data.entityId, data.entityType);
}
});
});
Template.originalPosition.helpers({
getOriginalPosition() {
return Template.instance().originalPosition.get();
},
return this.originalPosition.get();
}
isLoading() {
return Template.instance().isLoading.get();
},
return this.isLoading.get();
}
hasMovedFromOriginal() {
return Template.instance().hasMoved.get();
},
return this.hasMoved.get();
}
getOriginalPositionDescription() {
const position = Template.instance().originalPosition.get();
const position = this.getOriginalPosition();
if (!position) return 'No original position data';
if (position.originalPosition) {
const data = Template.currentData();
const entityType = data.entityType;
const entityType = this.data().entityType;
let description = `Original position: ${position.originalPosition.sort || 0}`;
if (entityType === 'list' && position.originalSwimlaneId) {
@ -81,14 +81,18 @@ Template.originalPosition.helpers({
}
return 'No original position data';
},
}
getOriginalTitle() {
const position = Template.instance().originalPosition.get();
const position = this.getOriginalPosition();
return position ? position.originalTitle : '';
},
}
showOriginalPosition() {
return Template.instance().originalPosition.get() !== null;
},
});
return this.getOriginalPosition() !== null;
}
}
OriginalPositionComponent.register('originalPosition');
export default OriginalPositionComponent;

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

@ -1,16 +0,0 @@
import {
setupDatePicker,
datePickerRendered,
datePickerHelpers,
datePickerEvents,
} from '/client/lib/datepicker';
Template.datepicker.onCreated(function () {
setupDatePicker(this);
});
Template.datepicker.onRendered(function () {
datePickerRendered(this);
});
Template.datepicker.helpers(datePickerHelpers());

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,41 +0,0 @@
Template.ganttCard.onCreated(function () {
// Provide the expected parent component properties for cardDetails
this.showOverlay = new ReactiveVar(false);
this.mouseHasEnterCardDetails = false;
});
Template.ganttCard.helpers({
selectedCard() {
// The selected card is now passed as a parameter to the component
return Template.currentData();
},
});
Template.ganttCard.events({
'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;
}
},
});
// 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,69 +1,40 @@
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');
Template.importHeaderBar.helpers({
BlazeComponent.extendComponent({
title() {
return `import-board-title-${Session.get('importSource')}`;
},
});
}).register('importHeaderBar');
// Helper to find the closest ancestor template instance by name
function findParentTemplateInstance(childTemplateInstance, parentTemplateName) {
let view = childTemplateInstance.view;
while (view) {
if (view.name === `Template.${parentTemplateName}` && view.templateInstance) {
return view.templateInstance();
}
view = view.parentView;
}
return null;
}
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.steps = ['importTextarea', 'importMapMembers'];
this._currentStepIndex = new ReactiveVar(0);
this.importedData = new ReactiveVar();
this.membersToMap = new ReactiveVar([]);
this.importSource = Session.get('importSource');
},
function _prepareAdditionalData(dataObject) {
const importSource = Session.get('importSource');
let membersToMap;
switch (importSource) {
case 'trello':
membersToMap = trelloGetMembersToMap(dataObject);
break;
case 'wekan':
membersToMap = wekanGetMembersToMap(dataObject);
break;
case 'csv':
membersToMap = csvGetMembersToMap(dataObject);
break;
}
return membersToMap;
}
currentTemplate() {
return this.steps[this._currentStepIndex.get()];
},
Template.import.onCreated(function () {
this.error = new ReactiveVar('');
this.steps = ['importTextarea', 'importMapMembers'];
this._currentStepIndex = new ReactiveVar(0);
this.importedData = new ReactiveVar();
this.membersToMap = new ReactiveVar([]);
this.importSource = Session.get('importSource');
this.nextStep = () => {
nextStep() {
const nextStepIndex = this._currentStepIndex.get() + 1;
if (nextStepIndex >= this.steps.length) {
this.finishImport();
} else {
this._currentStepIndex.set(nextStepIndex);
}
};
},
this.setError = (error) => {
this.error.set(error);
};
this.importData = (evt, dataSource) => {
importData(evt, dataSource) {
evt.preventDefault();
const input = this.find('.js-import-json').value;
if (dataSource === 'csv') {
@ -71,7 +42,7 @@ Template.import.onCreated(function () {
const ret = Papa.parse(csv);
if (ret && ret.data && ret.data.length) this.importedData.set(ret.data);
else throw new Meteor.Error('error-csv-schema');
const membersToMap = _prepareAdditionalData(ret.data);
const membersToMap = this._prepareAdditionalData(ret.data);
this.membersToMap.set(membersToMap);
this.nextStep();
} else {
@ -79,7 +50,7 @@ Template.import.onCreated(function () {
const dataObject = JSON.parse(input);
this.setError('');
this.importedData.set(dataObject);
const membersToMap = _prepareAdditionalData(dataObject);
const membersToMap = this._prepareAdditionalData(dataObject);
// store members data and mapping in Session
// (we go deep and 2-way, so storing in data context is not a viable option)
this.membersToMap.set(membersToMap);
@ -88,9 +59,13 @@ Template.import.onCreated(function () {
this.setError('error-json-malformed');
}
}
};
},
this.finishImport = () => {
setError(error) {
this.error.set(error);
},
finishImport() {
const additionalData = {};
const membersMapping = this.membersToMap.get();
if (membersMapping) {
@ -118,27 +93,44 @@ Template.import.onCreated(function () {
FlowRouter.go('board', {
id: res,
slug: title,
});
})
//Utils.goBoardId(res);
}
},
);
};
});
Template.import.helpers({
error() {
return Template.instance().error;
},
currentTemplate() {
return Template.instance().steps[Template.instance()._currentStepIndex.get()];
},
});
Template.importTextarea.helpers({
_prepareAdditionalData(dataObject) {
const importSource = Session.get('importSource');
let membersToMap;
switch (importSource) {
case 'trello':
membersToMap = trelloGetMembersToMap(dataObject);
break;
case 'wekan':
membersToMap = wekanGetMembersToMap(dataObject);
break;
case 'csv':
membersToMap = csvGetMembersToMap(dataObject);
break;
}
return membersToMap;
},
_screenAdditionalData() {
return 'mapMembers';
},
}).register('import');
BlazeComponent.extendComponent({
template() {
return 'importTextarea';
},
instruction() {
return `import-board-instruction-${Session.get('importSource')}`;
},
importPlaceHolder() {
const importSource = Session.get('importSource');
if (importSource === 'csv') {
@ -147,37 +139,81 @@ Template.importTextarea.helpers({
return 'import-json-placeholder';
}
},
});
Template.importTextarea.events({
submit(evt, tpl) {
const importTpl = findParentTemplateInstance(tpl, 'import');
if (importTpl) {
return importTpl.importData(evt, Session.get('importSource'));
}
events() {
return [
{
submit(evt) {
return this.parentComponent().importData(
evt,
Session.get('importSource'),
);
},
},
];
},
});
}).register('importTextarea');
// Module-level reference so popup children can access importMapMembers methods
let _importMapMembersTpl = null;
BlazeComponent.extendComponent({
onCreated() {
this.usersLoaded = new ReactiveVar(false);
Template.importMapMembers.onCreated(function () {
_importMapMembersTpl = this;
this.usersLoaded = new ReactiveVar(false);
this.autorun(() => {
const handle = this.subscribe(
'user-miniprofile',
this.members().map(member => {
return member.username;
}),
);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
if (
handle.ready() &&
!this.usersLoaded.get() &&
this.members().length
) {
this._refreshMembers(
this.members().map(member => {
if (!member.wekanId) {
let user = ReactiveCache.getUser({ username: member.username });
if (!user) {
user = ReactiveCache.getUser({ importUsernames: member.username });
}
if (user) {
// eslint-disable-next-line no-console
// console.log('found username:', user.username);
member.wekanId = user._id;
}
}
return member;
}),
);
}
this.usersLoaded.set(handle.ready());
});
});
});
},
this.members = () => {
const importTpl = findParentTemplateInstance(this, 'import');
return importTpl ? importTpl.membersToMap.get() : [];
};
members() {
return this.parentComponent().membersToMap.get();
},
this._refreshMembers = (listOfMembers) => {
const importTpl = findParentTemplateInstance(this, 'import');
if (importTpl) {
importTpl.membersToMap.set(listOfMembers);
}
};
_refreshMembers(listOfMembers) {
return this.parentComponent().membersToMap.set(listOfMembers);
},
this._setPropertyForMember = (property, value, memberId, unset = false) => {
/**
* Will look into the list of members to import for the specified memberId,
* then set its property to the supplied value.
* If unset is true, it will remove the property from the rest of the list as well.
*
* use:
* - memberId = null to use selected member
* - value = null to unset a property
* - unset = true to ensure property is only set on 1 member at a time
*/
_setPropertyForMember(property, value, memberId, unset = false) {
const listOfMembers = this.members();
let finder = null;
if (memberId) {
@ -203,13 +239,17 @@ Template.importMapMembers.onCreated(function () {
});
// Session.get gives us a copy, we have to set it back so it sticks
this._refreshMembers(listOfMembers);
};
},
this.setSelectedMember = (memberId) => {
setSelectedMember(memberId) {
return this._setPropertyForMember('selected', true, memberId, true);
};
},
this.getMember = (memberId = null) => {
/**
* returns the member with specified id,
* or the selected member if memberId is not specified
*/
getMember(memberId = null) {
const allMembers = this.members();
let finder = null;
if (memberId) {
@ -218,154 +258,117 @@ Template.importMapMembers.onCreated(function () {
finder = user => user.selected;
}
return allMembers.find(finder);
};
},
this.mapSelectedMember = (wekanId) => {
mapSelectedMember(wekanId) {
return this._setPropertyForMember('wekanId', wekanId, null);
};
},
this.unmapMember = (memberId) => {
unmapMember(memberId) {
return this._setPropertyForMember('wekanId', null, memberId);
};
this.autorun(() => {
const handle = this.subscribe(
'user-miniprofile',
this.members().map(member => {
return member.username;
}),
);
Tracker.nonreactive(() => {
Tracker.autorun(() => {
if (
handle.ready() &&
!this.usersLoaded.get() &&
this.members().length
) {
this._refreshMembers(
this.members().map(member => {
if (!member.wekanId) {
let user = ReactiveCache.getUser({ username: member.username });
if (!user) {
user = ReactiveCache.getUser({ importUsernames: member.username });
}
if (user) {
member.wekanId = user._id;
}
}
return member;
}),
);
}
this.usersLoaded.set(handle.ready());
});
});
});
});
Template.importMapMembers.onDestroyed(function () {
if (_importMapMembersTpl === this) {
_importMapMembersTpl = null;
}
});
Template.importMapMembers.helpers({
usersLoaded() {
return Template.instance().usersLoaded;
},
members() {
return Template.instance().members();
},
});
Template.importMapMembers.events({
submit(evt, tpl) {
onSubmit(evt) {
evt.preventDefault();
const importTpl = findParentTemplateInstance(tpl, 'import');
if (importTpl) {
importTpl.nextStep();
}
this.parentComponent().nextStep();
},
'click .js-select-member'(evt, tpl) {
const memberToMap = Template.currentData();
if (memberToMap.wekan) {
// todo xxx ask for confirmation?
tpl.unmapMember(memberToMap.id);
} else {
tpl.setSelectedMember(memberToMap.id);
Popup.open('importMapMembersAdd')(evt);
}
events() {
return [
{
submit: this.onSubmit,
'click .js-select-member'(evt) {
const memberToMap = this.currentData();
if (memberToMap.wekan) {
// todo xxx ask for confirmation?
this.unmapMember(memberToMap.id);
} else {
this.setSelectedMember(memberToMap.id);
Popup.open('importMapMembersAdd')(evt);
}
},
},
];
},
});
}).register('importMapMembers');
BlazeComponent.extendComponent({
onRendered() {
this.find('.js-map-member input').focus();
},
onSelectUser() {
Popup.getOpenerComponent(5).mapSelectedMember(this.currentData().__originalId);
Popup.back();
},
events() {
return [
{
'click .js-select-import': this.onSelectUser,
},
];
},
}).register('importMapMembersAddPopup');
// Global reactive variables for import member popup
const importMemberPopupState = {
searching: new ReactiveVar(false),
searchResults: new ReactiveVar([]),
noResults: new ReactiveVar(false),
searchTimeout: null,
searchTimeout: null
};
Template.importMapMembersAddPopup.onCreated(function () {
this.searching = importMemberPopupState.searching;
this.searchResults = importMemberPopupState.searchResults;
this.noResults = importMemberPopupState.noResults;
this.searchTimeout = null;
this.searching.set(false);
this.searchResults.set([]);
this.noResults.set(false);
});
Template.importMapMembersAddPopup.onRendered(function () {
this.find('.js-search-member-input').focus();
});
Template.importMapMembersAddPopup.onDestroyed(function () {
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searching.set(false);
});
function importPerformSearch(tpl, query) {
if (!query || query.length < 2) {
tpl.searchResults.set([]);
tpl.noResults.set(false);
return;
}
tpl.searching.set(true);
tpl.noResults.set(false);
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
tpl.searchResults.set(results);
tpl.searching.set(false);
if (results.length === 0) {
tpl.noResults.set(true);
}
}
Template.importMapMembersAddPopup.events({
'click .js-select-import'(event, tpl) {
if (_importMapMembersTpl) {
_importMapMembersTpl.mapSelectedMember(Template.currentData().__originalId);
}
Popup.back();
BlazeComponent.extendComponent({
onCreated() {
// Use global state
this.searching = importMemberPopupState.searching;
this.searchResults = importMemberPopupState.searchResults;
this.noResults = importMemberPopupState.noResults;
this.searchTimeout = importMemberPopupState.searchTimeout;
},
'keyup .js-search-member-input'(event, tpl) {
const query = event.target.value.trim();
if (tpl.searchTimeout) {
clearTimeout(tpl.searchTimeout);
onRendered() {
this.find('.js-search-member-input').focus();
},
performSearch(query) {
if (!query || query.length < 2) {
this.searchResults.set([]);
this.noResults.set(false);
return;
}
tpl.searchTimeout = setTimeout(() => {
importPerformSearch(tpl, query);
}, 300);
this.searching.set(true);
this.noResults.set(false);
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
this.searchResults.set(results);
this.searching.set(false);
if (results.length === 0) {
this.noResults.set(true);
}
},
});
events() {
return [
{
'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);
},
},
];
},
}).register('importMapMembersAddPopupSearch');
Template.importMapMembersAddPopup.helpers({
searchResults() {
@ -376,5 +379,5 @@ Template.importMapMembersAddPopup.helpers({
},
noResults() {
return importMemberPopupState.noResults;
},
});
}
})

View file

@ -1,11 +0,0 @@
template(name="basicTabs")
.basicTabs-container(class="{{name}}")
ul.tabs-list
each tabs
li.tab-item(class="{{isActiveTab slug}} {{class}}") {{name}}
.tabs-content-container
+Template.contentBlock
template(name="tabContent")
section.tabs-content(class="{{isActiveTab slug}}" data-tab="{{slug}}")
+Template.contentBlock

View file

@ -1,50 +0,0 @@
const { ReactiveVar } = require('meteor/reactive-var');
Template.basicTabs.onCreated(function () {
const activeTab = this.data.activeTab
? { slug: this.data.activeTab }
: this.data.tabs[0];
this._activeTab = new ReactiveVar(activeTab);
this.isActiveSlug = (slug) => {
const current = this._activeTab.get();
return current && current.slug === slug;
};
});
Template.basicTabs.helpers({
isActiveTab(slug) {
if (Template.instance().isActiveSlug(slug)) {
return 'active';
}
},
});
Template.basicTabs.events({
'click .tab-item'(e, t) {
t._activeTab.set(this);
},
});
function findBasicTabsInstance() {
let view = Blaze.currentView;
while (view) {
if (view.name === 'Template.basicTabs' && view.templateInstance) {
const inst = view.templateInstance();
if (inst && inst.isActiveSlug) {
return inst;
}
}
view = view.parentView;
}
return null;
}
Template.tabContent.helpers({
isActiveTab(slug) {
const inst = findBasicTabsInstance();
if (inst && inst.isActiveSlug(slug)) {
return 'active';
}
},
});

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;
@ -437,8 +395,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 +403,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 +570,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;
@ -839,9 +750,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;
}
@ -1056,9 +964,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 +1035,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 +1124,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

@ -4,202 +4,196 @@ require('/client/lib/jquery-ui.js')
const { calculateIndex } = Utils;
Template.list.onCreated(function () {
this.newCardFormIsVisible = new ReactiveVar(true);
BlazeComponent.extendComponent({
// Proxy
openForm(options) {
this.childComponents('listBody')[0].openForm(options);
},
// Proxy - find the listBody child template instance via the DOM
this.openForm = (options) => {
const listBodyEl = this.find('.list-body');
const view = listBodyEl && Blaze.getView(listBodyEl, 'Template.listBody');
const listBodyInstance = view?.templateInstance?.();
if (listBodyInstance) listBodyInstance.openForm(options);
};
});
onCreated() {
this.newCardFormIsVisible = new ReactiveVar(true);
},
// The jquery UI sortable library is the best solution I've found so far. I
// tried sortable and dragula but they were not powerful enough four our use
// case. I also considered writing/forking a drag-and-drop + sortable library
// but it's probably too much work.
// By calling asking the sortable library to cancel its move on the `stop`
// callback, we basically solve all issues related to reactive updates. A
// comment below provides further details.
Template.list.onRendered(function () {
const boardBodyEl = this.firstNode?.parentElement?.closest?.('.board-body') ||
document.querySelector('.board-body');
const boardView = boardBodyEl && Blaze.getView(boardBodyEl, 'Template.boardBody');
const boardComponent = boardView?.templateInstance?.();
// The jquery UI sortable library is the best solution I've found so far. I
// tried sortable and dragula but they were not powerful enough four our use
// case. I also considered writing/forking a drag-and-drop + sortable library
// but it's probably too much work.
// By calling asking the sortable library to cancel its move on the `stop`
// callback, we basically solve all issues related to reactive updates. A
// comment below provides further details.
onRendered() {
const boardComponent = this.parentComponent().parentComponent();
// Initialize list resize functionality immediately
this.initializeListResize();
// Initialize list resize functionality immediately
this.initializeListResize();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
if (MultiSelection.isActive()) {
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) {
helper.append(
$(
Blaze.toHTML(
HTML.DIV(
{ class: 'and-n-other' },
TAPi18n.__('and-n-other-card', { count: andNOthers }),
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
if (MultiSelection.isActive()) {
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) {
helper.append(
$(
Blaze.toHTML(
HTML.DIV(
{ class: 'and-n-other' },
TAPi18n.__('and-n-other-card', { count: andNOthers }),
),
),
),
),
);
);
}
}
}
return helper;
},
distance: 7,
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
scrollSpeed: 10,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
if (boardComponent) boardComponent.setIsDragging(true);
},
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 prevCardDom = ui.item.prev('.js-minicard').get(0);
const nextCardDom = ui.item.next('.js-minicard').get(0);
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
const currentBoard = Utils.getCurrentBoard();
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
let targetSwimlaneId = null;
return helper;
},
distance: 7,
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
scrollSpeed: 10,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
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 prevCardDom = ui.item.prev('.js-minicard').get(0);
const nextCardDom = ui.item.next('.js-minicard').get(0);
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
const currentBoard = Utils.getCurrentBoard();
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
let targetSwimlaneId = null;
// only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
)
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
._id;
// only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
)
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
._id;
// 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.
$cards.sortable('cancel');
// 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.
$cards.sortable('cancel');
if (MultiSelection.isActive()) {
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
if (MultiSelection.isActive()) {
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(
currentBoard._id,
newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(
currentBoard._id,
newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
if (boardComponent) boardComponent.setIsDragging(false);
},
sort(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
sort(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
if (event.pageX < 10) { // scroll to the left
boardCanvas.scrollLeft -= 15;
ui.helper[0].offsetLeft -= 15;
}
if (
event.pageX > boardCanvas.offsetWidth - 10 &&
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
) { // scroll to the right
boardCanvas.scrollLeft += 15;
}
if (
event.pageY > boardCanvas.offsetHeight - 10 &&
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
) { // scroll to the bottom
boardCanvas.scrollTop += 15;
}
if (event.pageY < 10) { // scroll to the top
boardCanvas.scrollTop -= 15;
}
},
activate(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
},
});
this.autorun(() => {
if ($cards.data('uiSortable') || $cards.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member
!Utils.canModifyBoard(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !Utils.canModifyBoard(),
);
}
});
// We want to re-run this function any time a card is added.
this.autorun(() => {
const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard');
if (event.pageX < 10) { // scroll to the left
boardCanvas.scrollLeft -= 15;
ui.helper[0].offsetLeft -= 15;
}
if (
event.pageX > boardCanvas.offsetWidth - 10 &&
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
) { // scroll to the right
boardCanvas.scrollLeft += 15;
}
if (
event.pageY > boardCanvas.offsetHeight - 10 &&
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
) { // scroll to the bottom
boardCanvas.scrollTop += 15;
}
if (event.pageY < 10) { // scroll to the top
boardCanvas.scrollTop -= 15;
}
},
activate(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
},
});
Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
const card = ReactiveCache.getCard(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
card.addLabel(labelId);
}
},
this.autorun(() => {
if ($cards.data('uiSortable') || $cards.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member
!Utils.canModifyBoard(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !Utils.canModifyBoard(),
);
}
});
// We want to re-run this function any time a card is added.
this.autorun(() => {
const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard');
});
Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
const card = ReactiveCache.getCard(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
card.addLabel(labelId);
}
},
});
});
});
});
});
},
Template.list.helpers({
listWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
@ -260,16 +254,7 @@ Template.list.helpers({
return user.isAutoWidth(list.boardId);
},
collapsed() {
return Utils.getListCollapseState(this);
},
});
// initializeListResize as a method on the template instance
Template.list.onCreated(function () {
const tpl = this;
tpl.initializeListResize = function () {
initializeListResize() {
// Check if we're still in a valid template context
if (!Template.currentData()) {
console.warn('No current template data available for list resize initialization');
@ -277,73 +262,47 @@ Template.list.onCreated(function () {
}
const list = Template.currentData();
const $list = tpl.$('.js-list');
const $resizeHandle = tpl.$('.js-list-resize-handle');
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');
Meteor.setTimeout(() => {
if (!tpl.isDestroyed) {
tpl.initializeListResize();
if (!this.isDestroyed) {
this.initializeListResize();
}
}, 100);
return;
}
// Helper to get autoWidth state
const getAutoWidth = () => {
const user = ReactiveCache.getCurrentUser();
const listData = Template.currentData();
if (!user) return false;
return user.isAutoWidth(listData.boardId);
};
// Reactively show/hide resize handle based on collapse and auto-width state
tpl.autorun(() => {
const isAutoWidth = getAutoWidth();
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
// Get listConstraint value
const getListConstraint = () => {
const user = ReactiveCache.getCurrentUser();
const listData = Template.currentData();
if (!listData) return 550;
if (user) {
return user.getListConstraintFromStorage(listData.boardId, listData._id);
}
try {
const stored = localStorage.getItem('wekan-list-constraints');
if (stored) {
const constraints = JSON.parse(stored);
if (constraints[listData.boardId] && constraints[listData.boardId][listData._id]) {
return constraints[listData.boardId][listData._id];
}
}
} catch (e) {}
return 550;
};
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
const startResize = (e) => {
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');
@ -358,7 +317,7 @@ Template.list.onCreated(function () {
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`);
@ -370,6 +329,7 @@ Template.list.onCreated(function () {
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
e.preventDefault();
e.stopPropagation();
};
@ -382,8 +342,7 @@ Template.list.onCreated(function () {
// Calculate final width
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, startWidth + deltaX);
const listConstraint = getListConstraint();
const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Ensure the final width is applied
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
@ -400,10 +359,14 @@ Template.list.onCreated(function () {
$('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') {
}
@ -463,15 +426,16 @@ Template.list.onCreated(function () {
$(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
tpl.autorun(() => {
const collapsed = Utils.getListCollapseState(list);
if (getAutoWidth() || collapsed) {
// Reactively update resize handle visibility when auto-width changes
component.autorun(() => {
if (component.autoWidth()) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
@ -479,14 +443,14 @@ Template.list.onCreated(function () {
});
// Clean up on component destruction
tpl.view.onViewDestroyed(() => {
component.onDestroyed(() => {
$(document).off('mousemove', doResize);
$(document).off('mouseup', stopResize);
$(document).off('touchmove', doResize);
$(document).off('touchend', stopResize);
});
};
});
},
}).register('list');
Template.miniList.events({
'click .js-select-list'() {
@ -494,7 +458,3 @@ Template.miniList.events({
Session.set('currentList', listId);
},
});
// NOTE: Collapsed list drag-reorder was previously here but referenced
// boardComponent from an outer scope. If needed, this should be moved
// into Template.list.onRendered where boardComponent is available.

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}}

File diff suppressed because it is too large Load diff

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
@ -16,7 +16,7 @@ template(name="listHeader")
span.cardCount {{cardsCount}}
if isMiniScreen
h2.list-header-name(
title="{{ displayDate modifiedAt 'LLL' }}"
title="{{ moment modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
@ -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="{{ displayDate modifiedAt 'LLL' }}"
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
title="{{ moment modifiedAt 'LLL' }}"
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) {{ displayDate createdAt 'LLL' }}
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';
@ -8,13 +7,13 @@ Meteor.startup(() => {
listsColors = Lists.simpleSchema()._schema.color.allowedValues;
});
Template.listHeader.helpers({
BlazeComponent.extendComponent({
canSeeAddCard() {
const list = Template.currentData();
return (
(!list.getWipLimit('enabled') ||
list.getWipLimit('soft') ||
!Template.instance().reachedWipLimit()) &&
!this.reachedWipLimit()) &&
!ReactiveCache.getCurrentUser().isWorker()
);
},
@ -22,19 +21,41 @@ Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
starred() {
starred(check = undefined) {
const list = Template.currentData();
return list.isStarred();
const status = list.isStarred();
if (check === undefined) {
// just check
return status;
} else {
list.star(!status);
return !status;
}
},
collapsed() {
collapsed(check = undefined) {
const list = Template.currentData();
return Utils.getListCollapseState(list);
const status = list.isCollapsed();
if (check === undefined) {
// just check
return status;
} else {
list.collapse(!status);
return !status;
}
},
editTitle(event) {
event.preventDefault();
const newTitle = this.childComponents('inlinedForm')[0]
.getValue()
.trim();
const list = this.currentData();
if (newTitle) {
list.rename(newTitle.trim());
}
},
isWatching() {
const list = Template.currentData();
const list = this.currentData();
return list.findWatcher(Meteor.userId());
},
@ -50,9 +71,10 @@ Template.listHeader.helpers({
cardsCount() {
const list = Template.currentData();
let swimlaneId = '';
if (Utils.boardView() === 'board-view-swimlanes') {
swimlaneId = list.swimlaneId || '';
}
if (Utils.boardView() === 'board-view-swimlanes')
swimlaneId = this.parentComponent()
.parentComponent()
.data()._id;
const ret = list.cards(swimlaneId).length;
return ret;
@ -75,8 +97,7 @@ Template.listHeader.helpers({
},
showCardsCountForList(count) {
const currentUser = ReactiveCache.getCurrentUser();
const limit = currentUser ? currentUser.getLimitToShowCardsCount() : false;
const limit = this.limitToShowCardsCount();
return limit >= 0 && count >= limit;
},
@ -88,98 +109,40 @@ Template.listHeader.helpers({
}
},
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;
events() {
return [
{
'click .js-list-star'(event) {
event.preventDefault();
this.starred(!this.starred());
},
'click .js-collapse'(event) {
event.preventDefault();
this.collapsed(!this.collapsed());
},
'click .js-open-list-menu': Popup.open('listAction'),
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(event.target).parents(
`#js-list-${this.currentData()._id}`,
)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
listComponent.openForm({
position: 'top',
});
},
'click .js-unselect-list'() {
Session.set('currentList', null);
},
submit: this.editTitle,
},
];
},
}).register('listHeader');
hasNumberFieldsSum() {
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
return !!(fields && fields.length);
},
});
// Helper function on template instance for reachedWipLimit check
Template.listHeader.onCreated(function () {
this.reachedWipLimit = function () {
const list = Template.currentData();
return (
list.getWipLimit('enabled') &&
list.getWipLimit('value') <= list.cards().length
);
};
});
Template.listHeader.events({
async 'click .js-list-star'(event) {
event.preventDefault();
const list = Template.currentData();
const status = list.isStarred();
await list.star(!status);
},
'click .js-collapse'(event) {
event.preventDefault();
const list = Template.currentData();
const status = Utils.getListCollapseState(list);
Utils.setListCollapseState(list, !status);
},
'click .js-open-list-menu': Popup.open('listAction'),
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(event.target).parents(
`#js-list-${Template.currentData()._id}`,
)[0];
const view = Blaze.getView(listDom, 'Template.listBody');
const listComponent = view?.templateInstance?.();
if (listComponent) {
listComponent.openForm({
position: 'top',
});
}
},
'click .js-unselect-list'() {
Session.set('currentList', null);
},
async 'submit'(event, tpl) {
event.preventDefault();
const newTitle = tpl.$('textarea,input[type=text]').val()?.trim();
const list = Template.currentData();
if (newTitle) {
await list.rename(newTitle.trim());
}
},
Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
}
});
Template.listActionPopup.helpers({
@ -198,29 +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 view = Blaze.getView(listDom, 'Template.listBody');
const listComponent = view?.templateInstance?.();
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 view = Blaze.getView(listDom, 'Template.listBody');
const listComponent = view?.templateInstance?.();
if (listComponent) {
listComponent.openForm({
position: 'bottom',
});
}
const listComponent = BlazeComponent.getComponentForElement(listDom);
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'() {
@ -235,16 +183,59 @@ 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'),
'click .js-more': Popup.open('listMore'),
});
Template.setWipLimitPopup.helpers({
BlazeComponent.extendComponent({
applyWipLimit() {
const list = Template.currentData();
const limit = parseInt(
Template.instance()
.$('.wip-limit-value')
.val(),
10,
);
if (limit < list.cards().length && !list.getWipLimit('soft')) {
Template.instance()
.$('.wip-limit-error')
.click();
} else {
Meteor.call('applyWipLimit', list._id, limit);
Popup.back();
}
},
enableSoftLimit() {
const list = Template.currentData();
if (
list.getWipLimit('soft') &&
list.getWipLimit('value') < list.cards().length
) {
list.setWipLimit(list.cards().length);
}
Meteor.call('enableSoftLimit', Template.currentData()._id);
},
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
) {
list.setWipLimit(list.cards().length);
}
Meteor.call('enableWipLimit', list._id);
},
isWipLimitSoft() {
return Template.currentData().getWipLimit('soft');
},
@ -256,114 +247,125 @@ Template.setWipLimitPopup.helpers({
wipLimitValue() {
return Template.currentData().getWipLimit('value');
},
});
Template.setWipLimitPopup.events({
async 'click .js-enable-wip-limit'() {
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);
}
Meteor.call('enableWipLimit', list._id);
events() {
return [
{
'click .js-enable-wip-limit': this.enableWipLimit,
'click .wip-limit-apply': this.applyWipLimit,
'click .wip-limit-error': Popup.open('wipLimitError'),
'click .materialCheckBox': this.enableSoftLimit,
},
];
},
'click .wip-limit-apply'(event, tpl) {
const list = Template.currentData();
const limit = parseInt(
tpl.$('.wip-limit-value').val(),
10,
);
if (limit < list.cards().length && !list.getWipLimit('soft')) {
tpl.$('.wip-limit-error').click();
} else {
Meteor.call('applyWipLimit', list._id, limit);
Popup.back();
}
},
'click .wip-limit-error': Popup.open('wipLimitError'),
async 'click .materialCheckBox'() {
const list = Template.currentData();
if (
list.getWipLimit('soft') &&
list.getWipLimit('value') < list.cards().length
) {
await list.setWipLimit(list.cards().length);
}
Meteor.call('enableSoftLimit', Template.currentData()._id);
},
});
}).register('setWipLimitPopup');
Template.listMorePopup.events({
'click .js-delete': Popup.afterConfirm('listDelete', function() {
Popup.back();
const list = Lists.findOne(this._id);
if (!list) return;
const allCards = list.allCards();
const allCards = this.allCards();
const allCardIds = _.pluck(allCards, '_id');
// it's okay if the linked cards are on the same list
if (
ReactiveCache.getCards({
$and: [
{ listId: { $ne: list._id } },
{ listId: { $ne: this._id } },
{ linkedId: { $in: allCardIds } },
],
}).length === 0
) {
allCardIds.map(_id => Cards.remove(_id));
Lists.remove(list._id);
Lists.remove(this._id);
} else {
// TODO: Figure out more informative message.
// Popup with a hint that the list cannot be deleted as there are
// linked cards. We can adapt the query above so we can list the linked
// cards.
// Related:
// client/components/cards/cardDetails.js about line 969
// https://github.com/wekan/wekan/issues/2785
const message = `${TAPi18n.__(
'delete-linked-cards-before-this-list',
)} linkedId: ${
list._id
this._id
} at client/components/lists/listHeader.js and https://github.com/wekan/wekan/issues/2785`;
alert(message);
}
Utils.goBoardId(list.boardId);
Utils.goBoardId(this.boardId);
}),
});
Template.setListColorPopup.onCreated(function () {
const data = Template.currentData();
this.currentList = Lists.findOne(data._id) || data;
this.currentColor = new ReactiveVar(this.currentList.color);
Template.listHeader.helpers({
isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
},
});
Template.setListColorPopup.helpers({
BlazeComponent.extendComponent({
onCreated() {
this.currentList = this.currentData();
this.currentColor = new ReactiveVar(this.currentList.color);
},
colors() {
return listsColors.map(color => ({ color, name: '' }));
},
isSelected(color) {
const tpl = Template.instance();
if (tpl.currentColor.get() === null) {
if (this.currentColor.get() === null) {
return color === 'white';
} else {
return tpl.currentColor.get() === color;
return this.currentColor.get() === color;
}
},
});
Template.setListColorPopup.events({
'click .js-palette-color'(event, tpl) {
tpl.currentColor.set(Template.currentData().color);
events() {
return [
{
'click .js-palette-color'() {
this.currentColor.set(this.currentData().color);
},
'click .js-submit'() {
this.currentList.setColor(this.currentColor.get());
Popup.close();
},
'click .js-remove-color'() {
this.currentList.setColor(null);
Popup.close();
},
},
];
},
}).register('setListColorPopup');
BlazeComponent.extendComponent({
applyListWidth() {
const list = Template.currentData();
const board = list.boardId;
const width = parseInt(
Template.instance()
.$('.list-width-value')
.val(),
10,
);
const constraint = parseInt(
Template.instance()
.$('.list-constraint-value')
.val(),
10,
);
// FIXME(mark-i-m): where do we put constants?
if (width < 100 || !width || constraint < 100 || !constraint) {
Template.instance()
.$('.list-width-error')
.click();
} else {
Meteor.call('applyListWidth', board, list._id, width, constraint);
Popup.back();
}
},
async 'click .js-submit'(event, tpl) {
await tpl.currentList.setColor(tpl.currentColor.get());
Popup.close();
},
async 'click .js-remove-color'(event, tpl) {
await tpl.currentList.setColor(null);
Popup.close();
},
});
Template.setListWidthPopup.helpers({
listWidthValue() {
const list = Template.currentData();
const board = list.boardId;
@ -381,147 +383,17 @@ Template.setListWidthPopup.helpers({
const user = ReactiveCache.getCurrentUser();
return user && user.isAutoWidth(boardId);
},
});
Template.setListWidthPopup.events({
'click .js-auto-width-board'() {
dragscroll.reset();
ReactiveCache.getCurrentUser().toggleAutoWidth(Utils.getCurrentBoardId());
events() {
return [
{
'click .js-auto-width-board'() {
dragscroll.reset();
ReactiveCache.getCurrentUser().toggleAutoWidth(Utils.getCurrentBoardId());
},
'click .list-width-apply': this.applyListWidth,
'click .list-width-error': Popup.open('listWidthError'),
},
];
},
'click .list-width-apply'(event, tpl) {
const list = Template.currentData();
const board = list.boardId;
const width = parseInt(
tpl.$('.list-width-value').val(),
10,
);
const constraint = parseInt(
tpl.$('.list-constraint-value').val(),
10,
);
// FIXME(mark-i-m): where do we put constants?
if (width < 270 || !width || constraint < 270 || !constraint) {
tpl.$('.list-width-error').click();
} else {
Meteor.call('applyListWidth', board, list._id, width, constraint);
Popup.back();
}
},
'click .list-width-error': Popup.open('listWidthError'),
});
Template.addListPopup.onCreated(function () {
this.currentBoard = Utils.getCurrentBoard();
this.currentSwimlaneId = new ReactiveVar(null);
this.currentListId = new ReactiveVar(null);
// Get the swimlane context from opener
const openerComponent = Popup.getOpenerComponent();
const openerData = openerComponent?.data || Popup._getTopStack()?.dataContext;
// 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 });
if (list) {
this.currentSwimlane = list.swimlaneId
? ReactiveCache.getSwimlane({ _id: list.swimlaneId })
: null;
this.currentSwimlaneId.set(this.currentSwimlane?._id || null);
this.currentListId.set(openerData._id);
}
}
if (!this.currentSwimlaneId.get()) {
const defaultSwimlane = this.currentBoard.getDefaultSwimline?.();
if (defaultSwimlane?._id) {
this.currentSwimlane = defaultSwimlane;
this.currentSwimlaneId.set(defaultSwimlane._id);
}
}
});
Template.addListPopup.helpers({
currentSwimlaneData() {
const tpl = Template.instance();
const swimlaneId = tpl.currentSwimlaneId.get();
return swimlaneId ? ReactiveCache.getSwimlane({ _id: swimlaneId }) : null;
},
currentListIdValue() {
return Template.instance().currentListId.get();
},
swimlaneLists() {
const tpl = Template.instance();
const swimlaneId = tpl.currentSwimlaneId.get();
if (swimlaneId) {
return ReactiveCache.getLists({ swimlaneId, archived: false }).sort((a, b) => a.sort - b.sort);
}
return tpl.currentBoard.lists;
},
});
Template.addListPopup.events({
'submit .js-add-list-form'(evt, tpl) {
evt.preventDefault();
const titleInput = tpl.find('.list-name-input');
const title = titleInput?.value.trim();
if (!title) return;
let sortIndex = 0;
const boardId = Utils.getCurrentBoardId();
let swimlaneId = tpl.currentSwimlane?._id;
const positionInput = tpl.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 = tpl.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 = tpl.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('setListWidthPopup');

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

@ -18,19 +18,21 @@ const accessibilityHelpers = {
};
// Main accessibility page component
Template.accessibility.onCreated(function () {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
Meteor.subscribe('setting');
Meteor.subscribe('accessibilitySettings');
});
Template.accessibility.helpers(accessibilityHelpers);
Meteor.subscribe('setting');
Meteor.subscribe('accessibilitySettings');
},
...accessibilityHelpers
}).register('accessibility');
// Header bar component
Template.accessibilityHeaderBar.onCreated(function () {
Meteor.subscribe('accessibilitySettings');
});
Template.accessibilityHeaderBar.helpers(accessibilityHelpers);
BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('accessibilitySettings');
},
...accessibilityHelpers
}).register('accessibilityHeaderBar');

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,57 +1,18 @@
import { CardSearchPaged } from '../../lib/cardSearch';
import { CardSearchPagedComponent } from '../../lib/cardSearch';
Template.brokenCards.onCreated(function () {
const search = new CardSearchPaged(this);
this.search = search;
Meteor.subscribe('brokenCards', search.sessionId);
});
BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar');
Template.brokenCards.helpers({
userId() {
return Meteor.userId();
},
// Return ReactiveVars so jade can use .get pattern
searching() {
return Template.instance().search.searching;
},
hasResults() {
return Template.instance().search.hasResults;
},
hasQueryErrors() {
return Template.instance().search.hasQueryErrors;
},
errorMessages() {
return Template.instance().search.queryErrorMessages();
},
resultsCount() {
return Template.instance().search.resultsCount;
},
resultsHeading() {
return Template.instance().search.resultsHeading;
},
results() {
return Template.instance().search.results;
},
getSearchHref() {
return Template.instance().search.getSearchHref();
},
hasPreviousPage() {
return Template.instance().search.hasPreviousPage;
},
hasNextPage() {
return Template.instance().search.hasNextPage;
},
});
Template.brokenCards.events({
'click .js-next-page'(evt, tpl) {
evt.preventDefault();
tpl.search.nextPage();
},
'click .js-previous-page'(evt, tpl) {
evt.preventDefault();
tpl.search.previousPage();
},
});
class BrokenCardsComponent extends CardSearchPagedComponent {
onCreated() {
super.onCreated();
Meteor.subscribe('brokenCards', this.sessionId);
}
}
BrokenCardsComponent.register('brokenCards');

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