Compare commits

..

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

556 changed files with 18798 additions and 61998 deletions

View file

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

1
.github/FUNDING.yml vendored
View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com 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] [o:wekan:p:wekan:r:application]
file_filter = imports/i18n/data/<lang>.i18n.json file_filter = imports/i18n/data/<lang>.i18n.json

View file

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

View file

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

View file

@ -4,23 +4,28 @@ LABEL org.opencontainers.image.ref.name="ubuntu"
LABEL org.opencontainers.image.version="24.04" LABEL org.opencontainers.image.version="24.04"
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan" LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH is automatically provided by Docker Buildx # 2022-04-25:
ARG TARGETARCH # - 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 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 \ ENV \
DEBUG=false \ DEBUG=false \
NODE_VERSION=v14.21.4 \ NODE_VERSION=v14.21.4 \
METEOR_RELEASE=METEOR@2.16 \ METEOR_RELEASE=METEOR@2.14 \
USE_EDGE=false \ USE_EDGE=false \
METEOR_EDGE=1.5-beta.17 \ METEOR_EDGE=1.5-beta.17 \
NPM_VERSION=6.14.17 \ NPM_VERSION=6.14.17 \
FIBERS_VERSION=4.0.1 \ FIBERS_VERSION=4.0.1 \
ARCHITECTURE=linux-x64 \
SRC_PATH=./ \ SRC_PATH=./ \
WITH_API=true \ WITH_API=true \
MONGO_OPLOG_URL="" \
RESULTS_PER_PAGE="" \ RESULTS_PER_PAGE="" \
DEFAULT_BOARD_ID="" \ DEFAULT_BOARD_ID="" \
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \ ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
@ -158,69 +163,134 @@ ENV \
MONGO_PASSWORD_FILE="" \ MONGO_PASSWORD_FILE="" \
S3_SECRET_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 RUN <<EOR
set -o xtrace set -o xtrace
# Create Wekan user # Add non-root user wekan
useradd --user-group --system --home-dir /home/wekan wekan useradd --user-group --system --home-dir /home/wekan wekan
# OS dependencies
# OS Updates
apt-get update --assume-yes apt-get update --assume-yes
apt-get upgrade --assume-yes
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS} apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
# Multi-arch mapping logic # Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
case "${TARGETARCH}" in # https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
"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
cp $(which tar) $(which tar)~ cp $(which tar) $(which tar)~
ln -sf $(which bsdtar) $(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 mkdir -p /home/wekan/app
cd /home/wekan/app cd /home/wekan/app
wget "https://github.com/wekan/wekan/releases/download/v8.31/wekan-8.31-${WEKAN_ARCH}.zip" #mkdir -p /home/wekan/.npm
unzip "wekan-8.31-${WEKAN_ARCH}.zip" #chown --recursive wekan:wekan /home/wekan/.npm
rm "wekan-8.31-${WEKAN_ARCH}.zip" #chmod u+w *.json
#gosu wekan:wekan meteor npm install --production
#gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build
#cd /home/wekan/app_build/bundle/programs/server/
#chmod u+w *.json
#gosu wekan:wekan meteor npm install --production
#cd node_modules/fibers
#node build.js
#cd ../..
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
#mv /home/wekan/app_build/bundle /build
wget "https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64.zip"
unzip wekan-8.14-amd64.zip
rm wekan-8.14-amd64.zip
mv /home/wekan/app/bundle /build mv /home/wekan/app/bundle /build
# Restore original tar # Put back the original tar
mv $(which tar)~ $(which tar) mv $(which tar)~ $(which tar)
# Cleanup # Cleanup
apt-get remove --purge --assume-yes ${BUILD_DEPS} apt-get remove --purge --assume-yes ${BUILD_DEPS}
#npm uninstall -g api2html
apt-get autoremove --assume-yes apt-get autoremove --assume-yes
apt-get clean --assume-yes apt-get clean --assume-yes
rm -Rf /tmp/* rm -Rf /tmp/*
rm -Rf /var/lib/apt/lists/* 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/app
rm -Rf /home/wekan/.meteor
mkdir -p /data mkdir /data
chown wekan:wekan --recursive /data chown wekan --recursive /data
EOR EOR
USER wekan USER wekan
ENV PORT=8080 ENV PORT=8080
EXPOSE $PORT EXPOSE $PORT
STOPSIGNAL SIGKILL
WORKDIR /build
CMD ["bash", "-c", "ulimit -s 65500; exec node main.js"] STOPSIGNAL SIGKILL
WORKDIR /home/wekan/app
#---------------------------------------------------------------------
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
# Add more Node heap:
# NODE_OPTIONS="--max_old_space_size=4096"
# Add more stack:
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
#---------------------------------------------------------------------
#
# CMD ["node", "/build/main.js"]
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 /build/main.js"]
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /build/main.js"]
CMD ["bash", "-c", "ulimit -s 65500; exec node /build/main.js"]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -231,30 +231,6 @@
font-size: 1em !important; /* Keep original icon size */ 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 */ /* Ensure scrollbars are positioned correctly */
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical { #content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
width: 12px; width: 12px;
@ -287,106 +263,63 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
animation: fadeIn 0.2s; animation: fadeIn 0.2s;
z-index: 16; 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 .open-minicard-composer,
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked { .board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
display: none; display: none;
} }
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */ /* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-wrapper.mobile-view { .board-wrapper.mobile-view {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper.mobile-view .board-canvas { .board-wrapper.mobile-view .board-canvas {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane { .board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important; display: flex;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden !important; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
width: 100vw !important; width: 100%;
max-width: 100vw !important; min-width: 100%;
min-width: 100vw !important;
} }
@media screen and (max-width: 800px), @media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) { screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.board-wrapper { .board-wrapper {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper .board-canvas { .board-wrapper .board-canvas {
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; min-width: 100% !important;
min-width: 100vw !important;
left: 0 !important; left: 0 !important;
right: 0 !important; right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
} }
.board-wrapper .board-canvas .swimlane { .board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
display: block !important; display: flex;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden !important; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
width: 100vw !important; width: 100%;
max-width: 100vw !important; min-width: 100%;
min-width: 100vw !important;
} }
} }
.calendar-event-green { .calendar-event-green {

View file

@ -1,6 +1,8 @@
template(name="board") template(name="board")
if isConverting.get if isMigrating.get
+migrationProgress
else if isConverting.get
+boardConversionProgress +boardConversionProgress
else if isBoardReady.get else if isBoardReady.get
if currentBoard if currentBoard
@ -22,7 +24,7 @@ template(name="boardBody")
// Debug information (remove in production) // Debug information (remove in production)
if debugBoardState 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;") .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-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes( .board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}" class="{{#if hasSwimlanes}}dragscroll{{/if}}"
@ -47,8 +49,6 @@ template(name="boardBody")
+listsGroup(currentBoard) +listsGroup(currentBoard)
else if isViewCalendar else if isViewCalendar
+calendarView +calendarView
else if isViewGantt
+ganttView
else else
// Default view - show swimlanes if they exist, otherwise show lists // Default view - show swimlanes if they exist, otherwise show lists
if hasSwimlanes if hasSwimlanes
@ -56,10 +56,6 @@ template(name="boardBody")
+swimlane(this) +swimlane(this)
else else
+listsGroup(currentBoard) +listsGroup(currentBoard)
//- Render multiple open cards in desktop mode
unless isMiniScreen
each openCards
+cardDetails(this cardIndex=@index)
+sidebar +sidebar
template(name="calendarView") template(name="calendarView")

View file

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

View file

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

View file

@ -505,18 +505,18 @@
flex-wrap: nowrap !important; flex-wrap: nowrap !important;
align-items: stretch !important; align-items: stretch !important;
justify-content: flex-start !important; justify-content: flex-start !important;
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; max-width: 100% !important;
min-width: 100vw !important; min-width: 100% !important;
overflow-x: hidden !important; overflow-x: hidden !important;
overflow-y: auto !important; overflow-y: auto !important;
} }
.mobile-mode .swimlane { .mobile-mode .swimlane {
display: block !important; display: block !important;
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; max-width: 100% !important;
min-width: 100vw !important; min-width: 100% !important;
margin: 0 0 2rem 0 !important; margin: 0 0 2rem 0 !important;
padding: 0 !important; padding: 0 !important;
float: none !important; float: none !important;
@ -525,9 +525,9 @@
.mobile-mode .swimlane .swimlane-header { .mobile-mode .swimlane .swimlane-header {
display: block !important; display: block !important;
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; max-width: 100% !important;
min-width: 100vw !important; min-width: 100% !important;
margin: 0 0 1rem 0 !important; margin: 0 0 1rem 0 !important;
padding: 1rem !important; padding: 1rem !important;
font-size: clamp(18px, 2.5vw, 32px) !important; font-size: clamp(18px, 2.5vw, 32px) !important;
@ -537,9 +537,9 @@
.mobile-mode .swimlane .lists { .mobile-mode .swimlane .lists {
display: block !important; display: block !important;
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; max-width: 100% !important;
min-width: 100vw !important; min-width: 100% !important;
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
flex-direction: column !important; flex-direction: column !important;
@ -550,9 +550,9 @@
.mobile-mode .list { .mobile-mode .list {
display: block !important; display: block !important;
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; max-width: 100% !important;
min-width: 100vw !important; min-width: 100% !important;
margin: 0 0 2rem 0 !important; margin: 0 0 2rem 0 !important;
padding: 0 !important; padding: 0 !important;
float: none !important; float: none !important;
@ -667,9 +667,9 @@
flex-wrap: nowrap !important; flex-wrap: nowrap !important;
align-items: stretch !important; align-items: stretch !important;
justify-content: flex-start !important; justify-content: flex-start !important;
width: 100vw !important; width: 100% !important;
max-width: 100vw !important; max-width: 100% !important;
min-width: 100vw !important; min-width: 100% !important;
overflow-x: hidden !important; overflow-x: hidden !important;
overflow-y: auto !important; overflow-y: auto !important;
} }

View file

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

View file

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

View file

@ -8,273 +8,6 @@
padding: 1vh 0; 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 { .zoom-controls {
display: flex; display: flex;
align-items: center; align-items: center;
@ -373,35 +106,23 @@
.board-list li.starred .is-star-active, .board-list li.starred .is-star-active,
.board-list li.starred .is-not-star-active { .board-list li.starred .is-not-star-active {
opacity: 1; 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 { .board-list .board-list-item {
overflow: hidden; overflow: hidden;
background-color: inherit; /* Inherit board color from parent li.js-board */ background-color: #999;
color: #f6f6f6; color: #f6f6f6;
min-height: 100px; min-height: 100px;
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
border-radius: 0; /* No border-radius - parent .js-board has it */ border-radius: 3px;
display: block; display: block;
font-weight: 700; font-weight: 700;
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */ padding: 8px;
margin: 0; /* No margin - moved to parent .js-board */ margin: 8px;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
word-wrap: break-word; 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 { .board-list .board-list-item.template-container {
border: 4px solid #fff; border: 4px solid #fff;
} }
@ -429,27 +150,13 @@
.board-list .js-add-board .label { .board-list .js-add-board .label {
font-weight: normal; font-weight: normal;
line-height: 56px; 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 { .board-list .js-add-board :hover {
color: #fff; /* White icon */ background-color: #939393;
}
.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 .is-star-active, .board-list .is-star-active,
.board-list .is-not-star-active { .board-list .is-not-star-active {
top: 0; bottom: 0;
font-size: 14px; font-size: 14px;
height: 18px; height: 18px;
line-height: 18px; line-height: 18px;
@ -457,6 +164,7 @@
padding: 9px 9px; padding: 9px 9px;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0;
transition-duration: 0.15s; transition-duration: 0.15s;
transition-property: color, font-size, background; transition-property: color, font-size, background;
} }
@ -530,107 +238,6 @@
.board-list li:hover a .is-not-star-active { .board-list li:hover a .is-not-star-active {
opacity: 1; 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 { .board-backgrounds-list .board-background-select {
box-sizing: border-box; box-sizing: border-box;
display: block; 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 { .board-backgrounds-list .board-background-select .background-box i.fa-check {
font-size: 25px; 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.) */ /* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-list.mobile-view { .board-list.mobile-view {
height: calc(100vh - 120px); height: calc(100vh - 120px);
@ -1143,62 +739,9 @@ body.grey-icons-enabled .checkmark-no-grey {
#resetBtn { #resetBtn {
display: inline; 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 { .js-board {
display: block; 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 { .minicard-members {
padding: 6px 0 6px 8px; padding: 6px 0 6px 8px;
width: 100%; width: 100%;

View file

@ -2,41 +2,6 @@ template(name="boardList")
.wrapper .wrapper
.board-list-header .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)
// Existing filter by orgs/teams (kept)
ul.AllBoardTeamsOrgs ul.AllBoardTeamsOrgs
li.AllBoardTeams li.AllBoardTeams
if userHasTeams if userHasTeams
@ -52,117 +17,79 @@ template(name="boardList")
each orgsDatas each orgsDatas
option(value="{{orgId}}") {{orgDisplayName}} 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 li.AllBoardBtns
div.AllBoardButtonsContainer div.AllBoardButtonsContainer
if userHasOrgsOrTeams if userHasOrgsOrTeams
span.emoji-icon i.fa.fa-filter
i.fa.fa-search
input#filterBtn(type="button" value="{{_ 'filter'}}") input#filterBtn(type="button" value="{{_ 'filter'}}")
button#resetBtn.filter-reset-btn input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
span.reset-icon
span.emoji-icon
i.fa.fa-times-thin
span {{_ 'filter-clear'}}
// Right boards grid ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
.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 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'}}
else
a.board-list-item.label(title="{{_ 'add-board'}}") a.board-list-item.label(title="{{_ 'add-board'}}")
span.emoji-icon | {{_ 'add-board'}}
i.fa.fa-plus
| &nbsp;{{_ 'add-board'}}
each boards each boards
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true") li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
if isInvited if isInvited
.board-list-item .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.details
span.board-list-item-name= title span.board-list-item-name= title
span.js-star-board( i.fa.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}" class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
title="{{_ 'star-board-title'}}") title="{{_ 'star-board-title'}}")
span.emoji-icon
| {{#if isStarred}}⭐{{else}}☆{{/if}}
p.board-list-item-desc {{_ 'just-invited'}} p.board-list-item-desc {{_ 'just-invited'}}
button.js-accept-invite.primary {{_ 'accept'}} button.js-accept-invite.primary {{_ 'accept'}}
button.js-decline-invite {{_ 'decline'}} button.js-decline-invite {{_ 'decline'}}
else else
if $eq type "template-container" if $eq type "template-container"
.template-container.board-list-item a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
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.details
span.board-list-item-name(title="{{_ 'template-container'}}") span.board-list-item-name(title="{{_ 'template-container'}}")
+viewer +viewer
= title = 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 p.board-list-item-desc
+viewer +viewer
= description = description
if hasSpentTimeCards if hasSpentTimeCards
span.js-has-spenttime-cards( i.fa.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}" 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}}") title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
span.emoji-icon i.fa.board-handle(
i.fa.fa-clock-o class="fa-arrows"
span.js-star-board( title="{{_ 'drag-board'}}")
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}" if isSandstorm
title="{{_ 'star-board-title'}}") i.fa.js-clone-board(
span.emoji-icon class="fa-clone"
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") 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 else
.board-list-item a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
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.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}") span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+viewer +viewer
@ -179,61 +106,54 @@ template(name="boardList")
each list in boardLists _id each list in boardLists _id
.item .item
| {{ list }} | {{ 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 p.board-list-item-desc
+viewer +viewer
= description = description
if hasSpentTimeCards if hasSpentTimeCards
span.js-has-spenttime-cards( i.fa.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}" 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}}") title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
span.emoji-icon i.fa.board-handle(
i.fa.fa-clock-o class="fa-arrows"
a.js-star-board( title="{{_ 'drag-board'}}")
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}" if isSandstorm
title="{{_ 'star-board-title'}}") a.js-clone-board(
span.emoji-icon class="fa-clone"
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") 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") template(name="boardListHeaderBar")
h1 {{_ title }} h1 {{_ title }}
//.board-header-btns.right //.board-header-btns.right
// // a.board-header-btn.js-open-archived-board
a.board-header-btn.js-open-archived-board // i.fa.fa-archive
// // span {{_ 'archives'}}
i.fa.fa-archive // a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
// // i.fa.fa-clone
span {{_ 'archives'}} // span {{_ 'templates'}}
//
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)

View file

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

View file

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

View file

@ -55,12 +55,6 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
.attachment-actions a {
margin-left: 16px;
}
.attachment-actions a:first-child {
margin-left: 0;
}
.add-attachment { .add-attachment {
display: flex; display: flex;
align-items: center; align-items: center;
@ -112,9 +106,6 @@
color: white; color: white;
cursor: pointer; cursor: pointer;
font-size: 4em; font-size: 4em;
position: absolute;
right: 50px;
top: 16px;
} }
/* Upload progress indicators for drag-and-drop uploads */ /* Upload progress indicators for drag-and-drop uploads */
@ -250,6 +241,10 @@
.js-card-details.is-dragging-over { .js-card-details.is-dragging-over {
border: 2px dashed #007bff !important; border: 2px dashed #007bff !important;
background: rgba(0, 123, 255, 0.05) !important; background: rgba(0, 123, 255, 0.05) !important;
}
top: 0;
right: 8px;
position: absolute;
} }
.attachment-arrow { .attachment-arrow {
font-size: 4em; font-size: 4em;
@ -258,20 +253,6 @@
align-self: center; align-self: center;
margin: 0 20px; 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 { #viewer-content {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -285,13 +266,6 @@
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
} }
#video-viewer {
max-width: 100%;
max-height: 100%;
}
#audio-viewer {
max-width: 100%;
}
#pdf-viewer { #pdf-viewer {
width: 40vw; width: 40vw;
height: 100%; height: 100%;
@ -326,19 +300,9 @@
} }
#prev-attachment { #prev-attachment {
left: 0; left: 0;
position: absolute;
bottom: 2.2em;
font-size: 1.6em;
padding: 16px;
margin-left: 0;
} }
#next-attachment { #next-attachment {
right: 0; right: 0;
position: absolute;
bottom: 2.2em;
font-size: 1.6em;
padding: 16px;
margin-right: 0;
} }
#pdf-viewer { #pdf-viewer {
width: 100%; width: 100%;
@ -372,3 +336,36 @@
margin-top: 10px; 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-overlay.hidden
#viewer-top-bar #viewer-top-bar
span#attachment-name span#attachment-name
a#viewer-close a#viewer-close ❌
i.fa.fa-times-thin
#viewer-container #viewer-container
i.fa.fa-caret-left#prev-attachment | ◀️
#viewer-content #viewer-content
img#image-viewer.hidden img#image-viewer.hidden
video#video-viewer.hidden(controls="true") video#video-viewer.hidden(controls="true")
@ -46,7 +45,7 @@ template(name="attachmentViewer")
object#pdf-viewer.hidden(type="application/pdf") object#pdf-viewer.hidden(type="application/pdf")
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }} span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
object#txt-viewer.hidden(type="text/plain") object#txt-viewer.hidden(type="text/plain")
i.fa.fa-caret-right#next-attachment | ▶️
template(name="attachmentGallery") template(name="attachmentGallery")
@ -54,7 +53,7 @@ template(name="attachmentGallery")
if canModifyCard if canModifyCard
a.attachment-item.add-attachment.js-add-attachment a.attachment-item.add-attachment.js-add-attachment
i.fa.fa-plus |
each attachments each attachments
@ -88,21 +87,22 @@ template(name="attachmentGallery")
span.file-size ({{fileSize size}}) span.file-size ({{fileSize size}})
.attachment-actions .attachment-actions
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}") a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
i.fa.fa-arrow-down | ⬇️
if currentUser.isBoardMember if currentUser.isBoardMember
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
unless currentUser.isWorker unless currentUser.isWorker
a.js-rename(title="{{_ 'rename'}}") a.js-rename(title="{{_ 'rename'}}")
i.fa.fa-pencil-square-o | ✏️
a.js-confirm-delete(title="{{_ 'delete'}}") a.js-confirm-delete(title="{{_ 'delete'}}")
i.fa.fa-trash | 🗑️
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}") a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
i.fa.fa-bars | ☰
// Migration spinner overlay // Migration spinner overlay
if isAttachmentMigrating _id if isAttachmentMigrating _id
.attachment-migration-overlay .attachment-migration-overlay
.migration-spinner .migration-spinner
i.fa.fa-cog.fa-spin | ⚙️
.migration-text {{_ 'migrating-attachment'}} .migration-text {{_ 'migrating-attachment'}}
template(name="attachmentActionsPopup") template(name="attachmentActionsPopup")
@ -110,12 +110,16 @@ template(name="attachmentActionsPopup")
li li
if isImage if isImage
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}") 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 currentUser.isBoardAdmin
if isImage if isImage
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}") a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
i.fa.fa-picture-o | 🖼️
if isBackgroundImage if isBackgroundImage
| {{_ 'remove-background-image'}} | {{_ 'remove-background-image'}}
else else
@ -123,19 +127,19 @@ template(name="attachmentActionsPopup")
if $neq versions.original.storage "fs" if $neq versions.original.storage "fs"
a.js-move-storage-fs a.js-move-storage-fs
i.fa.fa-arrow-right | ▶️
| {{_ 'attachment-move-storage-fs'}} | {{_ 'attachment-move-storage-fs'}}
if $neq versions.original.storage "gridfs" if $neq versions.original.storage "gridfs"
if versions.original.storage if versions.original.storage
a.js-move-storage-gridfs a.js-move-storage-gridfs
i.fa.fa-arrow-right | ▶️
| {{_ 'attachment-move-storage-gridfs'}} | {{_ 'attachment-move-storage-gridfs'}}
if $neq versions.original.storage "s3" if $neq versions.original.storage "s3"
if versions.original.storage if versions.original.storage
a.js-move-storage-s3 a.js-move-storage-s3
i.fa.fa-arrow-right | ▶️
| {{_ 'attachment-move-storage-s3'}} | {{_ 'attachment-move-storage-s3'}}
template(name="attachmentRenamePopup") template(name="attachmentRenamePopup")

View file

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

View file

@ -112,7 +112,6 @@ CardCustomField.register('cardCustomField');
events() { events() {
return [ return [
{ {
'click .js-checklist-item .check-box-unicode': this.toggleItem,
'click .js-checklist-item .check-box-container': this.toggleItem, 'click .js-checklist-item .check-box-container': this.toggleItem,
}, },
]; ];

View file

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

View file

@ -47,6 +47,11 @@ import {
this.data().getStart() && this.date.set(new Date(this.data().getStart())); this.data().getStart() && this.date.set(new Date(this.data().getStart()));
} }
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) { _storeDate(date) {
this.card.setStart(formatDateTime(date)); this.card.setStart(formatDateTime(date));
} }
@ -63,6 +68,11 @@ import {
this.data().getDue() && this.date.set(new Date(this.data().getDue())); this.data().getDue() && this.date.set(new Date(this.data().getDue()));
} }
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) { _storeDate(date) {
this.card.setDue(formatDateTime(date)); this.card.setDue(formatDateTime(date));
} }
@ -79,6 +89,11 @@ import {
this.data().getEnd() && this.date.set(new Date(this.data().getEnd())); this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
} }
onRendered() {
super.onRendered();
// DatePicker base class handles initialization with native HTML inputs
}
_storeDate(date) { _storeDate(date) {
this.card.setEnd(formatDateTime(date)); this.card.setEnd(formatDateTime(date));
} }

View file

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

View file

@ -1,25 +1,23 @@
/* Date Format Selector */ /* Date Format Selector */
.card-details-item-date-format { .card-details-item-date-format {
margin-bottom: 12px; margin-bottom: 10px;
} }
.card-details-item-date-format .card-details-item-title { .card-details-item-date-format .card-details-item-title {
font-size: 15px; font-size: 14px;
font-weight: bold; font-weight: bold;
margin-bottom: 6px; margin-bottom: 5px;
color: #333; color: #333;
letter-spacing: 0.03em;
} }
.card-details-item-date-format .js-date-format-selector { .card-details-item-date-format .js-date-format-selector {
width: 100%; width: 100%;
padding: 9px 10px; padding: 8px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 5px; border-radius: 4px;
background-color: #fff; background-color: #fff;
font-size: 15px; font-size: 14px;
cursor: pointer; cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
} }
.card-details-item-date-format .js-date-format-selector:focus { .card-details-item-date-format .js-date-format-selector:focus {
@ -29,18 +27,18 @@
} }
.assignee { .assignee {
border-radius: 3px;
display: block; display: block;
position: relative; position: relative;
float: left; float: left;
height: clamp(24px, 3.5vw, 36px); height: 30px;
width: clamp(24px, 3.5vw, 36px); width: 30px;
margin: 0.3vh; margin: .3vh;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
z-index: 1; z-index: 1;
text-decoration: none; text-decoration: none;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.04);
} }
.assignee .avatar { .assignee .avatar {
overflow: hidden; overflow: hidden;
@ -53,18 +51,12 @@
background-color: #dbdbdb; background-color: #dbdbdb;
color: #444; color: #444;
position: absolute; position: absolute;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
} }
.assignee .avatar.avatar-image { .assignee .avatar.avatar-image {
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
height: 100%; height: 100%;
width: 100%; width: 100%;
display: block;
} }
.assignee .assignee-presence-status { .assignee .assignee-presence-status {
background-color: #b3b3b3; background-color: #b3b3b3;
@ -75,6 +67,7 @@
position: absolute; position: absolute;
right: -1px; right: -1px;
bottom: -1px; bottom: -1px;
border: 1px solid #fff;
z-index: 15; z-index: 15;
} }
.assignee .assignee-presence-status.active { .assignee .assignee-presence-status.active {
@ -98,7 +91,6 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 0 0 2px #bfbfbf inset; box-shadow: 0 0 0 2px #bfbfbf inset;
transition: box-shadow 0.12s;
} }
.assignee.add-assignee:hover, .assignee.add-assignee:hover,
.assignee.add-assignee.is-active { .assignee.add-assignee.is-active {
@ -110,83 +102,22 @@
background-color: rgba(0,0,0,0.875); background-color: rgba(0,0,0,0.875);
color: #fff; color: #fff;
border-radius: 0.7vw; border-radius: 0.7vw;
font-size: 0.98em;
} }
.card-details { .card-details {
padding: 0; padding: 0;
flex-shrink: 0; flex-shrink: 0;
flex-basis: min(600px, 80vw); flex-basis: min(600px, 80vw);
will-change: flex-basis; will-change: flex-basis;
overflow-y: auto; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
background: #f7f7f7; background: #f7f7f7;
border-radius: 0 0 0.4vw 0.4vw; border-radius: bottom 0.4vw;
z-index: 30; z-index: 30;
animation: flexGrowIn 0.1s; animation: flexGrowIn 0.1s;
box-shadow: 0 0 0.9vh 0 #b3b3b3; 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; 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 { .card-details .mCustomScrollBox {
padding-left: 0; padding-left: 0;
} }
@ -196,47 +127,18 @@ body.desktop-mode .card-details.card-details-collapsed {
} }
.card-details .card-details-header { .card-details .card-details-header {
margin: 0 -20px 5px; margin: 0 -20px 5px;
padding: 8px 20px; padding: 7px 20px;
background: #ededed; background: #ededed;
border-bottom: 1px solid #dbdbdb; border-bottom: 1px solid #dbdbdb;
position: sticky; position: sticky;
top: 0px; top: 0px;
z-index: 500; z-index: 500;
display: flow-root;
min-height: 44px;
} }
.card-details .card-details-header .card-number { .card-details .card-details-header .card-number {
color: #b3b3b3; color: #b3b3b3;
display: inline-block; 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; 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 .close-card-details,
.card-details .card-details-header .maximize-card-details, .card-details .card-details-header .maximize-card-details,
.card-details .card-details-header .minimize-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; font-size: 24px;
padding: 5px 10px 5px 10px; padding: 5px 10px 5px 10px;
margin-right: -8px; 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 .close-card-details-mobile-web {
.card-details .card-details-header .card-mobile-desktop-toggle {
font-size: 24px; font-size: 24px;
padding: 5px; padding: 5px;
margin-right: 5px; margin-right: 40px;
cursor: pointer;
user-select: none;
} }
.card-details .card-details-header .card-copy-button { .card-details .card-details-header .card-copy-button {
font-size: 17px; font-size: 17px;
@ -281,44 +175,12 @@ body.desktop-mode .card-details.card-details-collapsed {
.card-details .card-details-header .card-details-menu { .card-details .card-details-header .card-details-menu {
font-size: 17px; font-size: 17px;
padding: 10px; padding: 10px;
vertical-align: middle;
line-height: 1.2;
} }
.card-details .card-details-header .card-details-menu-mobile-web { .card-details .card-details-header .card-details-menu-mobile-web {
font-size: 17px; font-size: 17px;
padding: 10px; padding: 10px;
margin-right: 30px; 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 { .card-details .card-details-header .card-details-watch {
font-size: 17px; font-size: 17px;
padding-left: 7px; padding-left: 7px;
@ -326,13 +188,9 @@ body.desktop-mode .card-details.card-details-collapsed {
} }
.card-details .card-details-header .card-details-title { .card-details .card-details-header .card-details-title {
font-weight: bold; font-weight: bold;
font-size: 1.35em; font-size: 1.33em;
margin: 7px 0 0; margin: 7px 0 0;
padding: 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 { .card-details .card-details-header .linked-card-location {
font-style: italic; font-style: italic;
@ -347,10 +205,10 @@ body.desktop-mode .card-details.card-details-collapsed {
margin-bottom: 10px; margin-bottom: 10px;
} }
.card-details .card-details-header form.inlined-form .copied-tooltip { .card-details .card-details-header form.inlined-form .copied-tooltip {
padding: 0 10px; padding: 0px 10px;
} }
.card-details .card-details-header .card-details-list { .card-details .card-details-header .card-details-list {
font-size: 0.9em; font-size: 0.85em;
margin-bottom: 3px; margin-bottom: 3px;
} }
.card-details .card-details-header .card-details-list a.card-details-list-title { .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; display: inline-block;
background: #e6e6e6; background: #e6e6e6;
border-radius: 3px; border-radius: 3px;
padding: 0 5px; padding: 0px 5px;
} }
.card-details .card-details-header .copied-tooltip { .card-details .card-details-header .copied-tooltip {
margin-right: 10px; margin-right: 10px;
@ -371,13 +229,11 @@ body.desktop-mode .card-details.card-details-collapsed {
} }
.card-details .card-description textarea { .card-details .card-description textarea {
min-height: 100px; min-height: 100px;
resize: vertical;
} }
.card-details .card-details-items { .card-details .card-details-items {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 15px 0; margin: 15px 0;
gap: 0.5em;
} }
.card-details .card-details-items .card-details-item { .card-details .card-details-items .card-details-item {
margin-right: 0.5em; margin-right: 0.5em;
@ -428,28 +284,15 @@ body.desktop-mode .card-details.card-details-collapsed {
position: fixed; position: fixed;
resize: both; 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 { .card-details-maximized {
padding: 0; padding: 0;
flex-shrink: 0; flex-shrink: 0;
flex-basis: calc(100% - 20px); flex-basis: calc(100% - 20px);
will-change: flex-basis; will-change: flex-basis;
overflow-y: auto; overflow-y: scroll;
overflow-x: auto; overflow-x: scroll;
background: #f7f7f7; background: #f7f7f7;
border-radius: 0 0 3px 3px; border-radius: bottom 3px;
z-index: 100; z-index: 100;
animation: flexGrowIn 0.1s; animation: flexGrowIn 0.1s;
box-shadow: 0 0 7px 0 #b3b3b3; box-shadow: 0 0 7px 0 #b3b3b3;
@ -492,52 +335,19 @@ input[type="submit"].attachment-add-link-submit {
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.card-details { .card-details {
width: 100% !important; width: calc(100% - 1px);
padding: 0 !important; padding: 0px 20px 0px 20px;
margin: 0 !important; margin: 0px;
transition: none; transition: none;
overflow-y: auto; overflow-y: revert;
overflow-x: hidden; overflow-x: revert;
-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;
} }
.card-details .card-details-canvas { .card-details .card-details-canvas {
width: 100%; width: 100%;
padding-left: 0px; padding-left: 0px;
padding: 0 15px;
} }
.card-details .card-details-header .close-card-details { .card-details .card-details-header .close-card-details {
margin-right: 0px; 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 { .card-details .card-details-header .card-details-menu {
margin-right: 40px; 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 { .pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
margin: 0; 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 { .card-details-white {
background: #fff !important; background: #fff !important;
@ -727,15 +481,13 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
.vote-title { .vote-title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
.vote-title .js-edit-date { .vote-title .js-edit-date {
align-self: flex-start; align-self: baseline;
margin-left: 6px; margin-left: 5px;
} }
.vote-result { .vote-result {
display: flex; display: flex;
gap: 6px;
} }
.js-show-positive-votes { .js-show-positive-votes {
cursor: pointer; cursor: pointer;
@ -746,33 +498,29 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
.poker-title { .poker-title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
.poker-title .js-edit-date { .poker-title .js-edit-date {
align-self: flex-start; align-self: baseline;
margin-left: 6px; margin-left: 5px;
} }
.poker-result { .poker-result {
display: flex; display: flex;
flex-wrap: wrap; flex-flow: row wrap;
gap: 7px;
} }
.js-show-positive-poker-votes { .js-show-positive-poker-votes {
cursor: pointer; cursor: pointer;
} }
.poker-deck { .poker-deck {
display: grid; display: grid;
grid-auto-flow: row; flex-direction: column;
text-align: center; text-align: center;
gap: 6px;
} }
.poker-card-result { .poker-card-result {
width: 34px; width: 32px;
font-size: 1em; font-size: 1em;
font-weight: bold; font-weight: bold;
padding: 4px 2px; padding: 4px 2px 4px 2px;
cursor: default; cursor: default;
border-radius: 3px;
} }
.winner { .winner {
font-weight: bold; font-weight: bold;
@ -783,7 +531,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
} }
.responsive-table { .responsive-table {
overflow-x: auto; overflow-x: auto;
width: 100%;
} }
.poker-table { .poker-table {
display: table; display: table;
@ -846,15 +593,11 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
margin: auto; margin: auto;
margin-right: 10px; margin-right: 10px;
width: 100px; width: 100px;
border-radius: 2px;
padding: 3px 6px;
} }
.estimation-add button { .estimation-add button {
display: inline-block; display: inline-block;
float: right; float: right;
margin: auto; margin: auto;
border-radius: 2px;
padding: 3px 10px;
} }
.poker-card { .poker-card {
width: 48px; width: 48px;
@ -873,7 +616,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
text-align: center; text-align: center;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
transition: box-shadow 0.12s;
} }
.poker-card .inner { .poker-card .inner {
display: table-cell; display: table-cell;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -223,13 +223,9 @@
.card-label-edit-button:hover { .card-label-edit-button:hover {
background: #dbdbdb; background: #dbdbdb;
} }
ul.edit-labels-pop-over span.label-handle { ul.edit-labels-pop-over span.fa.label-handle {
padding-right: 10px; 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; max-width: 180px;
} }

View file

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

View file

@ -125,8 +125,19 @@ Template.createLabelPopup.events({
.$('#labelName') .$('#labelName')
.val() .val()
.trim(); .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(); Popup.back();
}, },
}); });
@ -144,8 +155,19 @@ Template.editLabelPopup.events({
.$('#labelName') .$('#labelName')
.val() .val()
.trim(); .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(); Popup.back();
}, },
}); });

View file

@ -45,10 +45,9 @@
} }
.minicard-details-menu-with-handle { .minicard-details-menu-with-handle {
float: right; float: right;
padding-left: 0.7vw;
font-size: clamp(14px, 3vw, 18px); font-size: clamp(14px, 3vw, 18px);
padding: 0; padding-right: 4vw;
z-index: 1; padding-left: 0.7vw;
} }
.minicard-details-menu { .minicard-details-menu {
float: right; float: right;
@ -98,7 +97,6 @@
} }
.minicard .minicard-labels { .minicard .minicard-labels {
float: none; float: none;
margin-right: 6vw;
} }
.minicard .minicard-labels .minicard-label { .minicard .minicard-labels .minicard-label {
width: clamp(12px, 1.5vw, 16px); width: clamp(12px, 1.5vw, 16px);
@ -113,7 +111,6 @@
} }
.minicard .minicard-custom-fields { .minicard .minicard-custom-fields {
display: block; display: block;
margin-right: 6vw;
} }
.minicard .minicard-custom-field { .minicard .minicard-custom-field {
display: flex; display: flex;
@ -136,25 +133,18 @@
width: clamp(20px, 2.5vw, 28px); width: clamp(20px, 2.5vw, 28px);
height: clamp(20px, 2.5vw, 28px); height: clamp(20px, 2.5vw, 28px);
position: absolute; position: absolute;
right: 0vw; right: 0.7vw;
top: 4vh; top: 0.7vh;
display: none; display: none;
z-index: 1;
} }
@media only screen { @media only screen {
.minicard .handle { .minicard .handle {
display: block; display: block;
} }
} }
.minicard .handle .drag-handle { .minicard .handle .fa-arrows {
font-size: clamp(16px, 3vw, 20px); font-size: clamp(16px, 3vw, 20px);
color: #ccc; color: #ccc;
display: inline-block;
width: 1.4em;
text-align: center;
}
.minicard .minicard-title {
margin-right: 1.5vw;
} }
.minicard .minicard-title .card-number { .minicard .minicard-title .card-number {
color: #b3b3b3; color: #b3b3b3;
@ -174,10 +164,6 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
position: relative;
z-index: 5;
margin-right: 6vw;
clear: both;
} }
.minicard .date { .minicard .date {
margin-right: 0.4vw; margin-right: 0.4vw;
@ -311,6 +297,19 @@
background-color: #1976d2 !important; 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 { .minicard .badges {
float: left; float: left;
margin-top: 1vh; margin-top: 1vh;
@ -742,80 +741,7 @@
gap: 0.3vw; gap: 0.3vw;
} }
/* Checklist display on minicard */ .minicard-list-name i.fa {
.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 {
font-size: 0.8em; font-size: 0.8em;
font-weight: bold; opacity: 0.7;
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;
} }

View file

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

View file

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

View file

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

View file

@ -87,15 +87,6 @@ textarea.js-edit-subtask-item {
top: 0; top: 0;
bottom: -600px; bottom: -600px;
right: 0; 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 { .subtasks {
background: #f7f7f7; background: #f7f7f7;
@ -136,25 +127,6 @@ textarea.js-edit-subtask-item {
border-bottom: 2px solid #3cb500; border-bottom: 2px solid #3cb500;
border-right: 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 { .subtasks-item .item-title {
flex: 1; flex: 1;
padding-left: 10px; padding-left: 10px;

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ template(name="datepicker")
.fields .fields
.left .left
label(for="date") {{_ 'date'}} 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 .right
label(for="time") {{_ 'time'}} label(for="time") {{_ 'time'}}
input.js-time-field#time(type="time" name="time" value=showTime) input.js-time-field#time(type="time" name="time" value=showTime)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,7 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import { trelloGetMembersToMap } from './trelloMembersMapper'; import { trelloGetMembersToMap } from './trelloMembersMapper';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { wekanGetMembersToMap } from './wekanMembersMapper'; import { wekanGetMembersToMap } from './wekanMembersMapper';
import { csvGetMembersToMap } from './csvMembersMapper'; import { csvGetMembersToMap } from './csvMembersMapper';
import getSlug from 'limax';
const Papa = require('papaparse'); const Papa = require('papaparse');

View file

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

View file

@ -276,21 +276,19 @@ BlazeComponent.extendComponent({
return; return;
} }
// Reactively show/hide resize handle based on collapse and auto-width state
this.autorun(() => { // Only enable resize for non-collapsed, non-auto-width lists
const isAutoWidth = this.autoWidth(); const isAutoWidth = this.autoWidth();
const isCollapsed = Utils.getListCollapseState(list); if (list.collapsed || isAutoWidth) {
if (isCollapsed || isAutoWidth) {
$resizeHandle.hide(); $resizeHandle.hide();
} else { return;
$resizeHandle.show();
} }
});
let isResizing = false; let isResizing = false;
let startX = 0; let startX = 0;
let startWidth = 0; let startWidth = 0;
let minWidth = 270; // Minimum width matching system default let minWidth = 100; // Minimum width as defined in the existing code
let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers 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 component = this; // Store reference to component for use in event handlers
@ -319,7 +317,7 @@ BlazeComponent.extendComponent({
const currentX = e.pageX || e.originalEvent.touches[0].pageX; const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX; 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 // Apply the new width immediately for real-time feedback
$list[0].style.setProperty('--list-width', `${newWidth}px`); $list[0].style.setProperty('--list-width', `${newWidth}px`);
@ -344,7 +342,7 @@ BlazeComponent.extendComponent({
// Calculate final width // Calculate final width
const currentX = e.pageX || e.originalEvent.touches[0].pageX; const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX; const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, startWidth + deltaX); const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
// Ensure the final width is applied // Ensure the final width is applied
$list[0].style.setProperty('--list-width', `${finalWidth}px`); $list[0].style.setProperty('--list-width', `${finalWidth}px`);
@ -435,10 +433,9 @@ BlazeComponent.extendComponent({
}); });
// Reactively update resize handle visibility when auto-width or collapse changes // Reactively update resize handle visibility when auto-width changes
component.autorun(() => { component.autorun(() => {
const collapsed = Utils.getListCollapseState(list); if (component.autoWidth()) {
if (component.autoWidth() || collapsed) {
$resizeHandle.hide(); $resizeHandle.hide();
} else { } else {
$resizeHandle.show(); $resizeHandle.show();
@ -455,28 +452,9 @@ BlazeComponent.extendComponent({
}, },
}).register('list'); }).register('list');
Template.list.helpers({
collapsed() {
return Utils.getListCollapseState(this);
},
});
Template.miniList.events({ Template.miniList.events({
'click .js-select-list'() { 'click .js-select-list'() {
const listId = this._id; const listId = this._id;
Session.set('currentList', listId); Session.set('currentList', listId);
}, },
}); });
// Enable drag-reorder for collapsed lists from .js-collapsed-list-drag area
this.$('.js-collapsed-list-drag').draggable({
axis: 'x',
helper: 'clone',
revert: 'invalid',
start(evt, ui) {
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
}
});

View file

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

View file

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

View file

@ -8,7 +8,7 @@ template(name="listHeader")
if isMiniScreen if isMiniScreen
if currentList if currentList
a.list-header-left-icon.js-unselect-list a.list-header-left-icon.js-unselect-list
i.fa.fa-caret-left | ◀️
else else
if collapsed if collapsed
if showCardsCountForList cards.length if showCardsCountForList cards.length
@ -26,19 +26,15 @@ template(name="listHeader")
|/#{wipLimit.value}) |/#{wipLimit.value})
if showCardsCountForList cards.length if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}} span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
else else
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
if collapsed if collapsed
i.fa.fa-caret-right a.js-collapse(title="{{_ 'uncollapse'}}")
else | ⬅️
i.fa.fa-caret-down | ➡️
div(class="{{#if collapsed}}list-rotated{{/if}}") div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name( h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}" title="{{ moment modifiedAt 'LLL' }}"
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}") class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer +viewer
= title = title
if wipLimit.enabled if wipLimit.enabled
@ -48,54 +44,35 @@ template(name="listHeader")
unless collapsed unless collapsed
if showCardsCountForList cards.length if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}} span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
if isMiniScreen if isMiniScreen
if currentList if currentList
if isWatching if isWatching
i.list-header-watch-icon i.fa.fa-eye i.list-header-watch-icon | 👁️
div.list-header-menu div.list-header-menu
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
if canSeeAddCard if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") 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'}}") ☰
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
else else
a.list-header-menu-icon.js-select-list a.list-header-menu-icon.js-select-list ▶️
i.fa.fa-caret-right a.list-header-handle.handle.js-list-handle ↕️
unless currentUser.isWorker
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle.handle.js-list-handle
i.fa.fa-arrows
else if currentUser.isBoardMember else if currentUser.isBoardMember
if isWatching if isWatching
i.list-header-watch-icon i.fa.fa-eye i.list-header-watch-icon | 👁️
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
unless collapsed unless collapsed
div.list-header-menu div.list-header-menu
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
//if isBoardAdmin //if isBoardAdmin
// // a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
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 if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
i.fa.fa-plus a.js-collapse(title="{{_ 'collapse'}}")
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ⬅️
i.fa.fa-bars | ➡️
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") template(name="editListTitleForm")
.list-composer .list-composer
@ -103,77 +80,62 @@ template(name="editListTitleForm")
.edit-controls.clearfix .edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}} button.primary.confirm(type="submit") {{_ 'save'}}
a.js-close-inlined-form a.js-close-inlined-form
i.fa.fa-times-thin | ❌
template(name="listActionPopup") template(name="listActionPopup")
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
ul.pop-over-list 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 li
a.js-add-card.list-header-plus-bottom a.js-add-card.list-header-plus-bottom
i.fa.fa-plus |
i.fa.fa-arrow-down | ⬇️
| {{_ 'add-card-to-bottom-of-list'}} | {{_ 'add-card-to-bottom-of-list'}}
hr hr
ul.pop-over-list
li
a.js-add-list
i.fa.fa-plus
| {{_ 'add-list'}}
hr
ul.pop-over-list ul.pop-over-list
li li
a.js-set-list-width a.js-set-list-width
i.fa.fa-arrows-h | ↔️
| {{_ 'set-list-width'}} | {{_ 'set-list-width'}}
ul.pop-over-list ul.pop-over-list
li li
a.js-toggle-watch-list a.js-toggle-watch-list
if isWatching if isWatching
i.fa.fa-eye | 👁️
| {{_ 'unwatch'}} | {{_ 'unwatch'}}
else else
i.fa.fa-eye-slash | 🙈
| {{_ 'watch'}} | {{_ 'watch'}}
unless currentUser.isCommentOnly unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
unless currentUser.isWorker unless currentUser.isWorker
ul.pop-over-list ul.pop-over-list
li li
a.js-set-color-list a.js-set-color-list
i.fa.fa-paint-brush | 🎨
| {{_ 'set-color-list'}} | {{_ 'set-color-list'}}
ul.pop-over-list ul.pop-over-list
if cards.length if cards.length
li li
a.js-select-cards a.js-select-cards
i.fa.fa-select-square | ☑️
| {{_ 'list-select-cards'}} | {{_ 'list-select-cards'}}
if currentUser.isBoardAdmin if currentUser.isBoardAdmin
ul.pop-over-list ul.pop-over-list
li li
a.js-set-wip-limit a.js-set-wip-limit
i.fa.fa-ban | 🚫
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}} | {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
unless currentUser.isWorker unless currentUser.isWorker
hr hr
ul.pop-over-list ul.pop-over-list
li li
a.js-close-list a.js-close-list
i.fa.fa-arrow-right | ➡️
i.fa.fa-archive | 📦
| {{_ 'archive-list'}} | {{_ 'archive-list'}}
hr hr
ul.pop-over-list ul.pop-over-list
li li
a.js-more a.js-more
i.fa.fa-link | 🔗
| {{_ 'listMorePopup-title'}} | {{_ 'listMorePopup-title'}}
template(name="boardLists") template(name="boardLists")
@ -190,15 +152,13 @@ template(name="listMorePopup")
span.clearfix span.clearfix
span {{_ 'link-list'}} 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 }}") input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}} | {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }} span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
//unless currentUser.isWorker //unless currentUser.isWorker
// // if currentUser.isBoardAdmin
if currentUser.isBoardAdmin // a.js-delete {{_ 'delete'}}
//
a.js-delete {{_ 'delete'}}
template(name="listDeletePopup") template(name="listDeletePopup")
p {{_ "list-delete-pop"}} p {{_ "list-delete-pop"}}
@ -212,7 +172,7 @@ template(name="setWipLimitPopup")
ul.pop-over-list ul.pop-over-list
li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}} li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
if isWipLimitEnabled if isWipLimitEnabled
i.fa.fa-check | ✅
if isWipLimitEnabled if isWipLimitEnabled
p p
input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99") input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
@ -233,8 +193,8 @@ template(name="setListWidthPopup")
#js-list-width-edit #js-list-width-edit
label {{_ 'set-list-width-value'}} label {{_ 'set-list-width-value'}}
p p
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270") input.list-width-value(type="number" value="{{ listWidthValue }}" min="100")
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="270") input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="100")
input.list-width-apply(type="submit" value="{{_ 'apply'}}") input.list-width-apply(type="submit" value="{{_ 'apply'}}")
input.list-width-error input.list-width-error
br br
@ -245,7 +205,7 @@ template(name="setListWidthPopup")
template(name="listWidthErrorPopup") template(name="listWidthErrorPopup")
.list-width-invalid .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'}} button.full.js-back-view(type="submit") {{_ 'cancel'}}
template(name="setListColorPopup") template(name="setListColorPopup")
@ -254,29 +214,6 @@ template(name="setListColorPopup")
// note: we use the swimlane palette to have more than just the border // 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}}") span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color) if(isSelected color)
i.fa.fa-check | ✅
button.primary.confirm.js-submit {{_ 'save'}} button.primary.confirm.js-submit {{_ 'save'}}
button.js-remove-color.negate.wide.right {{_ 'unset-color'}} 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 { ReactiveCache } from '/imports/reactiveCache';
import Lists from '../../../models/lists';
import { TAPi18n } from '/imports/i18n'; import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll'; import dragscroll from '@wekanteam/dragscroll';
@ -22,37 +21,36 @@ BlazeComponent.extendComponent({
isBoardAdmin() { isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin(); return ReactiveCache.getCurrentUser().isBoardAdmin();
}, },
async starred(check = undefined) { starred(check = undefined) {
const list = Template.currentData(); const list = Template.currentData();
const status = list.isStarred(); const status = list.isStarred();
if (check === undefined) { if (check === undefined) {
// just check // just check
return status; return status;
} else { } else {
await list.star(!status); list.star(!status);
return !status; return !status;
} }
}, },
collapsed(check = undefined) { collapsed(check = undefined) {
const list = Template.currentData(); const list = Template.currentData();
const status = Utils.getListCollapseState(list); const status = list.isCollapsed();
if (check === undefined) { if (check === undefined) {
// just check // just check
return status; return status;
} else { } else {
const next = typeof check === 'boolean' ? check : !status; list.collapse(!status);
Utils.setListCollapseState(list, next); return !status;
return next;
} }
}, },
async editTitle(event) { editTitle(event) {
event.preventDefault(); event.preventDefault();
const newTitle = this.childComponents('inlinedForm')[0] const newTitle = this.childComponents('inlinedForm')[0]
.getValue() .getValue()
.trim(); .trim();
const list = this.currentData(); const list = this.currentData();
if (newTitle) { if (newTitle) {
await list.rename(newTitle.trim()); list.rename(newTitle.trim());
} }
}, },
@ -144,48 +142,7 @@ BlazeComponent.extendComponent({
Template.listHeader.helpers({ Template.listHeader.helpers({
isBoardAdmin() { isBoardAdmin() {
return ReactiveCache.getCurrentUser().isBoardAdmin(); return ReactiveCache.getCurrentUser().isBoardAdmin();
},
numberFieldsSum() {
const list = Template.currentData();
if (!list) return 0;
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
if (!fields || !fields.length) return 0;
const cards = ReactiveCache.getCards({ listId: list._id, archived: false });
let total = 0;
if (cards && cards.length) {
cards.forEach(card => {
const cfs = (card.customFields || []);
fields.forEach(field => {
const cf = cfs.find(f => f && f._id === field._id);
if (!cf || cf.value === null || cf.value === undefined) return;
let v = cf.value;
if (typeof v === 'string') {
const parsed = parseFloat(v.replace(',', '.'));
if (isNaN(parsed)) return;
v = parsed;
} }
if (typeof v === 'number' && isFinite(v)) {
total += v;
}
});
});
}
return total;
},
hasNumberFieldsSum() {
const boardId = Session.get('currentBoard');
const fields = ReactiveCache.getCustomFields({
boardIds: { $in: [boardId] },
showSumAtTopOfList: true,
type: 'number',
});
return !!(fields && fields.length);
},
}); });
Template.listActionPopup.helpers({ Template.listActionPopup.helpers({
@ -204,27 +161,14 @@ Template.listActionPopup.helpers({
Template.listActionPopup.events({ Template.listActionPopup.events({
'click .js-list-subscribe'() {}, 'click .js-list-subscribe'() {},
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(`#js-list-${this._id}`)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
if (listComponent) {
listComponent.openForm({
position: 'top',
});
}
Popup.back();
},
'click .js-add-card.list-header-plus-bottom'(event) { 'click .js-add-card.list-header-plus-bottom'(event) {
const listDom = $(`#js-list-${this._id}`)[0]; const listDom = $(`#js-list-${this._id}`)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom); const listComponent = BlazeComponent.getComponentForElement(listDom);
if (listComponent) {
listComponent.openForm({ listComponent.openForm({
position: 'bottom', position: 'bottom',
}); });
}
Popup.back(); Popup.back();
}, },
'click .js-add-list': Popup.open('addList'),
'click .js-set-list-width': Popup.open('setListWidth'), 'click .js-set-list-width': Popup.open('setListWidth'),
'click .js-set-color-list': Popup.open('setListColor'), 'click .js-set-color-list': Popup.open('setListColor'),
'click .js-select-cards'() { 'click .js-select-cards'() {
@ -239,9 +183,9 @@ Template.listActionPopup.events({
if (!err && ret) Popup.back(); if (!err && ret) Popup.back();
}); });
}, },
async 'click .js-close-list'(event) { 'click .js-close-list'(event) {
event.preventDefault(); event.preventDefault();
await this.archive(); this.archive();
Popup.back(); Popup.back();
}, },
'click .js-set-wip-limit': Popup.open('setWipLimit'), 'click .js-set-wip-limit': Popup.open('setWipLimit'),
@ -268,26 +212,26 @@ BlazeComponent.extendComponent({
} }
}, },
async enableSoftLimit() { enableSoftLimit() {
const list = Template.currentData(); const list = Template.currentData();
if ( if (
list.getWipLimit('soft') && list.getWipLimit('soft') &&
list.getWipLimit('value') < list.cards().length list.getWipLimit('value') < list.cards().length
) { ) {
await list.setWipLimit(list.cards().length); list.setWipLimit(list.cards().length);
} }
Meteor.call('enableSoftLimit', Template.currentData()._id); Meteor.call('enableSoftLimit', Template.currentData()._id);
}, },
async enableWipLimit() { enableWipLimit() {
const list = Template.currentData(); 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 // Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
if ( if (
!list.getWipLimit('enabled') && !list.getWipLimit('enabled') &&
list.getWipLimit('value') < list.cards().length list.getWipLimit('value') < list.cards().length
) { ) {
await list.setWipLimit(list.cards().length); list.setWipLimit(list.cards().length);
} }
Meteor.call('enableWipLimit', list._id); Meteor.call('enableWipLimit', list._id);
}, },
@ -381,12 +325,12 @@ BlazeComponent.extendComponent({
'click .js-palette-color'() { 'click .js-palette-color'() {
this.currentColor.set(this.currentData().color); this.currentColor.set(this.currentData().color);
}, },
async 'click .js-submit'() { 'click .js-submit'() {
await this.currentList.setColor(this.currentColor.get()); this.currentList.setColor(this.currentColor.get());
Popup.close(); Popup.close();
}, },
async 'click .js-remove-color'() { 'click .js-remove-color'() {
await this.currentList.setColor(null); this.currentList.setColor(null);
Popup.close(); Popup.close();
}, },
}, },
@ -412,7 +356,7 @@ BlazeComponent.extendComponent({
); );
// FIXME(mark-i-m): where do we put constants? // FIXME(mark-i-m): where do we put constants?
if (width < 270 || !width || constraint < 270 || !constraint) { if (width < 100 || !width || constraint < 100 || !constraint) {
Template.instance() Template.instance()
.$('.list-width-error') .$('.list-width-error')
.click(); .click();
@ -453,108 +397,3 @@ BlazeComponent.extendComponent({
]; ];
}, },
}).register('setListWidthPopup'); }).register('setListWidthPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Utils.getCurrentBoard();
this.currentSwimlaneId = new ReactiveVar(null);
this.currentListId = new ReactiveVar(null);
// Get the swimlane context from opener
const openerData = Popup.getOpenerComponent()?.data();
// If opened from swimlane menu, openerData is the swimlane
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
this.currentSwimlane = openerData;
this.currentSwimlaneId.set(openerData._id);
} else if (openerData?._id) {
// If opened from list menu, get swimlane from the list
const list = ReactiveCache.getList({ _id: openerData._id });
this.currentSwimlane = list?.swimlaneId ? ReactiveCache.getSwimlane({ _id: list.swimlaneId }) : null;
this.currentSwimlaneId.set(this.currentSwimlane?._id || null);
this.currentListId.set(openerData._id);
}
},
currentSwimlaneData() {
const swimlaneId = this.currentSwimlaneId.get();
return swimlaneId ? ReactiveCache.getSwimlane({ _id: swimlaneId }) : null;
},
currentListIdValue() {
return this.currentListId.get();
},
swimlaneLists() {
const swimlaneId = this.currentSwimlaneId.get();
if (swimlaneId) {
return ReactiveCache.getLists({ swimlaneId, archived: false }).sort((a, b) => a.sort - b.sort);
}
return this.currentBoard.lists;
},
events() {
return [
{
'submit .js-add-list-form'(evt) {
evt.preventDefault();
const titleInput = this.find('.list-name-input');
const title = titleInput?.value.trim();
if (!title) return;
let sortIndex = 0;
const boardId = Utils.getCurrentBoardId();
let swimlaneId = this.currentSwimlane?._id;
const positionInput = this.find('.list-position-input');
if (positionInput && positionInput.value) {
const positionId = positionInput.value.trim();
const selectedList = ReactiveCache.getList({ boardId, _id: positionId, archived: false });
if (selectedList) {
sortIndex = selectedList.sort + 1;
// Use the swimlane ID from the selected list to ensure the new list
// is added to the same swimlane as the selected list
swimlaneId = selectedList.swimlaneId;
} else {
// No specific position, add at end of swimlane
if (swimlaneId) {
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
} else {
const lastList = this.currentBoard.getLastList();
sortIndex = Utils.calculateIndexData(lastList, null).base;
}
}
} else {
// No position input, add at end of swimlane
if (swimlaneId) {
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
} else {
const lastList = this.currentBoard.getLastList();
sortIndex = Utils.calculateIndexData(lastList, null).base;
}
}
Lists.insert({
title,
boardId: Session.get('currentBoard'),
sort: sortIndex,
type: 'list',
swimlaneId: swimlaneId,
});
Popup.back();
},
'click .js-list-template': Popup.open('searchElement'),
},
];
},
}).register('addListPopup');

View file

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

View file

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

View file

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

View file

@ -232,24 +232,6 @@ class DueCardsComponent extends BlazeComponent {
}); });
} }
// Normalize dueAt to timestamps for stable client-side ordering
const future = new Date('2100-12-31').getTime();
const toTime = v => {
if (v === null || v === undefined || v === '') return future;
if (v instanceof Date) return v.getTime();
const t = new Date(v);
if (!isNaN(t.getTime())) return t.getTime();
return future;
};
filteredCards.sort((a, b) => {
const x = toTime(a.dueAt);
const y = toTime(b.dueAt);
if (x > y) return 1;
if (x < y) return -1;
return 0;
});
if (process.env.DEBUG === 'true') { if (process.env.DEBUG === 'true') {
console.log('dueCards client: filtered to', filteredCards.length, 'cards'); console.log('dueCards client: filtered to', filteredCards.length, 'cards');
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -81,27 +81,6 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
/* iOS Safari fixes */
-webkit-overflow-scrolling: touch;
}
/* Mobile mode specific fixes for iOS Safari */
body.mobile-mode {
overflow-x: hidden;
position: fixed;
width: 100%;
height: 100vh;
/* Prevent iOS Safari bounce scroll */
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
}
/* Ensure content area is scrollable in mobile mode */
body.mobile-mode #content {
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
height: calc(100vh - 48px);
} }
#content { #content {
position: relative; position: relative;
@ -548,7 +527,7 @@ a:not(.disabled).is-active i.fa {
/* Board canvas */ /* Board canvas */
.board-canvas { .board-canvas {
padding: 0 8px 8px 0; padding: 8px;
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
@ -696,7 +675,7 @@ a:not(.disabled).is-active i.fa {
} }
.board-canvas { .board-canvas {
padding: 0 12px 12px 0; padding: 12px;
} }
#header { #header {
@ -720,11 +699,6 @@ a:not(.disabled).is-active i.fa {
.setting-content .content-body .side-menu { .setting-content .content-body .side-menu {
width: 250px; width: 250px;
} }
/* Responsive handling for quick-access description on tablets */
#header-quick-access ul.header-quick-access-list li.current.empty {
max-width: 300px;
}
} }
/* Large displays and digital signage (1920px+) */ /* Large displays and digital signage (1920px+) */
@ -782,7 +756,7 @@ a:not(.disabled).is-active i.fa {
.inline-input { .inline-input {
height: 37px; height: 37px;
margin: 8px 10px 0 0; margin: 8px 10px 0 0;
width: 100px; width: 50px;
} }
.select-authentication { .select-authentication {
width: 100%; width: 100%;
@ -925,40 +899,6 @@ a:not(.disabled).is-active i.fa {
height: 100%; height: 100%;
} }
} }
/* iOS Safari Mobile Mode Fixes */
@media screen and (max-width: 800px) {
/* Prevent scrolling issues on iOS Safari when card popup is open */
body.mobile-mode {
overflow: hidden;
position: fixed;
width: 100%;
height: 100vh;
}
/* Fix z-index stacking for mobile Safari */
body.mobile-mode .board-wrapper {
z-index: 1;
}
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
body.mobile-mode .card-details {
z-index: 100 !important;
}
body.mobile-mode .pop-over {
z-index: 999;
}
/* Ensure smooth scrolling on iOS */
body.mobile-mode .card-details,
body.mobile-mode .pop-over .content-wrapper {
-webkit-overflow-scrolling: touch;
}
}
@-moz-keyframes lds-roller { @-moz-keyframes lds-roller {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);

View file

@ -2,10 +2,8 @@ template(name="main")
html(lang="{{TAPi18n.getLanguage}}") html(lang="{{TAPi18n.getLanguage}}")
head head
title title
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes, viewport-fit=cover") meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
meta(http-equiv="X-UA-Compatible" content="IE=edge") meta(http-equiv="X-UA-Compatible" content="IE=edge")
meta(name="apple-mobile-web-app-capable" content="yes")
meta(name="apple-mobile-web-app-status-bar-style" content="black-translucent")
//- XXX We should use pathFor in the following `href` to support the case //- XXX We should use pathFor in the following `href` to support the case
where the application is deployed with a path prefix, but it seems to be where the application is deployed with a path prefix, but it seems to be
difficult to do that cleanly with Blaze -- at least without adding extra difficult to do that cleanly with Blaze -- at least without adding extra
@ -69,13 +67,7 @@ template(name="userFormsLayout")
select.select-lang.js-userform-set-language#userform-set-language-select(aria-label="{{_ 'changeLanguagePopup-title'}}") select.select-lang.js-userform-set-language#userform-set-language-select(aria-label="{{_ 'changeLanguagePopup-title'}}")
each languages each languages
if isCurrentLanguage if isCurrentLanguage
if rtl
option(value="{{tag}}" selected="selected") {{name}} (RTL)
else
option(value="{{tag}}" selected="selected") {{name}} option(value="{{tag}}" selected="selected") {{name}}
else
if rtl
option(value="{{tag}}") {{name}} (RTL)
else else
option(value="{{tag}}") {{name}} option(value="{{tag}}") {{name}}
@ -85,6 +77,7 @@ template(name="defaultLayout")
| {{{afterBodyStart}}} | {{{afterBodyStart}}}
+Template.dynamic(template=content) +Template.dynamic(template=content)
| {{{beforeBodyEnd}}} | {{{beforeBodyEnd}}}
+migrationProgress
+boardConversionProgress +boardConversionProgress
if (Modal.isOpen) if (Modal.isOpen)
#modal #modal
@ -92,13 +85,13 @@ template(name="defaultLayout")
if (Modal.isWide) if (Modal.isWide)
.modal-content-wide.modal-container .modal-content-wide.modal-container
a.modal-close-btn.js-close-modal a.modal-close-btn.js-close-modal
i.fa.fa-times-thin | ❌
+Template.dynamic(template=Modal.getHeaderName) +Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName) +Template.dynamic(template=Modal.getTemplateName)
else else
.modal-content.modal-container .modal-content.modal-container
a.modal-close-btn.js-close-modal a.modal-close-btn.js-close-modal
i.fa.fa-times-thin | ❌
+Template.dynamic(template=Modal.getHeaderName) +Template.dynamic(template=Modal.getHeaderName)
+Template.dynamic(template=Modal.getTemplateName) +Template.dynamic(template=Modal.getTemplateName)
@ -109,7 +102,8 @@ template(name="message")
.big-message.quiet(class=color) .big-message.quiet(class=color)
h1 {{_ label}} h1 {{_ label}}
unless currentUser unless currentUser
p {{{_ 'page-maybe-private' '/sign-in'}}} with(pathFor route='atSignIn')
p {{{_ 'page-maybe-private' this}}}
template(name="loader") template(name="loader")
h1.loadingText {{_ 'loading'}} h1.loadingText {{_ 'loading'}}

View file

@ -1,6 +1,7 @@
import { ReactiveCache } from '/imports/reactiveCache'; import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n'; import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
BlazeLayout.setRoot('body');
let alreadyCheck = 1; let alreadyCheck = 1;
let isCheckDone = false; let isCheckDone = false;
@ -86,91 +87,17 @@ Template.userFormsLayout.onRendered(() => {
); );
EscapeActions.executeAll(); EscapeActions.executeAll();
// Set up MutationObserver for OIDC button instead of deprecated DOMSubtreeModified
const oidcButton = document.getElementById('at-oidc');
if (oidcButton) {
const observer = new MutationObserver((mutations) => {
if (alreadyCheck <= 2) {
let currSetting = ReactiveCache.getCurrentSetting();
let oidcBtnElt = $('#at-oidc');
if (
currSetting &&
currSetting !== undefined &&
currSetting.oidcBtnText !== undefined &&
oidcBtnElt != null &&
oidcBtnElt != undefined
) {
let htmlvalue = "<i class='fa fa-oidc'></i>" + currSetting.oidcBtnText;
if (alreadyCheck == 1) {
alreadyCheck++;
oidcBtnElt.html('');
} else {
alreadyCheck++;
oidcBtnElt.html(htmlvalue);
}
}
} else {
alreadyCheck = 1;
}
});
observer.observe(oidcButton, { childList: true, subtree: true });
}
// Set up MutationObserver for .at-form instead of deprecated DOMSubtreeModified
const atForm = document.querySelector('.at-form');
if (atForm) {
const formObserver = new MutationObserver((mutations) => {
if (alreadyCheck <= 2 && !isCheckDone) {
if (document.getElementById('at-oidc') != null) {
let currSetting = ReactiveCache.getCurrentSetting();
let oidcBtnElt = $('#at-oidc');
if (
currSetting &&
currSetting !== undefined &&
currSetting.oidcBtnText !== undefined &&
oidcBtnElt != null &&
oidcBtnElt != undefined
) {
let htmlvalue =
"<i class='fa fa-oidc'></i>" + currSetting.oidcBtnText;
if (alreadyCheck == 1) {
alreadyCheck++;
oidcBtnElt.html('');
} else {
alreadyCheck++;
isCheckDone = true;
oidcBtnElt.html(htmlvalue);
}
}
}
} else {
alreadyCheck = 1;
}
});
formObserver.observe(atForm, { childList: true, subtree: true });
}
// Add autocomplete attribute to login input for WCAG compliance // Add autocomplete attribute to login input for WCAG compliance
const loginInput = document.querySelector( const loginInput = document.querySelector('input[type="text"], input[type="email"]');
'input[type="text"], input[type="email"]', if (loginInput && loginInput.name && (loginInput.name.toLowerCase().includes('user') || loginInput.name.toLowerCase().includes('email'))) {
);
if (
loginInput &&
loginInput.name &&
(loginInput.name.toLowerCase().includes('user') ||
loginInput.name.toLowerCase().includes('email'))
) {
loginInput.setAttribute('autocomplete', 'username email'); loginInput.setAttribute('autocomplete', 'username email');
} }
// Add autocomplete attributes to password fields for WCAG compliance // Add autocomplete attributes to password fields for WCAG compliance
const passwordInputs = document.querySelectorAll('input[type="password"]'); const passwordInputs = document.querySelectorAll('input[type="password"]');
passwordInputs.forEach((input) => { passwordInputs.forEach(input => {
if (input.name && input.name.includes('password')) { if (input.name && input.name.includes('password')) {
if ( if (input.name.includes('password_again') || input.name.includes('new_password')) {
input.name.includes('password_again') ||
input.name.includes('new_password')
) {
input.setAttribute('autocomplete', 'new-password'); input.setAttribute('autocomplete', 'new-password');
} else { } else {
input.setAttribute('autocomplete', 'current-password'); input.setAttribute('autocomplete', 'current-password');
@ -184,18 +111,18 @@ Template.userFormsLayout.helpers({
isLegalNoticeLinkExist() { isLegalNoticeLinkExist() {
const currSet = Template.instance().currentSetting.get(); const currSet = Template.instance().currentSetting.get();
if (currSet && currSet !== undefined && currSet != null) { if (currSet && currSet !== undefined && currSet != null) {
return ( return currSet.legalNotice !== undefined && currSet.legalNotice.trim() != "";
currSet.legalNotice !== undefined && currSet.legalNotice.trim() != '' }
); else
} else return false; return false;
}, },
getLegalNoticeWithWritTraduction() { getLegalNoticeWithWritTraduction() {
let spanLegalNoticeElt = $('#legalNoticeSpan'); let spanLegalNoticeElt = $("#legalNoticeSpan");
if (spanLegalNoticeElt != null && spanLegalNoticeElt != undefined) { if (spanLegalNoticeElt != null && spanLegalNoticeElt != undefined) {
spanLegalNoticeElt.html(TAPi18n.__('acceptance_of_our_legalNotice', {})); spanLegalNoticeElt.html(TAPi18n.__('acceptance_of_our_legalNotice', {}));
} }
let atLinkLegalNoticeElt = $('#legalNoticeAtLink'); let atLinkLegalNoticeElt = $("#legalNoticeAtLink");
if (atLinkLegalNoticeElt != null && atLinkLegalNoticeElt != undefined) { if (atLinkLegalNoticeElt != null && atLinkLegalNoticeElt != undefined) {
atLinkLegalNoticeElt.html(TAPi18n.__('legalNotice', {})); atLinkLegalNoticeElt.html(TAPi18n.__('legalNotice', {}));
} }
@ -216,7 +143,7 @@ Template.userFormsLayout.helpers({
languages() { languages() {
return TAPi18n.getSupportedLanguages() return TAPi18n.getSupportedLanguages()
.map(({ tag, name, rtl }) => ({ tag, name, rtl })) .map(({ tag, name }) => ({ tag: tag, name }))
.sort((a, b) => { .sort((a, b) => {
if (a.name === b.name) { if (a.name === b.name) {
return 0; return 0;
@ -247,6 +174,52 @@ Template.userFormsLayout.events({
} }
isCheckDone = false; isCheckDone = false;
}, },
'click #at-signUp'(event, templateInstance) {
isCheckDone = false;
},
'DOMSubtreeModified #at-oidc'(event) {
if (alreadyCheck <= 2) {
let currSetting = ReactiveCache.getCurrentSetting();
let oidcBtnElt = $("#at-oidc");
if (currSetting && currSetting !== undefined && currSetting.oidcBtnText !== undefined && oidcBtnElt != null && oidcBtnElt != undefined) {
let htmlvalue = "<i class='fa fa-oidc'></i>" + currSetting.oidcBtnText;
if (alreadyCheck == 1) {
alreadyCheck++;
oidcBtnElt.html("");
}
else {
alreadyCheck++;
oidcBtnElt.html(htmlvalue);
}
}
}
else {
alreadyCheck = 1;
}
},
'DOMSubtreeModified .at-form'(event) {
if (alreadyCheck <= 2 && !isCheckDone) {
if (document.getElementById("at-oidc") != null) {
let currSetting = ReactiveCache.getCurrentSetting();
let oidcBtnElt = $("#at-oidc");
if (currSetting && currSetting !== undefined && currSetting.oidcBtnText !== undefined && oidcBtnElt != null && oidcBtnElt != undefined) {
let htmlvalue = "<i class='fa fa-oidc'></i>" + currSetting.oidcBtnText;
if (alreadyCheck == 1) {
alreadyCheck++;
oidcBtnElt.html("");
}
else {
alreadyCheck++;
isCheckDone = true;
oidcBtnElt.html(htmlvalue);
}
}
}
}
else {
alreadyCheck = 1;
}
},
}); });
Template.defaultLayout.events({ Template.defaultLayout.events({
@ -274,14 +247,14 @@ async function authentication(event, templateInstance) {
switch (result) { switch (result) {
case 'ldap': case 'ldap':
return new Promise((resolve) => { return new Promise(resolve => {
Meteor.loginWithLDAP(match, password, function () { Meteor.loginWithLDAP(match, password, function () {
resolve(FlowRouter.go('/')); resolve(FlowRouter.go('/'));
}); });
}); });
case 'saml': case 'saml':
return new Promise((resolve) => { return new Promise(resolve => {
const provider = Meteor.settings.public.SAML_PROVIDER; const provider = Meteor.settings.public.SAML_PROVIDER;
Meteor.loginWithSaml( Meteor.loginWithSaml(
{ {
@ -294,7 +267,7 @@ async function authentication(event, templateInstance) {
}); });
case 'cas': case 'cas':
return new Promise((resolve) => { return new Promise(resolve => {
Meteor.loginWithCas(match, password, function () { Meteor.loginWithCas(match, password, function () {
resolve(FlowRouter.go('/')); resolve(FlowRouter.go('/'));
}); });
@ -306,15 +279,9 @@ async function authentication(event, templateInstance) {
} }
function getAuthenticationMethod( function getAuthenticationMethod(
settings, { displayAuthenticationMethod, defaultAuthenticationMethod },
match, match,
) { ) {
if (!settings) {
return getUserAuthenticationMethod(undefined, match);
}
const { displayAuthenticationMethod, defaultAuthenticationMethod } = settings;
if (displayAuthenticationMethod) { if (displayAuthenticationMethod) {
return $('.select-authentication').val(); return $('.select-authentication').val();
} }
@ -322,7 +289,7 @@ function getAuthenticationMethod(
} }
function getUserAuthenticationMethod(defaultAuthenticationMethod, match) { function getUserAuthenticationMethod(defaultAuthenticationMethod, match) {
return new Promise((resolve) => { return new Promise(resolve => {
try { try {
Meteor.subscribe('user-authenticationMethod', match, { Meteor.subscribe('user-authenticationMethod', match, {
onReady() { onReady() {

View file

@ -2,25 +2,24 @@ template(name="myCardsHeaderBar")
if currentUser if currentUser
h1 h1
//a.back-btn(href="{{pathFor 'home'}}") //a.back-btn(href="{{pathFor 'home'}}")
// // i.fa.fa-chevron-left
i.fa.fa-chevron-left | 📋
i.fa.fa-list
| {{_ 'my-cards'}} | {{_ 'my-cards'}}
.board-header-btns.left .board-header-btns.left
a.board-header-btn.js-my-cards-view-change(title="{{_ 'myCardsViewChange-title'}}") a.board-header-btn.js-my-cards-view-change(title="{{_ 'myCardsViewChange-title'}}")
i.fa.fa-caret-down | ▼
if $eq myCardsView 'boards' if $eq myCardsView 'boards'
i.fa.fa-list | 📋
| {{_ 'myCardsViewChange-choice-boards'}} | {{_ 'myCardsViewChange-choice-boards'}}
if $eq myCardsView 'table' if $eq myCardsView 'table'
i.fa.fa-bar-chart | 📊
| {{_ 'myCardsViewChange-choice-table'}} | {{_ 'myCardsViewChange-choice-table'}}
template(name="myCardsModalTitle") template(name="myCardsModalTitle")
if currentUser if currentUser
h2 h2
i.fa.fa-keyboard-o | ⌨️
| {{_ 'my-cards'}} | {{_ 'my-cards'}}
template(name="myCards") template(name="myCards")
@ -73,8 +72,7 @@ template(name="myCards")
.my-cards-card-title-table .my-cards-card-title-table
| {{card.title}} | {{card.title}}
//a.minicard-wrapper(href=card.originRelativeUrl) //a.minicard-wrapper(href=card.originRelativeUrl)
// // | {{card.title}}
| {{card.title}}
td td
| {{list.title}} | {{list.title}}
td td
@ -104,15 +102,15 @@ template(name="myCardsViewChangePopup")
li li
with "myCardsViewChange-choice-boards" with "myCardsViewChange-choice-boards"
a.js-my-cards-view-boards a.js-my-cards-view-boards
i.fa.fa-list | 📋
| {{_ 'myCardsViewChange-choice-boards'}} | {{_ 'myCardsViewChange-choice-boards'}}
if $eq Utils.myCardsView "boards" if $eq Utils.myCardsView "boards"
i.fa.fa-check | ✅
hr hr
li li
with "myCardsViewChange-choice-table" with "myCardsViewChange-choice-table"
a.js-my-cards-view-table a.js-my-cards-view-table
i.fa.fa-bar-chart | 📊
| {{_ 'myCardsViewChange-choice-table'}} | {{_ 'myCardsViewChange-choice-table'}}
if $eq Utils.myCardsView "table" if $eq Utils.myCardsView "table"
i.fa.fa-check | ✅

View file

@ -93,44 +93,25 @@
max-height: inherit; max-height: inherit;
} }
/* Fix overflow in the Member Settings (member menu) popup:
the popup itself gets a max-height inline style, but the header consumes space.
Make the header overlay the scrollable area so the list can't spill out. */
.pop-over[data-popup="memberMenuPopup"] {
overflow: hidden;
}
.pop-over[data-popup="memberMenuPopup"] > .header {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-bottom: 0;
z-index: 1;
}
.pop-over[data-popup="memberMenuPopup"] > .content-wrapper {
padding-top: calc(4.5vh + 1vh);
box-sizing: border-box;
}
/* Admin edit popups: use full height */ /* Admin edit popups: use full height */
.pop-over[data-popup="editUserPopup"], .pop-over[data-popup="editUser"],
.pop-over[data-popup="editOrgPopup"], .pop-over[data-popup="editOrg"],
.pop-over[data-popup="editTeamPopup"] { .pop-over[data-popup="editTeam"] {
height: calc(100vh - 20px) !important; height: calc(100vh - 20px) !important;
max-height: calc(100vh - 20px) !important; max-height: calc(100vh - 20px) !important;
} }
.pop-over[data-popup="editUserPopup"] .content-wrapper, .pop-over[data-popup="editUser"] .content-wrapper,
.pop-over[data-popup="editOrgPopup"] .content-wrapper, .pop-over[data-popup="editOrg"] .content-wrapper,
.pop-over[data-popup="editTeamPopup"] .content-wrapper { .pop-over[data-popup="editTeam"] .content-wrapper {
max-height: calc(100vh - 80px) !important; /* Subtract header height */ max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important; height: calc(100vh - 80px) !important;
overflow-y: auto !important; overflow-y: auto !important;
} }
.pop-over[data-popup="editUserPopup"] .content-container, .pop-over[data-popup="editUser"] .content-container,
.pop-over[data-popup="editOrgPopup"] .content-container, .pop-over[data-popup="editOrg"] .content-container,
.pop-over[data-popup="editTeamPopup"] .content-container { .pop-over[data-popup="editTeam"] .content-container {
max-height: calc(100vh - 80px) !important; /* Subtract header height */ max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important; height: calc(100vh - 80px) !important;
} }
@ -142,7 +123,7 @@
} }
/* Specific styling for language popup list */ /* Specific styling for language popup list */
.pop-over[data-popup="changeLanguagePopup"] .pop-over-list { .pop-over[data-popup="changeLanguage"] .pop-over-list {
max-height: none; max-height: none;
overflow: visible; overflow: visible;
height: auto; height: auto;
@ -150,69 +131,46 @@
} }
/* Ensure content div in language popup contains all items */ /* Ensure content div in language popup contains all items */
.pop-over[data-popup="changeLanguagePopup"] .content { .pop-over[data-popup="changeLanguage"] .content {
height: auto; height: auto;
/* Remove forced min-height to avoid top gap */ min-height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
/* Ensure hidden stack pages truly take no space */ /* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguagePopup"] .content.no-height { .pop-over[data-popup="changeLanguage"] .content-wrapper {
min-height: 0 !important; max-height: inherit; /* Use dynamic height from JavaScript */
height: 0 !important; }
padding: 0 !important;
margin: 0 !important; .pop-over[data-popup="changeLanguage"] .content-container {
visibility: hidden !important; max-height: inherit; /* Use dynamic height from JavaScript */
} }
/* Make language popup extend to bottom of browser window */ /* Make language popup extend to bottom of browser window */
.pop-over[data-popup="changeLanguagePopup"] { .pop-over[data-popup="changeLanguage"] {
position: fixed !important; height: calc(100vh - 30px);
bottom: 0 !important; min-height: 300px;
top: auto !important; /* Adjust positioning to move popup 30px higher */
left: auto !important; transform: translateY(-30px);
right: 20px !important;
width: auto !important;
max-width: 450px !important;
height: 100vh !important;
max-height: 100vh !important;
min-height: 300px !important;
display: flex !important;
flex-direction: column !important;
margin: 0 !important;
} }
/* Allow dynamic height for Change Language popup */ .pop-over[data-popup="changeLanguage"] .content-wrapper {
.pop-over[data-popup="changeLanguagePopup"] .header { height: calc(100% - 50px); /* Subtract header height more precisely */
flex-shrink: 0 !important; min-height: 250px;
height: auto !important; overflow-y: auto;
max-height: none; /* Remove any max-height constraints */
display: flex;
flex-direction: column;
} }
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper { .pop-over[data-popup="changeLanguage"] .content-container {
flex: 1 !important; height: auto; /* Let content determine height */
overflow-y: auto !important; min-height: 250px;
overflow-x: hidden !important; max-height: none; /* Remove any max-height constraints */
min-height: 0 !important; flex: 1;
max-height: none !important; display: flex;
height: auto !important; flex-direction: column;
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content-container {
height: auto !important;
max-height: none !important;
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content {
height: auto !important;
max-height: none !important;
padding-bottom: 50px !important;
width: 100% !important;
} }
/* Date popup sizing for native HTML inputs */ /* Date popup sizing for native HTML inputs */
@ -335,8 +293,6 @@
overflow-y: auto !important; overflow-y: auto !important;
} }
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button, .pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button, .pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button, .pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
@ -431,6 +387,9 @@
margin: 0; margin: 0;
visibility: hidden; visibility: hidden;
} }
.pop-over .quiet {
/* padding: 6px 6px 4px;*/
}
.pop-over.search-over { .pop-over.search-over {
background: #f0f0f0; background: #f0f0f0;
min-height: 14vh; min-height: 14vh;
@ -497,7 +456,6 @@
/* flex-wrap:wrap;*/ /* flex-wrap:wrap;*/
gap:5px; gap:5px;
align-items: center; align-items: center;
color: #000 !important;
} }
.pop-over-list li > a > .member{ .pop-over-list li > a > .member{
align-self: flex-start; align-self: flex-start;
@ -558,7 +516,6 @@
position: absolute; position: absolute;
top: 6px; top: 6px;
right: 12px; right: 12px;
color: #3cb500;
} }
.pop-over-list .pop-over-list.checkable li.active a { .pop-over-list .pop-over-list.checkable li.active a {
padding-right: 28px; padding-right: 28px;
@ -566,10 +523,6 @@
.pop-over-list .pop-over-list.checkable li.active a .fa-check { .pop-over-list .pop-over-list.checkable li.active a .fa-check {
display: block; display: block;
} }
/* Grey check icons when grey icons setting is enabled */
body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
color: #7a7a7a;
}
.pop-over.miniprofile .header { .pop-over.miniprofile .header {
border-bottom-color: transparent; border-bottom-color: transparent;
height: 30px; height: 30px;
@ -615,10 +568,6 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
overflow: hidden; overflow: hidden;
margin-top: 0px; margin-top: 0px;
border: 0px solid #dbdbdb; border: 0px solid #dbdbdb;
/* Ensure popups appear above card details on mobile */
z-index: 999999 !important;
/* iOS Safari scrolling fix */
-webkit-overflow-scrolling: touch;
} }
.pop-over .header { .pop-over .header {
color: #fff; color: #fff;
@ -703,23 +652,3 @@ body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
transform: none !important; transform: none !important;
} }
} }
/* Force full-screen popups in mobile mode regardless of screen width */
body.mobile-mode .pop-over {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
}
body.mobile-mode .pop-over .content-wrapper {
width: 100% !important;
height: calc(100vh - 48px) !important;
max-height: calc(100vh - 48px) !important;
overflow-y: auto !important;
overflow-x: hidden !important;
}

View file

@ -2,14 +2,13 @@
class="{{#unless title}}miniprofile{{/unless}}" class="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}" class="{{#unless title}}no-title{{/unless}}"
data-popup="{{popupName}}"
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}") style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
.header .header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}") a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")
i.fa.fa-caret-left | ◀️
span.header-title= title span.header-title= title
a.close-btn.js-close-pop-over a.close-btn.js-close-pop-over
i.fa.fa-times-thin | ❌
.content-wrapper .content-wrapper
//- //-
We display the all stack of popup content next to each other and move We display the all stack of popup content next to each other and move

View file

@ -1,29 +0,0 @@
template(name="supportHeaderBar")
h1
if isSupportEnabled
= supportTitle
else
| {{_ 'support'}}
template(name="support")
.support-page
if isSupportPublic
if isSupportEnabled
.support-page-content
+viewer
| {{supportContent}}
else
.support-page-content
| {{_ 'support-info-not-added-yet'}}
else
if currentUser
if isSupportEnabled
.support-page-content
+viewer
| {{supportContent}}
else
.support-page-content
| {{_ 'support-info-not-added-yet'}}
else
.support-page-content
| {{_ 'support-info-only-for-logged-in-users'}}

View file

@ -1,41 +0,0 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
// Shared helpers for both support templates
const supportHelpers = {
supportTitle() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportTitle ? setting.supportTitle : TAPi18n.__('support');
},
supportContent() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportPageText ? setting.supportPageText : TAPi18n.__('support-info-not-added-yet');
},
isSupportEnabled() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportPageEnabled;
},
isSupportPublic() {
const setting = ReactiveCache.getCurrentSetting();
return setting && setting.supportPagePublic;
}
};
// Main support page component
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
Meteor.subscribe('setting');
},
...supportHelpers
}).register('support');
// Header bar component
BlazeComponent.extendComponent({
onCreated() {
Meteor.subscribe('setting');
},
...supportHelpers
}).register('supportHeaderBar');

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