mirror of
https://github.com/wekan/wekan.git
synced 2026-02-08 09:24:20 +01:00
Compare commits
No commits in common. "main" and "v8.14" have entirely different histories.
556 changed files with 18798 additions and 61998 deletions
|
|
@ -97,8 +97,9 @@
|
|||
"Avatar": true,
|
||||
"Avatars": true,
|
||||
"BlazeComponent": false,
|
||||
|
||||
"BlazeLayout": false,
|
||||
"CollectionHooks": false,
|
||||
"DocHead": false,
|
||||
"ESSearchResults": false,
|
||||
"FastRender": false,
|
||||
"FlowRouter": false,
|
||||
|
|
|
|||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1,4 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: wekan
|
||||
custom: ['https://wekan.fi/commercial-support/']
|
||||
|
|
|
|||
56
.github/ISSUE_TEMPLATE.md
vendored
Normal file
56
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
|
||||
|
||||
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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.
|
||||
|
||||
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -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.
|
||||
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/security-report.yml
vendored
14
.github/ISSUE_TEMPLATE/security-report.yml
vendored
|
|
@ -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.
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/ucs-issue.yml
vendored
23
.github/ISSUE_TEMPLATE/ucs-issue.yml
vendored
|
|
@ -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.
|
||||
2
.github/workflows/depsreview.yaml
vendored
2
.github/workflows/depsreview.yaml
vendored
|
|
@ -9,6 +9,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
|
|
|||
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
|
|
@ -32,13 +32,13 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
|
|
|
|||
2
.github/workflows/dockerimage.yml
vendored
2
.github/workflows/dockerimage.yml
vendored
|
|
@ -15,6 +15,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Build the Docker image
|
||||
run: docker build . --file Dockerfile --tag wekan:$(date +%s)
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
|
|||
20
.github/workflows/test_suite.yml
vendored
20
.github/workflows/test_suite.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v6
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
# needs: [lintcode]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v6
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
# needs: [lintcode,lintstyle]
|
||||
# steps:
|
||||
# - name: checkout
|
||||
# uses: actions/checkout@v6
|
||||
# uses: actions/checkout@v5
|
||||
#
|
||||
# - name: setup node
|
||||
# uses: actions/setup-node@v1
|
||||
|
|
@ -90,12 +90,12 @@ jobs:
|
|||
|
||||
# CHECKOUTS
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# CACHING
|
||||
- name: Install Meteor
|
||||
id: cache-meteor-install
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.meteor
|
||||
key: v1-meteor-${{ hashFiles('.meteor/versions') }}
|
||||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
|
||||
- name: Cache NPM dependencies
|
||||
id: cache-meteor-npm
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: v1-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
|
@ -113,7 +113,7 @@ jobs:
|
|||
|
||||
- name: Cache Meteor build
|
||||
id: cache-meteor-build
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.meteor/local/resolver-result-cache.json
|
||||
|
|
@ -136,7 +136,7 @@ jobs:
|
|||
run: sh ./test-wekan.sh -cv
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
|
@ -147,10 +147,10 @@ jobs:
|
|||
needs: [tests]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download coverage
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: coverage-folder
|
||||
path: .coverage/
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@ es5-shim@4.8.0
|
|||
|
||||
# Collections
|
||||
aldeed:collection2
|
||||
reywood:publish-composite
|
||||
cottz:publish-relations
|
||||
dburles:collection-helpers
|
||||
idmontie:migrations
|
||||
easy:search
|
||||
mongo@1.16.8
|
||||
mquandalle:collection-mutations
|
||||
|
||||
# Account system
|
||||
accounts-password@2.4.0
|
||||
useraccounts:core
|
||||
useraccounts:flow-routing
|
||||
useraccounts:unstyled
|
||||
simple:rest-accounts-password
|
||||
wekan-ldap
|
||||
|
|
@ -40,7 +44,11 @@ reactive-dict@1.3.1
|
|||
session@1.2.1
|
||||
tracker@1.3.3
|
||||
underscore@1.0.13
|
||||
arillo:flow-router-helpers
|
||||
audit-argument-checks@1.0.7
|
||||
kadira:dochead
|
||||
mquandalle:autofocus
|
||||
ongoworks:speakingurl
|
||||
raix:handlebar-helpers
|
||||
http@2.0.0! # force new http package
|
||||
|
||||
|
|
@ -50,6 +58,9 @@ http@2.0.0! # force new http package
|
|||
# UI components
|
||||
ostrio:i18n
|
||||
reactive-var@1.0.12
|
||||
mousetrap:mousetrap
|
||||
mquandalle:jquery-textcomplete
|
||||
mquandalle:mousetrap-bindglobal
|
||||
templates:tabs
|
||||
meteor-autosize
|
||||
shell-server@0.5.0
|
||||
|
|
@ -61,25 +72,25 @@ meteorhacks:subs-manager
|
|||
meteorhacks:aggregate@1.3.0
|
||||
wekan-markdown
|
||||
konecty:mongo-counter
|
||||
quave:synced-cron
|
||||
percolate:synced-cron
|
||||
ostrio:cookies
|
||||
ostrio:files@2.3.0
|
||||
pascoual:pdfkit
|
||||
lmieulet:meteor-coverage
|
||||
meteortesting:mocha@2.0.3
|
||||
aldeed:simple-schema
|
||||
matb33:collection-hooks
|
||||
simple:json-routes
|
||||
kadira:flow-router
|
||||
spacebars
|
||||
service-configuration@1.3.2
|
||||
communitypackages:picker
|
||||
minifier-css@1.6.4
|
||||
blaze
|
||||
kadira:blaze-layout
|
||||
peerlibrary:blaze-components
|
||||
ejson@1.1.3
|
||||
logging@1.3.3
|
||||
wekan-fullcalendar
|
||||
momentjs:moment@2.29.3
|
||||
wekan-fontawesome
|
||||
|
||||
useraccounts:flow-routing-extra
|
||||
ostrio:flow-router-extra
|
||||
# wekan-fontawesome
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
METEOR@2.16
|
||||
METEOR@2.14
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
accounts-base@2.2.11
|
||||
accounts-oauth@1.4.4
|
||||
accounts-base@2.2.10
|
||||
accounts-oauth@1.4.3
|
||||
accounts-password@2.4.0
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
|
|
@ -7,6 +7,7 @@ aldeed:schema-deny@1.1.0
|
|||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
allow-deny@1.1.1
|
||||
arillo:flow-router-helpers@0.5.2
|
||||
audit-argument-checks@1.0.7
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.10.5
|
||||
|
|
@ -19,25 +20,29 @@ boilerplate-generator@1.7.2
|
|||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.5.1
|
||||
check@1.4.1
|
||||
check@1.3.2
|
||||
coffeescript@2.7.0
|
||||
coffeescript-compiler@2.4.1
|
||||
communitypackages:picker@1.1.1
|
||||
cottz:publish-relations@2.0.8
|
||||
dburles:collection-helpers@1.1.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.2
|
||||
ddp-common@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.2.1
|
||||
ddp-server@2.7.1
|
||||
ddp-server@2.7.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
easy:search@2.2.1
|
||||
easysearch:components@2.2.2
|
||||
easysearch:core@2.2.2
|
||||
ecmascript@0.16.8
|
||||
ecmascript-runtime@0.8.1
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.3
|
||||
email@2.2.6
|
||||
email@2.2.5
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.4
|
||||
geojson-utils@1.0.11
|
||||
|
|
@ -46,12 +51,16 @@ html-tools@1.1.3
|
|||
htmljs@1.1.1
|
||||
http@2.0.0
|
||||
id-map@1.1.1
|
||||
idmontie:migrations@1.0.3
|
||||
inter-process-messaging@0.1.1
|
||||
jquery@3.0.0
|
||||
kadira:blaze-layout@2.3.0
|
||||
kadira:dochead@1.5.0
|
||||
kadira:flow-router@2.12.1
|
||||
konecty:mongo-counter@0.0.5_3
|
||||
lmieulet:meteor-coverage@1.1.4
|
||||
localstorage@1.2.0
|
||||
logging@1.3.4
|
||||
logging@1.3.3
|
||||
matb33:collection-hooks@1.3.0
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.11.5
|
||||
|
|
@ -65,39 +74,45 @@ meteortesting:browser-tests@1.4.2
|
|||
meteortesting:mocha@2.1.0
|
||||
meteortesting:mocha-core@8.0.1
|
||||
minifier-css@1.6.4
|
||||
minifier-js@2.8.0
|
||||
minifier-js@2.7.5
|
||||
minifiers@1.1.8-faster-rebuild.0
|
||||
minimongo@1.9.4
|
||||
minimongo@1.9.3
|
||||
modern-browsers@0.1.10
|
||||
modules@0.20.0
|
||||
modules-runtime@0.13.1
|
||||
momentjs:moment@2.29.3
|
||||
mongo@1.16.10
|
||||
mongo@1.16.8
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
mousetrap:mousetrap@1.4.6_1
|
||||
mquandalle:autofocus@1.0.0
|
||||
mquandalle:collection-mutations@0.1.0
|
||||
mquandalle:jade@0.4.9
|
||||
mquandalle:jade-compiler@0.4.5
|
||||
mquandalle:jquery-textcomplete@0.8.0_1
|
||||
mquandalle:mousetrap-bindglobal@0.0.1
|
||||
msavin:usercache@1.8.0
|
||||
npm-mongo@4.17.2
|
||||
oauth@2.2.1
|
||||
oauth2@1.3.2
|
||||
observe-sequence@1.0.21
|
||||
ongoworks:speakingurl@1.1.0
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
ostrio:cstorage@4.0.1
|
||||
ostrio:files@2.3.3
|
||||
ostrio:flow-router-extra@3.10.1
|
||||
ostrio:i18n@3.2.1
|
||||
pascoual:pdfkit@1.0.7
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:base-component@0.17.1
|
||||
peerlibrary:blaze-components@0.23.0
|
||||
peerlibrary:computed-field@0.10.0
|
||||
peerlibrary:data-lookup@0.3.0
|
||||
peerlibrary:reactive-field@0.6.0
|
||||
percolate:synced-cron@1.5.2
|
||||
promise@0.12.2
|
||||
quave:synced-cron@2.2.1
|
||||
raix:eventemitter@0.1.3
|
||||
raix:handlebar-helpers@0.2.5
|
||||
random@1.2.1
|
||||
|
|
@ -107,9 +122,8 @@ reactive-dict@1.3.1
|
|||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
reywood:publish-composite@1.9.0
|
||||
routepolicy@1.1.1
|
||||
service-configuration@1.3.4
|
||||
service-configuration@1.3.3
|
||||
session@1.2.1
|
||||
sha@1.0.9
|
||||
shell-server@0.5.0
|
||||
|
|
@ -130,21 +144,21 @@ templating-tools@1.2.2
|
|||
tracker@1.3.3
|
||||
typescript@4.9.5
|
||||
ui@1.0.13
|
||||
underscore@1.6.1
|
||||
underscore@1.0.13
|
||||
url@1.3.2
|
||||
useraccounts:core@1.16.2
|
||||
useraccounts:flow-routing-extra@1.1.0
|
||||
useraccounts:flow-routing@1.15.0
|
||||
useraccounts:unstyled@1.14.2
|
||||
webapp@1.13.8
|
||||
webapp@1.13.6
|
||||
webapp-hashing@1.1.1
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-lockout@1.1.0
|
||||
wekan-accounts-lockout@1.0.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-accounts-sandstorm@0.8.0
|
||||
wekan-fontawesome@6.4.2
|
||||
wekan-fullcalendar@3.10.5
|
||||
wekan-ldap@0.0.2
|
||||
wekan-markdown@1.0.9
|
||||
wekan-oidc@1.0.12
|
||||
yasaricli:slugify@0.0.7
|
||||
zodern:types@1.0.13
|
||||
zimme:active-route@2.3.2
|
||||
zodern:types@1.0.10
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
lang_map = te_IN: te-IN, es_AR: es-AR, es_419: es-LA, es_TX: es-TX, he_IL: he-IL, zh_CN: zh-CN, ar_EG: ar-EG, cs_CZ: cs-CZ, fa_IR: fa-IR, ms_MY: ms-MY, nl_NL: nl-NL, de_CH: de-CH, en_IT: en-IT, uz_UZ: uz-UZ, fr_CH: fr-CH, hi_IN: hi-IN, et_EE: et-EE, es_PE: es-PE, es_MX: es-MX, gl_ES: gl-ES, mn_MN: mn, sl_SI: sl, zh_TW: zh-TW, ast_ES: ast-ES, es_CL: es-CL, ja_JP: ja, lv_LV: lv, ro_RO: ro-RO, az_AZ: az-AZ, cy_GB: cy-GB, gu_IN: gu-IN, pl_PL: pl-PL, vep: ve-PP, en_BR: en-BR, en@ysv: en-YS, hu_HU: hu, ko_KR: ko-KR, pt_BR: pt-BR, zh_HK: zh-HK, zu_ZA: zu-ZA, en_MY: en-MY, ja-Hira: ja-HI, fi_FI: fi, vec: ve-CC, vi_VN: vi-VN, fr_FR: fr-FR, id_ID: id, zh_Hans: zh-Hans, en_DE: en-DE, en_GB: en-GB, el_GR: el-GR, uk_UA: uk-UA, az@latin: az-LA, de_AT: de-AT, uz@Latn: uz-LA, vls: vl-SS, ar_DZ: ar-DZ, bg_BG: bg, es_PY: es-PY, fy_NL: fy-NL, uz@Arab: uz-AR, ru_UA: ru-UA, war: wa-RR, zh_CN.GB2312: zh-GB
|
||||
|
||||
[o:wekan:p:wekan:r:application]
|
||||
file_filter = imports/i18n/data/<lang>.i18n.json
|
||||
|
|
|
|||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -9,8 +9,5 @@
|
|||
"TERM": "xterm-256color"
|
||||
},
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"terminal.integrated.shellArgs.linux": [
|
||||
"-l"
|
||||
],
|
||||
"files.simpleDialog.enable": true
|
||||
"terminal.integrated.shellArgs.linux": ["-l"]
|
||||
}
|
||||
534
CHANGELOG.md
534
CHANGELOG.md
|
|
@ -8,548 +8,20 @@ Newest WeKan at these platforms:
|
|||
- [Mac amd64, works also with Rosetta2 at Apple Silicon](https://github.com/wekan/wekan/blob/main/docs/Platforms/Propietary/Mac.md)
|
||||
- https://wekan.fi/install/
|
||||
- Snap Candidate amd64
|
||||
- Docker amd64/arm64/s390x
|
||||
- Docker amd64
|
||||
- Kubernetes Docker amd64
|
||||
- Bitnami MongoDB Docker images do not exist anymore. [MongoDump/MongoRestore to groundhog2k MongoDB images](https://github.com/wekan/charts/issues/45)
|
||||
|
||||
Fixing other platforms In Progress.
|
||||
|
||||
- [Node.js 14.21.4](https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4) or [Node.js 14.21.3](https://nodejs.org/dist/latest-v14.x/)
|
||||
- MongoDB 6.x and 7.x, or [FerretDB2/PostgreSQL](https://github.com/wekan/wekan/blob/main/docs/Databases/FerretDB2-PostgreSQL.md)
|
||||
- Node.js 14.x at https://github.com/wekan/node-v14-esm/releases/tag/v14.21.4 and https://nodejs.org/dist/latest-v14.x/
|
||||
- MongoDB 6.x and 7.x, or FerretDB/PostgreSQL https://blog.ferretdb.io/building-project-management-stack-wekan-ferretdb/
|
||||
|
||||
[Upgrade WeKan](https://wekan.fi/upgrade/)
|
||||
|
||||
WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible.
|
||||
Those are fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
|
||||
|
||||
WeKan 8.00-8.24 used Colorful Unicode Emoji Icons, versions before and after use mostly Font Awesome 4.7 icons.
|
||||
|
||||
# v8.31 2026-02-08 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix Copy Card and Move Card](https://github.com/wekan/wekan/commit/f8aa487e9118264f4d96c4d0cde384bcaf05e0a0).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.30 2026-02-08 WeKan ® release
|
||||
|
||||
This release reverts the following new features and adds the following fixes:
|
||||
|
||||
- [Reverted New UI Design of WeKan v8.29 and added more fixes and performance improvements](https://github.com/wekan/wekan/commit/1b8b8d2eef5b56654026597ae445f3f20ad886b2).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.29 2026-02-07 WeKan ® release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- New UI Design.
|
||||
[Part 1](https://github.com/wekan/wekan/pull/6131),
|
||||
[Part 2](https://github.com/wekan/wekan/pull/6133).
|
||||
Thanks to Chostakovitch.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix List widths](https://github.com/wekan/wekan/pull/6129).
|
||||
Thanks to KhaoulaMaleh.
|
||||
- [Fix extra space at RTL need margin](https://github.com/wekan/wekan/commit/4456bc13609b2d0e944ee71a82df200060a601b2).
|
||||
Thanks to mimZD and xet7.
|
||||
- [Fix No Add Card + etc](https://github.com/wekan/wekan/commit/55710835fe8879775b73c8bc921bac5febf552a2).
|
||||
Thanks to mimZD and xet7.
|
||||
- [Removed extra file](https://github.com/wekan/wekan/commit/0987154a7fea89b0416f48d9bffd5fa7fba9908a).
|
||||
Thanks to xet7.
|
||||
- [Added missing linefeeds](https://github.com/wekan/wekan/commit/0ae9865fcbad42966988225393fa66bca49cf14e).
|
||||
Thanks to xet7.
|
||||
- [Fix Notifications from not allowed Boards](https://github.com/wekan/wekan/commit/0a92e896f8d2cf0677891857d163ada336a45c61).
|
||||
Thanks to FK-PATZ3 and xet7.
|
||||
- [Fix move and copy popup duplicate view](https://github.com/wekan/wekan/commit/631c250f403172937b76ddd37bab54bc9b6dbb78).
|
||||
Thanks to mimZD and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.28 2026-02-05 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Bump docker/login-action from 3.6.0 to 3.7.0](https://github.com/wekan/wekan/pull/6122).
|
||||
Thanks to dependabot.
|
||||
- [Updated meteor-node-stubs](https://github.com/wekan/wekan/commit/6c2e2f271d6343b347224430a4eedfe54db2d838).
|
||||
Thanks to Meteor developers.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fixed text truncation at quick-access board link bar](https://github.com/wekan/wekan/pull/6121).
|
||||
Thanks to KhaoulaMaleh.
|
||||
- [Improved cardDetails.css for better UI](https://github.com/wekan/wekan/pull/6124).
|
||||
Thanks to AymenHassini19.
|
||||
- [Fixed Jade syntax at header](https://github.com/wekan/wekan/commit/c31758960f5372e88f47e8d081404294751284c8).
|
||||
Thanks to xet7.
|
||||
- [Await async setDone before closing popup in copy/move dialogs](https://github.com/wekan/wekan/pull/6126).
|
||||
Thanks to harryadel.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.27 2026-01-31 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Updated MongoDB to 7.0.29 at Windows install docs](https://github.com/wekan/wekan/commit/b55e1bbd409f76bd0388d19d4d0a8420cee8df96).
|
||||
Thanks to MongoDB developers.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix async/await in copy/move card operations](https://github.com/wekan/wekan/pull/6120).
|
||||
Thanks to harryadel.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.26 2026-01-31 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Migrate wekan-accounts-lockout to async API for Meteor 3.0](https://github.com/wekan/wekan/pull/6113).
|
||||
Thanks to harryadel.
|
||||
- Added Docs: Spreadsheet vs Kanban.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/a0a8d0186cbc7fefe38f72244723bcff292ae2f4),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/37d0daee590ab48cbfa1672e4bc5efd95d341211).
|
||||
Thanks to xet7.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/03439d1bccf82511870eed7301b621b1d495941b).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Reduce visual overflow in Member Settings menu by extending container height](https://github.com/wekan/wekan/pull/6104).
|
||||
Thanks to AymenHassini19.
|
||||
- [Fix Card copy menu is not displayed](https://github.com/wekan/wekan/commit/0b891464b907b272e075d8aafd3ce29e704739cf).
|
||||
Thanks to xet7.
|
||||
- [Fix Bug: Rules view translation not is not shown correctly](https://github.com/wekan/wekan/commit/f73eab23f997efe5347aa1f06515bf355cfe7ed5).
|
||||
Thanks to cactus7as and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.25 2026-01-28 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [Floppybleed](https://wekan.fi/hall-of-fame/floppybleed/):
|
||||
|
||||
- [Fix Filebleed of Floppybleed](https://github.com/wekan/wekan/commit/a419d831a408f251c798f5410375b20afd98c04b).
|
||||
Thanks to Luke Hebenstreit Twitter lheben_ and xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Updated code counts](https://github.com/wekan/wekan/commit/2f25f47d0ba4c7f543264cd7fe2ed117ab0ec9ee).
|
||||
Thanks to xet7.
|
||||
- Updated FerretDB 2 / PostgreSQL docs location.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/710d522e069b7521b6c2ec4f93f1491a897cf2b4),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/0ede9d6d93a688f24fc36c0c456e184a0aa6af8c),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/bf5d50e8a9fce327a16b069932fa3e13c6d81978).
|
||||
Thanks to xet7.
|
||||
- [Updated Dockerfile](https://github.com/wekan/wekan/commit/d298ab7486d489d353fc410232a9dcdd68501c72).
|
||||
Thanks to xet7.
|
||||
- Docker for Linux amd64/arm64/s390x.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/38711f0a29bf37d1e0a3fd9c8a9bcfb2442934b3),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/e72019fa55ef6142767fd83e928bf2a0a966f9e6),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/b2c7c7f55b5136bc91251cd57125316ec622d4a3),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/98e5adfba80ee935b2a1293851d88812ad707b78),
|
||||
[Part 5](https://github.com/wekan/wekan/commit/60846a44959d46262672c6a3048bd76d829c03bf),
|
||||
[Part 6](https://github.com/wekan/wekan/commit/7ff174cf660f43dfbb471b29d75820f527771bbd).
|
||||
Thanks to xet7.
|
||||
- [Most Unicode Icons back to Font Awesome 4.7 for better accessibility. Less always visible buttons, More at ☰ Menu](https://github.com/wekan/wekan/commit/7ad04f45353e1628881fec310caedf7625a34d4d).
|
||||
Thanks to xet7.
|
||||
- [Updated to MongoDB 7.0.29 at Snap Candidate](https://github.com/wekan/wekan/commit/ac70fe28488c09364133a65fbc80f5a819a1e4bf).
|
||||
Thanks to developers of MongoDB.
|
||||
- [Updated to MongoDB 7.0.29 at Helm Charts](https://github.com/wekan/charts/commit/8169739260b6f104c4d011dac5a4bf5485db8b45).
|
||||
Thanks to developers of MongoDB.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix autofocus](https://github.com/wekan/wekan/commit/440f553de0baf460acc891ee5864f84bb982104a).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.24 2026-01-24 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- Secure Sandbox for VSCode at Debian 13 amd64.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/639ac9549f88069d8569de777c533ab4c9438088),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/cc8b771eb448199fa23a87955cf9fa1a504ba8d2).
|
||||
Thanks to xet7.
|
||||
- [Updated build scripts and docs to Meteor 2.16](https://github.com/wekan/wekan/commit/1d374db0f3ed35a0463b5f89ca2d01078e245d11).
|
||||
Thanks to xet7.
|
||||
- [Replace mquandalle:collection-mutations with collection helpers](https://github.com/wekan/wekan/pull/6086).
|
||||
Thanks to harryadel.
|
||||
- [Replace ongoworks:speakingurl with limax](https://github.com/wekan/wekan/pull/6087).
|
||||
Thanks to harryadel.
|
||||
- [Migrate createIndex to createIndexAsync](https://github.com/wekan/wekan/pull/6093).
|
||||
Thanks to harryadel.
|
||||
- [Remove idmontie:migrations](https://github.com/wekan/wekan/pull/6095).
|
||||
Thanks to harryadel.
|
||||
- Remove mquandalle:autofocus.
|
||||
[Part 1](https://github.com/wekan/wekan/pull/6088),
|
||||
[Part 2](https://github.com/wekan/wekan/pull/6096).
|
||||
Thanks to harryadel.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.23 2026-01-21 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Migrate from percolate:synced-cron to quave:synced-cron](https://github.com/wekan/wekan/pull/6080).
|
||||
Thanks to harryadel.
|
||||
- [Replace mousetrap](https://github.com/wekan/wekan/pull/6082).
|
||||
Thanks to harryadel.
|
||||
- [Remove kadira:dochead](https://github.com/wekan/wekan/pull/6083).
|
||||
Thanks to harryadel.
|
||||
- [Replace cottz:publish-relations with reywood:publish-composite](https://github.com/wekan/wekan/pull/6084).
|
||||
Thanks to harryadel.
|
||||
- [Bump tar from 7.5.3 to 7.5.6](https://github.com/wekan/wekan/pull/6085).
|
||||
Thanks to dependabot.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/04bfa0e6ba278a9d6544a678d1fba3ea71841062).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fixed newly created "Default" swimlane are displayed as "key 'default (LOCALE)' returned an object instead of string"](https://github.com/wekan/wekan/commit/ce55f0d8f432922ca4c0e3d28b1fb0e826d8008f).
|
||||
Thanks to brlin-tw and xet7.
|
||||
- [Fix DB migration from 8.19 to 8.21 stuck forever](https://github.com/wekan/wekan/commit/a31a615da6911a2db22d4db86875b31fc951ae96).
|
||||
Thanks to MaccabeeY and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.22 2026-01-20 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fixed Add member and @mentions](https://github.com/wekan/wekan/commit/ad511bd1378afdca7264597900a11ab6b5e09b77).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.21 2026-01-18 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
|
||||
|
||||
- [Security Fix 2: OrgsTeamsBleed](https://github.com/wekan/wekan/commit/cabfeed9a68e21c469bf206d8655941444b9912c).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 3: ChecklistRESTBleed](https://github.com/wekan/wekan/commit/251d49eea94834cf351bb395808f4a56fb4dbb44).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 4: MigrationsBleed2](https://github.com/wekan/wekan/commit/cc35dafef57ef6e44a514a523f9a8d891e74ad8f).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 5: PositionHistoryBleed](https://github.com/wekan/wekan/commit/55576ec17722db094835470b386162c9a662fb60).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 6: SyncLDAPBleed](https://github.com/wekan/wekan/commit/146905a459106b5d00b4f09453a6554255e6965a).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 7: AttachmentMigrationBleed](https://github.com/wekan/wekan/commit/053bf1dfb76ef230db162c64a6ed50ebedf67eee).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 8: MoveStorageBleed](https://github.com/wekan/wekan/commit/c413a7e860bc4d93fe2adcf82516228570bf382d).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 9: ListWIPBleed](https://github.com/wekan/wekan/commit/8c0b4f79d8582932528ec2fdf2a4487c86770fb9).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 10: BoardTitleRESTBleed](https://github.com/wekan/wekan/commit/545566f5663545d16174e0f2399f231aa693ab6e).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 11: CardPubSubBleed](https://github.com/wekan/wekan/commit/0f5a9c38778ca550cbab6c5093470e1e90cb837f).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 12: FixDuplicateBleed](https://github.com/wekan/wekan/commit/4ce181d17249778094f73d21515f7f863f554743).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 13: LinkedBoardActivitiesBleed](https://github.com/wekan/wekan/commit/91a936e07d2976d4246dfe834281c3aaa87f9503).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 14: RulesBleed](https://github.com/wekan/wekan/commit/a787bcddf33ca28afb13ff5ea9a4cb92dceac005).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [Show password at Login and Register pages](https://github.com/wekan/wekan/commit/d30192f7f925a055e6f31723c47ad32b628ff2c0).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Updated Docker build command](https://github.com/wekan/wekan/commit/b88b27689af8c5abf23dd7891780581a2d92001d).
|
||||
Thanks to xet7.
|
||||
- [Updated Windows Bundle build .bat script](https://github.com/wekan/wekan/commit/f0118d52e984628b0e06e36d7b7f90166d18fbf7).
|
||||
Thanks to xet7.
|
||||
- [Updated Linux arm64 bundle build script](https://github.com/wekan/wekan/commit/e2ec50730ff7fd4eb805071bb17fe0c105514f83).
|
||||
Thanks to xet7.
|
||||
- [Updated Linux s390x bundle build script](https://github.com/wekan/wekan/commit/980510d71ad428325645dd53297f4ce20bd12983).
|
||||
Thanks to xet7.
|
||||
- [Bump tar and @mapbox/node-pre-gyp](https://github.com/wekan/wekan/pull/6071).
|
||||
Thanks to dependabot.
|
||||
- [Upgrade to Meteor 2.16](https://github.com/wekan/wekan/pull/6072).
|
||||
Thanks to harryadel.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/95da8966fe3bebc7c5ef2c1fc555de5fa239f8ca).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fixed "Copy card link to clipboard" icon often not working](https://github.com/wekan/wekan/commit/d337afd5d3e8ca719adcde13d2b24d983e0f9926).
|
||||
Thanks to brlin-tw and xet7.
|
||||
- [Fix DB migration from 8.19 to 8.20 is in a loop](https://github.com/wekan/wekan/commit/2fa490d83da858b193ca6a363e1599c5bbe55640).
|
||||
Thanks to MaccabeeY and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.20 2026-01-16 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [Snowbleed](https://wekan.fi/hall-of-fame/snowbleed/):
|
||||
|
||||
- [Security Fix 1: MigrationsBleed](https://github.com/wekan/wekan/commit/cbb1cd78de3e40264a5e047ace0ce27f8635b4e6).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
|
||||
and adds the following features:
|
||||
|
||||
- [Added back feature: Toggle Drag Handles. Improved positions of Add List etc buttons](https://github.com/wekan/wekan/commit/5cb712bee4cf46c6fe13d7dacf4b62298152b894).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/pull/6059).
|
||||
Thanks to dependabot.
|
||||
- [Updated dependencies and published as @wekanteam npm packages to npmjs.com](https://github.com/wekan/wekan/commit/a9a89b501a91ffcdbdd611a05029d9483c59e4db).
|
||||
Thanks to xet7.
|
||||
- Added FerretDB2/PostgreSQL Docs.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/9fb1aeb8272b011c3d0b6b2c26ff7cb498c7b37f),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/f198421f10dd3be9d58f64a242d12ea1ef45fee3),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/9431b2d53014289bebb06567f5662fdcb6dd409c),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/ffd37b9fd9171ca22973d6d0a62baef4a18494f5).
|
||||
Thanks to juri_ at WeKan Libera.Chat IRC and xet7.
|
||||
- [Added s390x firewall Docs](https://github.com/wekan/wekan/commit/ec7c0e6dc3641f43b1a110d285f6ef15c146584a).
|
||||
Thanks to xet7.
|
||||
- Updated GitHub issue templates.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/bd37b88e4d508c1f2712184a27dbbfd9df0e4c4e),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/cf6e6914989a7bf1d79f8b753a0a576c54ad7580),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/4a658dc02a770f8219669dc10bfe1077c760744f).
|
||||
Thanks to xet7.
|
||||
- [Migrate kadira:flow-router to ostrio:flow-router-extra](https://github.com/wekan/wekan/pull/6067), related to Meteor 3 upgrades.
|
||||
Thanks to harryadel.
|
||||
- [Some fixes to make WeKan working after Meteor 3 related router upgrades](https://github.com/wekan/wekan/commit/984a2dcec18fd20ebd1a5add8380d4c13d8303ba).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix attachment download error with non-ASCII filenames](https://github.com/wekan/wekan/pull/6056).
|
||||
Thanks to brlin-tw.
|
||||
- [Swimlane drag button position improvements](https://github.com/wekan/wekan/commit/376a30f8a9c5cc6b5341fda7336244ee1b9983fd).
|
||||
Thanks to TDSCDMA and xet7.
|
||||
- [Removed extra list borders](https://github.com/wekan/wekan/commit/a4f8faa48e3fb6c617cf9c5a398bc7f85b8bae92).
|
||||
Thanks to TDSCDMA and xet7.
|
||||
- [Add back button texts to Filter, Search, Board View and MultiSelection](https://github.com/wekan/wekan/commit/dac7e17500de97febc7ad8f84cd1bf5edab27c52).
|
||||
Thanks to audiocrush and xet7.
|
||||
- [Removed extra pipe character from UI](https://github.com/wekan/wekan/commit/66e79d2df7ecf5526dbae360cf93352657db7fcf).
|
||||
Thanks to xet7.
|
||||
- [Changed find.sh to not search from translations, because I'm trying to find code, not translations](https://github.com/wekan/wekan/commit/58ae2b6c6848235132308611fe3083533e120f72).
|
||||
Thanks to xet7.
|
||||
- [Fixed Change Avatar. Improved Admin Panel: People columns order, selected tab background color. Fixed can not edit existing user at Admin Panel/People/People](https://github.com/wekan/wekan/commit/07186e12a93c56555feb3b7332d43a918abe7f20).
|
||||
Thanks to xet7.
|
||||
- [Fix mentions and notifications drawer](https://github.com/wekan/wekan/commit/20b5e2ab8fd37303cda8305d87d757c1cb9bdd12).
|
||||
Thanks to xet7.
|
||||
- Fix New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/eabb6a239d20530f538d22f94d9cfbebeb847493).
|
||||
Thanks to nazim-oss and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.19 2025-12-29 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [Megableed](https://wekan.fi/hall-of-fame/megableed/):
|
||||
|
||||
- [Security Fix 1: IDOR in setCreateTranslation. Non-admin could change Custom Translation](https://github.com/wekan/wekan/commit/f244a43771f6ebf40218b83b9f46dba6b940d7de).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 2: Private-only board setting can be bypassed](https://github.com/wekan/wekan/commit/7ed76c180ede46ab1dac6b8ad27e9128a272c2c8).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 3: Card comment author spoofing (IDOR) via API](https://github.com/wekan/wekan/commit/67cb47173c1a152d9eaf5469740992b2dacdf62d).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 4: Cross-board card move without destination authorization](https://github.com/wekan/wekan/commit/198509e7600981400353aec6259247b3c04e043e).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 5: Read-only roles can still update cards](https://github.com/wekan/wekan/commit/181f837d8cbae96bdf9dcbd31beaa3653c2c0285).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 6: Checklist delete IDOR: checklist not verified against board/card](https://github.com/wekan/wekan/commit/08a6f084eba09487743a7c807fb4a9000fcfa9ac).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 7: Checklist create IDOR: cardId not verified against boardId](https://github.com/wekan/wekan/commit/5cd875813fdec5a3c40a0358b30a347967c85c14).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 8: Attachments publication leaks metadata without auth](https://github.com/wekan/wekan/commit/6dfa3beb2b6ab23438d0f4395b84bf0749eb4820).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 9: Attachment upload not scoped to card/board relationship](https://github.com/wekan/wekan/commit/1d16955b6d4f0a0282e89c2c1b0415c7597019b8).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
- [Security Fix 10: LDAP filter injection in LDAP auth](https://github.com/wekan/wekan/commit/0b0e16c3eae28bbf453d33a81a9c58ce7db6d5bb).
|
||||
Thanks to [Joshua Rogers](https://joshua.hu) of [Aisle Research](https://aisle.com) and xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [Opened card Checklist menu: Hide finished tasks. Show Checklist at Minicard](https://github.com/wekan/wekan/commit/fbfde81bc8208b718c070a6eeba4b2e2d2ce83ba).
|
||||
Thanks to C0rn3j and xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Helm Chart: Updated MongoDB to 7.0.28 at artifacthub.io](https://github.com/wekan/charts/commit/5e6d344e0b976ce683116b66a1fb8417590115aa).
|
||||
Thanks to xet7 and titver968.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Re-add JS closing class to unicode close announcement symbol](https://github.com/wekan/wekan/pull/6050).
|
||||
Thanks to Chostakovitch.
|
||||
- [Cannot re-arrange lists within swimlanes](https://github.com/wekan/wekan/pull/6052).
|
||||
Thanks to Chostakovitch.
|
||||
- Converted Gantt from js to Jade, and made card title to render markdown at Gantt view.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/2d3bef9033134c3b62cf22179bbee4b6fea81444),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/3af3c9a89d8a4020b6f1ccada7da2ccbec1a8562).
|
||||
Thanks to xet7.
|
||||
- [Fix find.sh work with spaces, for example: ./find.sh "Some text"](https://github.com/wekan/wekan/commit/db4b04d8377523440fd2c36c1633ee74d7b05146).
|
||||
Thanks to xet7.
|
||||
- [Fix copy move card at board and MultiSelect to have numbered target of board, card above or below. Added MultiSelect change color](https://github.com/wekan/wekan/commit/74f1dfde72b9448645552ae28ba8d989d3e823d8).
|
||||
Thanks to mimZD and xet7.
|
||||
- [Fix move card last selection is gone](https://github.com/wekan/wekan/commit/2d87ba18b31ab5d8dc91dce01199cf7b313bd560).
|
||||
Thanks to mimZD and xet7.
|
||||
- [Fix Unable to delete Checklist. Added confirm delete to Checklist and Chekclist Item](https://github.com/wekan/wekan/commit/cf62807ad5d056ce9b8045c55f7cf6c29044967b).
|
||||
Thanks to C0rn3j and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.18 2025-12-28 WeKan ® release
|
||||
|
||||
This release adds the following CRITICAL SECURITY FIXES:
|
||||
|
||||
- [Upgraded MongoDB to 7.0.28 to fix mongobleed at Snap Candidate](https://github.com/wekan/wekan/commit/e210c9973be55a4fa4e7dd15aefc24e06dbc3e7f).
|
||||
Thanks to developers of MongoDB.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [Gantt chart view to one board view menu Swimlanes/Lists/Calendar/Gantt](https://github.com/wekan/wekan/commit/f34e4c0e363e386dbcce8e6ee8933b2d50491c58).
|
||||
Thanks to xet7.
|
||||
- [Number of cards per list and sum of custom number field in list head](https://github.com/wekan/wekan/commit/e569c2957ecc2b5fbf65ddcf0793b97c3ed5da81).
|
||||
Thanks to xet7.
|
||||
- [New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly](https://github.com/wekan/wekan/commit/c1168d181b3ff34f5ee7794a5740281c4ab5e253).
|
||||
Thanks to xet7.
|
||||
- [More translations. Added support page to Admin Panel / Settings / Layout](https://github.com/wekan/wekan/commit/a7400dca4503961267cc5fd6a1c8efaa78668f77).
|
||||
Thanks to xet7.
|
||||
- [Right top User Settings / Grey Icons. Also fixed Change Language popup](https://github.com/wekan/wekan/commit/300b653ea3416892faf2d08f5e0be3752e2041d6).
|
||||
Thanks to xet7.
|
||||
- [Collapse Swimlane, List, Opened Card. Opened Card window X and Y position can be moved freely from drag handle. Fix some dragging not possible. Fix iPhone Safari](https://github.com/wekan/wekan/commit/58f4884ad603e4f8c68a8819dfb1440234da70b6).
|
||||
Thanks to xet7.
|
||||
- Per-User and Board-level data save fixes. Per-User is collapse, width, height. Per-Board is Swimlanes, Lists, Cards etc.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/414b8dbf41ecf368d54aeceb6a78ccd0aa58f6a6),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/58e970d68508a76a1b9333941eb1696fb8fb7727).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- [Update GitHub docker/metadata-action from 5.8.0 to 5.9.0](https://github.com/wekan/wekan/pull/6012).
|
||||
Thanks to dependabot.
|
||||
- [Updated security.md](https://github.com/wekan/wekan/commit/7ff1649d8909917cae590c68def6eecac0442f91).
|
||||
Thanks to xet7.
|
||||
- [Updated build script for Linux arm64 bundle](https://github.com/wekan/wekan/commit/3db1305e58168f7417023ccd8d54995026844b18).
|
||||
Thanks to xet7.
|
||||
- Update Backup docs about migrating to newest WeKan.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/e669b1b9c72278c8debbc9de74d3fa02224a66d8),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/19fa12bb26a0444acffd49f24123ed993c425f6a),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/4e346c0ab7fbfb39544063cbd0e095307b26648f),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/59fc756a0bda8e11b9d86961daa35bb755110a68),
|
||||
[Part 5](https://github.com/wekan/wekan/commit/30541260f0f979662889bc40b4db461af1583a07),
|
||||
[Part 6](https://github.com/wekan/wekan/commit/784c5c6b0c83397ab4344d1a0fa231f33ff26564),
|
||||
[Part 7](https://github.com/wekan/wekan/commit/5686c92e05452a5d91c10ed436fae71103ecfb1f),
|
||||
[Part 8](https://github.com/wekan/wekan/commit/b7ff370561153bbfbb07426f9bd8b4d2977b1d0c),
|
||||
[Part 9](https://github.com/wekan/wekan/commit/fe4b36b85d4ac8efddb2c7148bc5d2413cd643e1),
|
||||
[Part 10](https://github.com/wekan/wekan/commit/9ebdc82d46d86029df12adaafba95c0ecfc9d2c2),
|
||||
[Part 11](https://github.com/wekan/wekan/commit/3ef0a3e685657eba1cc07314ac8d195f89dbef74),
|
||||
[Part 12](https://github.com/wekan/wekan/commit/2cbf64da33aff2d0b77ee91e7e9ac360cd1edb99),
|
||||
[Part 13](https://github.com/wekan/wekan/commit/3c578403404084ae10e4349b5570b0d50ecd8eb4),
|
||||
[Part 14](https://github.com/wekan/wekan/commit/451e9f78705dbbac2ed6ce123fd5440a871b6dcc),
|
||||
[Part 15](https://github.com/wekan/wekan/commit/e07e461e482f54c8ddaebc63373c93dc4aa0d956).
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Broken Strikethroughs in Markdown to HTML conversion](https://github.com/wekan/wekan/pull/6009).
|
||||
Thanks to brlin-tw.
|
||||
- [Updated Mac docs for Applite](https://github.com/wekan/wekan/commit/400eb81206f346a973d871a8aaa55d4ac5d48753).
|
||||
Thanks to xet7.
|
||||
- [Fix checklist delete action (issue #6020), link-card popup defaults, and stabilize due-cards ordering](https://github.com/wekan/wekan/pull/5967).
|
||||
Thanks to seve12.
|
||||
- [Improve rules UI board dropdowns/loading, rule header titles, and ensure card move updates attachment metadata](https://github.com/wekan/wekan/pull/5967).
|
||||
Thanks to seve12.
|
||||
- [Improve imports: normalize id → _id, add default swimlane fallback, and add regression test](https://github.com/wekan/wekan/pull/5967).
|
||||
Thanks to seve12.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.17 2025-11-06 WeKan ® release
|
||||
|
||||
This release adds the following new feature:
|
||||
|
||||
- [Feature: Workspaces, at All Boards page](https://github.com/wekan/wekan/commit/0afbdc95b49537e06b4f9cf98f51a669ef249384).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix 8.16: Switching Board View fails with 403 error](https://github.com/wekan/wekan/commit/550d87ac6cb3ec946600616485afdbd242983ab4).
|
||||
Thanks to xet7.
|
||||
- [Moved migrations from opening board to right sidebar / Migrations](https://github.com/wekan/wekan/commit/1b25d1d5720d4f486a10d2acce37e315cf9b6057).
|
||||
Thanks to xet7.
|
||||
- [Fix 8.16 Lists with no items are deleted every time when board is opened. Moved migrations to right sidebar](https://github.com/wekan/wekan/commit/7713e613b431e44dc13cee72e7a1e5f031473fa6).
|
||||
Thanks to xet7.
|
||||
- [Remove old translations and code not in use anymore](https://github.com/wekan/wekan/commit/ba49d4d140bc0d4cfb5a96db9ab077bc85db58f1).
|
||||
Thanks to xet7.
|
||||
- [Fixed sidebar migrations to be per-board, not global. Clarified translations](https://github.com/wekan/wekan/commit/e4638d5fbcbe004ac393462331805cac3ba25097).
|
||||
Thanks to xet7.
|
||||
- [Fix star board](https://github.com/wekan/wekan/commit/8711b476be30496b96b845529b5717bb6e685c27).
|
||||
Thanks to xet7.
|
||||
- [Fix Card emoji issues](https://github.com/wekan/wekan/commit/e5e711c938edcca23c974c3eec97296898bcf24e).
|
||||
Thanks to xet7.
|
||||
- [Try to fix Edit Custom Fields button not working. Removed duplicate option from Boards Settings](https://github.com/wekan/wekan/commit/20af0a2ef55b11e7205845859ee92a929616ce91).
|
||||
Thanks to xet7.
|
||||
- [Fix Regression - calendar popup to set due date has gone](https://github.com/wekan/wekan/commit/581733d605b7e0494e72229c45947cff134f6dd6).
|
||||
Thanks to xet7.
|
||||
- [Remove not working Bookmark menu option](https://github.com/wekan/wekan/commit/c829c073cf822e48b7cd84bbfb79d42867412517).
|
||||
Thanks to xet7.
|
||||
- [Fix Workspaces at All Boards to have correct count of remaining etc, while starred also at Starred/Favorites](https://github.com/wekan/wekan/commit/6244657ca53a54646ec01e702851a51d89bd0d55).
|
||||
Thanks to xet7.
|
||||
- [Fix Worker Permissions does not allow for cards to be moved. - v8.15. Removed buttons Worker should not use](https://github.com/wekan/wekan/commit/18003900c2d497c129793d1653d4d9872a2f19da).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.16 2025-11-02 WeKan ® release
|
||||
|
||||
This release fixes the following CRITICAL SECURITY ISSUES of [Spacebleed](https://wekan.fi/hall-of-fame/spacebleed/):
|
||||
|
||||
- [Fix SECURITY ISSUE 1: File Attachments enables stored XSS (High)](https://github.com/wekan/wekan/commit/e9a727301d7b4f1689a703503df668c0f4f4cab8).
|
||||
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||
- [Fix SECURITY ISSUE 2: Access to boards of any Orgs/Teams, and avatar permissions](https://github.com/wekan/wekan/commit/f26d58201855e861bab1cd1fda4d62c664efdb81).
|
||||
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||
- [Fix SECURITY ISSUE 3: Unauthenticated (or any) user can update board sort](https://github.com/wekan/wekan/commit/ea310d7508b344512e5de0dfbc9bdfd38145c5c5).
|
||||
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||
- [Fix SECURITY ISSUE 4: Members can forge others’ votes (Low). Bonus: Similar fixes to planning poker too done by xet7](https://github.com/wekan/wekan/commit/0a1a075f3153e71d9a858576f1c68d2925230d9c).
|
||||
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||
- [Fix SECURITY ISSUE 5: Attachment API uses bearer value as userId and DoS (Low)](https://github.com/wekan/wekan/commit/ccd90343394f433b287733ad0a33c08e0a71f53c).
|
||||
Thanks to Siam Thanat Hack (STH) and xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [List menu / More / Delete duplicate lists that do not have any cards](https://github.com/wekan/wekan/commit/91b846e2cdee9154b045d11b4b4c1a7ae1d79016).
|
||||
Thanks to xet7.
|
||||
- [Disabled migrations that happen when opening board. Defaulting to per-swimlane lists and drag drop list to same or different swimlane](https://github.com/wekan/wekan/commit/034dc08269520ca31c780cce64e0150969e9228e).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix changing swimlane color to not reload webpage](https://github.com/wekan/wekan/commit/ecf2418347cae4329deb292b534f68eb099d3f90).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.15 2025-10-23 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- Fix drag lists did not work
|
||||
[Part 1](https://github.com/wekan/wekan/commit/8662c96d1c8d4fa76ce7b31eb06678ad59c3ebe1),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/0cebd8aa4dbe0bf2418b814716744ab806b671c2).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.14 2025-10-23 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
|
|
|||
152
Dockerfile
152
Dockerfile
|
|
@ -4,23 +4,28 @@ LABEL org.opencontainers.image.ref.name="ubuntu"
|
|||
LABEL org.opencontainers.image.version="24.04"
|
||||
LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
|
||||
|
||||
# TARGETARCH is automatically provided by Docker Buildx
|
||||
ARG TARGETARCH
|
||||
# 2022-04-25:
|
||||
# - gyp does not yet work with Ubuntu 22.04 ubuntu:rolling,
|
||||
# so changing to 21.10. https://github.com/wekan/wekan/issues/4488
|
||||
|
||||
# 2021-09-18:
|
||||
# - Above Ubuntu base image copied from Docker Hub ubuntu:hirsute-20210825
|
||||
# to Quay to avoid Docker Hub rate limits.
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
ENV BUILD_DEPS="apt-utils gnupg gosu wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
|
||||
|
||||
ENV \
|
||||
DEBUG=false \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
METEOR_RELEASE=METEOR@2.16 \
|
||||
METEOR_RELEASE=METEOR@2.14 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=6.14.17 \
|
||||
FIBERS_VERSION=4.0.1 \
|
||||
ARCHITECTURE=linux-x64 \
|
||||
SRC_PATH=./ \
|
||||
WITH_API=true \
|
||||
MONGO_OPLOG_URL="" \
|
||||
RESULTS_PER_PAGE="" \
|
||||
DEFAULT_BOARD_ID="" \
|
||||
ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE=3 \
|
||||
|
|
@ -158,69 +163,134 @@ ENV \
|
|||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# NODE_OPTIONS="--max_old_space_size=4096"
|
||||
|
||||
#---------------------------------------------
|
||||
# == at docker-compose.yml: AUTOLOGIN WITH OIDC/OAUTH2 ====
|
||||
# https://github.com/wekan/wekan/wiki/autologin
|
||||
#- OIDC_REDIRECTION_ENABLED=true
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Copy the app to the image
|
||||
#COPY ${SRC_PATH} /home/wekan/app
|
||||
|
||||
# Install OS
|
||||
RUN <<EOR
|
||||
set -o xtrace
|
||||
|
||||
# Create Wekan user
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan
|
||||
|
||||
# OS Updates
|
||||
# OS dependencies
|
||||
apt-get update --assume-yes
|
||||
apt-get upgrade --assume-yes
|
||||
apt-get install --assume-yes --no-install-recommends ${BUILD_DEPS}
|
||||
|
||||
# Multi-arch mapping logic
|
||||
case "${TARGETARCH}" in
|
||||
"amd64") NODE_ARCH="x64" WEKAN_ARCH="amd64" ;;
|
||||
"arm64") NODE_ARCH="arm64" WEKAN_ARCH="arm64" ;;
|
||||
"s390x") NODE_ARCH="s390x" WEKAN_ARCH="s390x" ;;
|
||||
*) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Node.js Installation
|
||||
cd /tmp
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
|
||||
grep "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt | shasum -a 256 -c -
|
||||
tar xzf "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
rm -f "node-${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" SHASUMS256.txt
|
||||
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
|
||||
|
||||
# NPM configuration
|
||||
npm install -g npm@${NPM_VERSION} --production
|
||||
chown --recursive wekan:wekan /home/wekan/
|
||||
|
||||
# Temporary Tar swap for Meteor bundle
|
||||
# Meteor installer doesn't work with the default tar binary, so using bsdtar while installing.
|
||||
# https://github.com/coreos/bugs/issues/1095#issuecomment-350574389
|
||||
cp $(which tar) $(which tar)~
|
||||
ln -sf $(which bsdtar) $(which tar)
|
||||
|
||||
# WeKan Bundle Installation
|
||||
# Install NodeJS
|
||||
cd /tmp
|
||||
|
||||
# Download nodejs
|
||||
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/node-v14.21.4-linux-x64.tar.gz"
|
||||
#wget "https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt"
|
||||
wget "https://github.com/wekan/node-v14-esm/releases/download/v14.21.4/SHASUMS256.txt"
|
||||
|
||||
# Verify nodejs authenticity
|
||||
#grep "node-${NODE_VERSION}-${ARCHITECTURE}.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
|
||||
grep "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt" | shasum -a 256 -c -
|
||||
rm -f "SHASUMS256.txt"
|
||||
|
||||
# Install Node
|
||||
#tar xzf "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
tar xzf "node-v14.21.4-linux-x64.tar.gz" -C /usr/local --strip-components=1 --no-same-owner
|
||||
#rm "node-$NODE_VERSION-$ARCHITECTURE.tar.gz" "SHASUMS256.txt"
|
||||
rm "node-v14.21.4-linux-x64.tar.gz" "SHASUMS256.txt"
|
||||
ln -s "/usr/local/bin/node" "/usr/local/bin/nodejs"
|
||||
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/${NODE_VERSION} /home/wekan/.config"
|
||||
#mkdir -p "/opt/nodejs/lib/node_modules/fibers/.node-gyp" "/root/.node-gyp/v14.21.4 /home/wekan/.config"
|
||||
|
||||
# Install node dependencies
|
||||
#npm install -g npm@${NPM_VERSION} --production
|
||||
npm install -g npm@$6.14.17 --production
|
||||
chown --recursive wekan:wekan /home/wekan/.config
|
||||
|
||||
# Install Meteor
|
||||
cd /home/wekan
|
||||
chown --recursive wekan:wekan /home/wekan
|
||||
echo "Starting meteor ${METEOR_RELEASE} installation... \n"
|
||||
#gosu wekan:wekan curl https://install.meteor.com/ | /bin/sh
|
||||
# Specify Meteor version 2.14 to be compatible: https://github.com/wekan/wekan/pull/5816/files
|
||||
#gosu wekan:wekan npm -g install meteor@2.14 --unsafe-perm
|
||||
#mv /root/.meteor /home/wekan/
|
||||
#chown --recursive wekan:wekan /home/wekan/.meteor
|
||||
|
||||
#sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' /home/wekan/app/packages/meteor-useraccounts-core/package.js
|
||||
#cd /home/wekan/.meteor
|
||||
#gosu wekan:wekan /home/wekan/.meteor/meteor -- help
|
||||
|
||||
# Build app (Production)
|
||||
#cd /home/wekan/app
|
||||
mkdir -p /home/wekan/app
|
||||
cd /home/wekan/app
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.31/wekan-8.31-${WEKAN_ARCH}.zip"
|
||||
unzip "wekan-8.31-${WEKAN_ARCH}.zip"
|
||||
rm "wekan-8.31-${WEKAN_ARCH}.zip"
|
||||
#mkdir -p /home/wekan/.npm
|
||||
#chown --recursive wekan:wekan /home/wekan/.npm
|
||||
#chmod u+w *.json
|
||||
#gosu wekan:wekan meteor npm install --production
|
||||
#gosu wekan:wekan /home/wekan/.meteor/meteor build --directory /home/wekan/app_build
|
||||
#cd /home/wekan/app_build/bundle/programs/server/
|
||||
#chmod u+w *.json
|
||||
#gosu wekan:wekan meteor npm install --production
|
||||
#cd node_modules/fibers
|
||||
#node build.js
|
||||
#cd ../..
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
|
||||
#mv /home/wekan/app_build/bundle /build
|
||||
wget "https://github.com/wekan/wekan/releases/download/v8.14/wekan-8.14-amd64.zip"
|
||||
unzip wekan-8.14-amd64.zip
|
||||
rm wekan-8.14-amd64.zip
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Restore original tar
|
||||
# Put back the original tar
|
||||
mv $(which tar)~ $(which tar)
|
||||
|
||||
# Cleanup
|
||||
apt-get remove --purge --assume-yes ${BUILD_DEPS}
|
||||
#npm uninstall -g api2html
|
||||
apt-get autoremove --assume-yes
|
||||
apt-get clean --assume-yes
|
||||
rm -Rf /tmp/*
|
||||
rm -Rf /var/lib/apt/lists/*
|
||||
rm -Rf /var/cache/apt
|
||||
rm -Rf /var/lib/apt/lists
|
||||
rm -Rf /home/wekan/app_build
|
||||
rm -Rf /home/wekan/app
|
||||
rm -Rf /home/wekan/.meteor
|
||||
|
||||
mkdir -p /data
|
||||
chown wekan:wekan --recursive /data
|
||||
mkdir /data
|
||||
chown wekan --recursive /data
|
||||
EOR
|
||||
|
||||
USER wekan
|
||||
|
||||
ENV PORT=8080
|
||||
EXPOSE $PORT
|
||||
STOPSIGNAL SIGKILL
|
||||
WORKDIR /build
|
||||
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node main.js"]
|
||||
STOPSIGNAL SIGKILL
|
||||
WORKDIR /home/wekan/app
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# https://github.com/wekan/wekan/issues/3585#issuecomment-1021522132
|
||||
# Add more Node heap:
|
||||
# NODE_OPTIONS="--max_old_space_size=4096"
|
||||
# Add more stack:
|
||||
# bash -c "ulimit -s 65500; exec node --stack-size=65500 main.js"
|
||||
#---------------------------------------------------------------------
|
||||
#
|
||||
# CMD ["node", "/build/main.js"]
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 /build/main.js"]
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /build/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /build/main.js"]
|
||||
|
|
|
|||
87
SECURITY.md
87
SECURITY.md
|
|
@ -1,20 +1,12 @@
|
|||
About money, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## Responsible Security Disclosure
|
||||
Security is very important to us. If you discover any issue regarding security, please disclose
|
||||
the information responsibly by sending an email from Protonmail to security@wekan.fi
|
||||
that is Protomail email address, or by using this PGP key
|
||||
[security-at-wekan.fi.asc](security-at-wekan.fi.asc) to security@wekan.fi
|
||||
and not by creating a GitHub issue. We will respond swiftly to fix verifiable security issues.
|
||||
|
||||
- To send email, use [ProtonMail](https://proton.me) email address or use PGP key [security-at-wekan.fi.asc](security-at-wekan.fi.asc)
|
||||
- Send info about security issue ONLY to security@wekan.fi (that is Protomail email address). NOT TO ANYWHERE ELSE. NO CC, NO BCC.
|
||||
- Wait for new WeKan release that fixes security issue
|
||||
- If you approve, we thank you by adding you to Hall of Fame: https://wekan.fi/hall-of-fame/
|
||||
|
||||
## Bonus Points
|
||||
|
||||
- If you include code for fixing security issue
|
||||
|
||||
## Losing Points
|
||||
|
||||
- If you ask about [bounty](CONTRIBUTING.md). There is no bounty. WeKan is NOT Big Tech. WeKan is FLOSS.
|
||||
- If you forget to include vulnerability details.
|
||||
- If you send info about security issue to somewhere else than security@wekan.fi
|
||||
We thank you with a place at our hall of fame page, that is at https://wekan.fi/hall-of-fame
|
||||
|
||||
## How should reports be formatted?
|
||||
|
||||
|
|
@ -34,7 +26,7 @@ CWSS (optional): %cwss
|
|||
|
||||
Anyone who reports a unique security issue in scope and does not disclose it to
|
||||
a third party before we have patched and updated may be upon their approval
|
||||
added to the WeKan Hall of Fame https://wekan.fi/hall-of-fame/
|
||||
added to the Wekan Hall of Fame.
|
||||
|
||||
## Which domains are in scope?
|
||||
|
||||
|
|
@ -71,6 +63,11 @@ and by by companies that have 30k users.
|
|||
- If you are thinking about TLS MITM, look at https://github.com/caddyserver/caddy/issues/2530
|
||||
- Let's Encrypt TLS requires publicly accessible webserver, that Let's Encrypt TLS validation servers check.
|
||||
- If firewall limits to only allowed IP addresses, you may need non-Let's Encrypt TLS cert.
|
||||
- For On Premise:
|
||||
- https://caddyserver.com/docs/automatic-https#local-https
|
||||
- https://github.com/wekan/wekan/wiki/Caddy-Webserver-Config
|
||||
- https://github.com/wekan/wekan/wiki/Azure
|
||||
- https://github.com/wekan/wekan/wiki/Traefik-and-self-signed-SSL-certs
|
||||
|
||||
## XSS
|
||||
|
||||
|
|
@ -175,57 +172,6 @@ Meteor.startup(() => {
|
|||
- https://github.com/wekan/wekan/blob/main/client/components/cards/attachments.js#L303-L312
|
||||
- https://wekan.github.io/hall-of-fame/filebleed/
|
||||
|
||||
### Attachments: Forced download to prevent stored XSS
|
||||
|
||||
- To prevent browser-side execution of uploaded content under the app origin, all attachment downloads are served with safe headers:
|
||||
- `Content-Type: application/octet-stream`
|
||||
- `Content-Disposition: attachment`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- A restrictive `Content-Security-Policy` with `sandbox`
|
||||
- This means attachments are downloaded instead of rendered inline by default. This mitigates HTML/JS/SVG based stored XSS vectors.
|
||||
- Avatars and inline images remain supported but SVG uploads are blocked and never rendered inline.
|
||||
|
||||
## Users: Client update restrictions
|
||||
|
||||
- Client-side updates to user documents are limited to safe fields only:
|
||||
- `username`
|
||||
- `profile.*`
|
||||
- Sensitive fields are blocked from any client updates and can only be modified by server methods with authorization:
|
||||
- `orgs`, `teams`, `roles`, `isAdmin`, `createdThroughApi`, `loginDisabled`, `authenticationMethod`, `services.*`, `emails.*`, `sessionData.*`
|
||||
- Attempts to update forbidden fields from the client are denied.
|
||||
- Admin operations like managing org/team membership or toggling flags must use server methods that check permissions.
|
||||
|
||||
## Voting: integrity and authorization
|
||||
|
||||
- Client updates to card `vote` fields are blocked to prevent forged votes and inconsistent policy enforcement.
|
||||
- Voting is performed via a server method that enforces:
|
||||
- Authentication and board membership, or an explicit per-card flag allowing non-members to vote.
|
||||
- Only the caller's own userId is added/removed from `vote.positive`/`vote.negative`.
|
||||
- This prevents members from fabricating other users' votes and ensures non-members cannot vote unless explicitly allowed.
|
||||
|
||||
## Planning Poker: integrity and authorization
|
||||
|
||||
- Client updates to card `poker` fields are blocked. All poker actions go through server methods that enforce:
|
||||
- Authentication and board membership for configuration and results.
|
||||
- For casting a poker vote, either board membership or an explicit per-card flag allowing non-members to participate.
|
||||
- Only the caller's own userId is added/removed from the selected estimation bucket (e.g., one, two, five, etc.).
|
||||
- Methods cover setting/unsetting poker question/end, casting votes, replaying, and setting final estimation.
|
||||
|
||||
## Attachment API: authentication and DoS prevention
|
||||
|
||||
- The attachment API (`/api/attachment/*`) requires proper authentication using `X-User-Id` and `X-Auth-Token` headers.
|
||||
- Authentication validates tokens by hashing with `Accounts._hashLoginToken` and matching against stored login tokens, preventing identity spoofing.
|
||||
- Request handlers implement:
|
||||
- 30-second timeout to prevent hanging connections.
|
||||
- Request body size limits (50MB for uploads, 10MB for metadata operations).
|
||||
- Proper error handling and guaranteed response completion.
|
||||
- Request error event handlers to clean up failed connections.
|
||||
- This prevents:
|
||||
- DoS attacks via concurrent unauthenticated or malformed requests.
|
||||
- Identity spoofing by using arbitrary bearer tokens or user IDs.
|
||||
- Resource exhaustion from hanging connections or excessive payloads.
|
||||
- Access control: all attachment operations verify board membership before allowing access.
|
||||
|
||||
## Brute force login protection
|
||||
|
||||
- https://github.com/wekan/wekan/commit/23e5e1e3bd081699ce39ce5887db7e612616014d
|
||||
|
|
@ -272,4 +218,9 @@ Typical already known or "no impact" bugs such as:
|
|||
- Email spoofing, SPF, DMARC & DKIM. Wekan does not include email server.
|
||||
|
||||
Wekan is Open Source with MIT license, and free to use also for commercial use.
|
||||
We welcome all fixes to improve security by email to security@wekan.fi
|
||||
We welcome all fixes to improve security by email to security@wekan.team
|
||||
|
||||
## Bonus Points
|
||||
|
||||
If your Responsible Security Disclosure includes code for fixing security issue,
|
||||
you get bonus points, as seen on [Hall of Fame](https://wekan.github.io/hall-of-fame).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v8.31.0"
|
||||
appVersion: "v8.14.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
|
|
|||
|
|
@ -10,73 +10,8 @@ import '/client/lib/boardConverter';
|
|||
import '/client/components/boardConversionProgress';
|
||||
|
||||
// Import migration manager and progress UI
|
||||
import '/client/lib/attachmentMigrationManager';
|
||||
import '/client/components/settings/migrationProgress';
|
||||
import '/client/lib/migrationManager';
|
||||
import '/client/components/migrationProgress';
|
||||
|
||||
// Import cron settings
|
||||
import '/client/components/settings/cronSettings';
|
||||
|
||||
// Mirror Meteor login token into a cookie for server-side file route auth
|
||||
// This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL
|
||||
// Token already lives in localStorage; cookie adds same-origin send-on-request semantics
|
||||
Meteor.startup(() => {
|
||||
const COOKIE_NAME = 'meteor_login_token';
|
||||
const cookieAttrs = () => {
|
||||
const attrs = ['Path=/', 'SameSite=Lax'];
|
||||
try {
|
||||
if (window.location && window.location.protocol === 'https:') {
|
||||
attrs.push('Secure');
|
||||
}
|
||||
} catch (_) {}
|
||||
return attrs.join('; ');
|
||||
};
|
||||
|
||||
const setCookie = (name, value) => {
|
||||
if (!value) return;
|
||||
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; ${cookieAttrs()}`;
|
||||
};
|
||||
const clearCookie = (name) => {
|
||||
document.cookie = `${encodeURIComponent(name)}=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; ${cookieAttrs()}`;
|
||||
};
|
||||
|
||||
const syncCookie = () => {
|
||||
try {
|
||||
const token = Accounts && typeof Accounts._storedLoginToken === 'function' ? Accounts._storedLoginToken() : null;
|
||||
if (token) setCookie(COOKIE_NAME, token); else clearCookie(COOKIE_NAME);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
// Initial sync on startup
|
||||
syncCookie();
|
||||
|
||||
// Keep cookie in sync on login/logout
|
||||
if (Accounts && typeof Accounts.onLogin === 'function') Accounts.onLogin(syncCookie);
|
||||
if (Accounts && typeof Accounts.onLogout === 'function') Accounts.onLogout(syncCookie);
|
||||
|
||||
// Sync across tabs/windows when localStorage changes
|
||||
window.addEventListener('storage', (ev) => {
|
||||
if (ev && typeof ev.key === 'string' && ev.key.indexOf('Meteor.loginToken') !== -1) {
|
||||
syncCookie();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Subscribe to per-user small publications
|
||||
Meteor.startup(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (Meteor.userId()) {
|
||||
Meteor.subscribe('userGreyIcons');
|
||||
Meteor.subscribe('userDesktopDragHandles');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize mobile mode on startup for iOS devices
|
||||
// This ensures mobile mode is applied correctly on page load
|
||||
Tracker.afterFlush(() => {
|
||||
if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
|
||||
Utils.initializeUserSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -199,5 +199,4 @@ template(name="activity")
|
|||
else if(currentData.timeValue)
|
||||
| {{_ activity.activityType currentData.timeValue}}
|
||||
|
||||
if($neq mode 'none')
|
||||
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
|
|
|
|||
|
|
@ -108,12 +108,15 @@
|
|||
text-decoration: none;
|
||||
height: 24px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
|
||||
display: inline-block;
|
||||
font-size: clamp(14px, 2vw, 18px);
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-left: 4px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
|
||||
font-size: 8px;
|
||||
margin-top: -7px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .reaction {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ template(name="comment")
|
|||
= text
|
||||
.edit-controls
|
||||
button.primary(type="submit") {{_ 'edit'}}
|
||||
a.js-close-inlined-form(title="{{_ 'close' }}")
|
||||
i.fa.fa-times-thin
|
||||
.fa.fa-times-thin.js-close-inlined-form
|
||||
else
|
||||
.comment-text
|
||||
+viewer
|
||||
|
|
@ -55,11 +54,9 @@ template(name="commentReactions")
|
|||
span.reaction-codepoint !{reaction.reactionCodepoint}
|
||||
span.reaction-count #{reaction.userIds.length}
|
||||
if (currentUser.isBoardMember)
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
span(title="{{_ 'reaction' }}") 😀
|
||||
span(title="{{_ 'add' }}") ➕
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
i.fa.fa-smile-o
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="addReactionPopup")
|
||||
.reactions-popup
|
||||
|
|
|
|||
|
|
@ -57,9 +57,8 @@ BlazeComponent.extendComponent({
|
|||
|
||||
BlazeComponent.extendComponent({
|
||||
getComments() {
|
||||
const data = this.data();
|
||||
if (!data || typeof data.comments !== 'function') return [];
|
||||
return data.comments();
|
||||
const ret = this.data().comments();
|
||||
return ret;
|
||||
},
|
||||
}).register("comments");
|
||||
|
||||
|
|
|
|||
|
|
@ -170,14 +170,14 @@
|
|||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
|
||||
.board-conversion-header,
|
||||
.board-conversion-content,
|
||||
.board-conversion-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
|
||||
.board-conversion-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,25 +3,25 @@ template(name="boardConversionProgress")
|
|||
.board-conversion-modal
|
||||
.board-conversion-header
|
||||
h3
|
||||
i.fa.fa-cog
|
||||
| ⚙️
|
||||
| {{_ 'converting-board'}}
|
||||
p {{_ 'converting-board-description'}}
|
||||
|
||||
|
||||
.board-conversion-content
|
||||
.conversion-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{conversionProgress}}%")
|
||||
.progress-text {{conversionProgress}}%
|
||||
|
||||
|
||||
.conversion-status
|
||||
i.fa.fa-cog
|
||||
| ⚙️
|
||||
| {{conversionStatus}}
|
||||
|
||||
|
||||
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
|
||||
i.fa.fa-clock-o
|
||||
| ⏰
|
||||
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
|
||||
|
||||
|
||||
.board-conversion-footer
|
||||
.conversion-info
|
||||
i.fa.fa-info-circle
|
||||
| ℹ️
|
||||
| {{_ 'conversion-info-text'}}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import {
|
||||
import {
|
||||
boardConverter,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
|
|
@ -12,15 +12,15 @@ Template.boardConversionProgress.helpers({
|
|||
isConverting() {
|
||||
return isConverting.get();
|
||||
},
|
||||
|
||||
|
||||
conversionProgress() {
|
||||
return conversionProgress.get();
|
||||
},
|
||||
|
||||
|
||||
conversionStatus() {
|
||||
return conversionStatus.get();
|
||||
},
|
||||
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return conversionEstimatedTime.get();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
template(name="archivedBoards")
|
||||
h2
|
||||
span(title="{{_ 'archived-boards'}}")
|
||||
i.fa.fa-archive
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archived-boards'}}
|
||||
|
||||
ul.archived-lists
|
||||
|
|
@ -9,7 +8,7 @@ template(name="archivedBoards")
|
|||
li.archived-lists-item
|
||||
div.board-header-btns
|
||||
button.board-header-btn.js-delete-board
|
||||
i.fa.fa-trash
|
||||
i.fa.fa-trash-o
|
||||
| {{_ 'delete-board'}}
|
||||
button.board-header-btn.js-restore-board
|
||||
i.fa.fa-undo
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
|
|
@ -23,7 +22,7 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
async 'click .js-restore-board'() {
|
||||
'click .js-restore-board'() {
|
||||
// TODO : Make isSandstorm variable global
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
|
|
@ -31,13 +30,13 @@ BlazeComponent.extendComponent({
|
|||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await currentBoard.archive();
|
||||
currentBoard.archive();
|
||||
}
|
||||
const board = this.currentData();
|
||||
await board.restore();
|
||||
board.restore();
|
||||
Utils.goBoardId(board._id);
|
||||
},
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', async function() {
|
||||
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
|
||||
Popup.back();
|
||||
const isSandstorm =
|
||||
Meteor.settings &&
|
||||
|
|
@ -45,9 +44,9 @@ BlazeComponent.extendComponent({
|
|||
Meteor.settings.public.sandstorm;
|
||||
if (isSandstorm && Utils.getCurrentBoardId()) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
await Boards.removeAsync(currentBoard._id);
|
||||
Boards.remove(currentBoard._id);
|
||||
}
|
||||
await Boards.removeAsync(this._id);
|
||||
Boards.remove(this._id);
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -231,30 +231,6 @@
|
|||
font-size: 1em !important; /* Keep original icon size */
|
||||
}
|
||||
|
||||
/* Mobile iPhone: scale card details text and icons to 2x */
|
||||
body.mobile-mode.iphone-device .card-details {
|
||||
font-size: 2em !important;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .fa,
|
||||
body.mobile-mode.iphone-device .card-details .icon,
|
||||
body.mobile-mode.iphone-device .card-details i,
|
||||
body.mobile-mode.iphone-device .card-details .emoji-icon,
|
||||
body.mobile-mode.iphone-device .card-details a,
|
||||
body.mobile-mode.iphone-device .card-details p,
|
||||
body.mobile-mode.iphone-device .card-details span,
|
||||
body.mobile-mode.iphone-device .card-details div,
|
||||
body.mobile-mode.iphone-device .card-details button,
|
||||
body.mobile-mode.iphone-device .card-details input,
|
||||
body.mobile-mode.iphone-device .card-details select,
|
||||
body.mobile-mode.iphone-device .card-details textarea {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
/* Section titles slightly larger than content but not as big as card title */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-item-title {
|
||||
font-size: 1.1em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are positioned correctly */
|
||||
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
|
||||
width: 12px;
|
||||
|
|
@ -287,106 +263,63 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
|
|||
animation: fadeIn 0.2s;
|
||||
z-index: 16;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure overlay stays behind card details */
|
||||
@media screen and (max-width: 800px) {
|
||||
.board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* In desktop mode on small screens, still keep overlay behind card */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* In mobile mode, lower the overlay z-index to stay behind card details */
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
/* iPhone in desktop mode: remove overlay to avoid blocking card */
|
||||
body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Desktop mode: hide overlay to allow multiple cards and board interaction */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
|
||||
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
|
||||
display: none;
|
||||
}
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-wrapper.mobile-view {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden !important;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
.board-wrapper {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.board-wrapper .board-canvas .swimlane {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden !important;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
.calendar-event-green {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
template(name="board")
|
||||
|
||||
if isConverting.get
|
||||
|
||||
if isMigrating.get
|
||||
+migrationProgress
|
||||
else if isConverting.get
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
if currentBoard
|
||||
|
|
@ -22,7 +24,7 @@ template(name="boardBody")
|
|||
// Debug information (remove in production)
|
||||
if debugBoardState
|
||||
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
|
||||
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
|
||||
| Board: {{currentBoard.title}} | View: {{boardView}} | HasSwimlanes: {{hasSwimlanes}} | Swimlanes: {{currentBoard.swimlanes.length}}
|
||||
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
.board-canvas.js-swimlanes(
|
||||
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
|
||||
|
|
@ -47,8 +49,6 @@ template(name="boardBody")
|
|||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
else if isViewGantt
|
||||
+ganttView
|
||||
else
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
|
|
@ -56,10 +56,6 @@ template(name="boardBody")
|
|||
+swimlane(this)
|
||||
else
|
||||
+listsGroup(currentBoard)
|
||||
//- Render multiple open cards in desktop mode
|
||||
unless isMiniScreen
|
||||
each openCards
|
||||
+cardDetails(this cardIndex=@index)
|
||||
+sidebar
|
||||
|
||||
template(name="calendarView")
|
||||
|
|
@ -67,4 +63,4 @@ template(name="calendarView")
|
|||
.calendar-view.swimlane
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
+fullcalendar(calendarOptions)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import '../gantt/gantt.js';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import { boardConverter } from '/client/lib/boardConverter';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import { migrationManager } from '/client/lib/migrationManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Lists from '/models/lists';
|
||||
|
||||
|
|
@ -16,6 +15,7 @@ BlazeComponent.extendComponent({
|
|||
onCreated() {
|
||||
this.isBoardReady = new ReactiveVar(false);
|
||||
this.isConverting = new ReactiveVar(false);
|
||||
this.isMigrating = new ReactiveVar(false);
|
||||
this._swimlaneCreated = new Set(); // Track boards where we've created swimlanes
|
||||
this._boardProcessed = false; // Track if board has been processed
|
||||
this._lastProcessedBoardId = null; // Track last processed board ID
|
||||
|
|
@ -27,16 +27,17 @@ BlazeComponent.extendComponent({
|
|||
this.autorun(() => {
|
||||
const currentBoardId = Session.get('currentBoard');
|
||||
if (!currentBoardId) return;
|
||||
|
||||
|
||||
const handle = subManager.subscribe('board', currentBoardId, false);
|
||||
|
||||
|
||||
// Use a separate autorun for subscription ready state to avoid reactive loops
|
||||
this.subscriptionReadyAutorun = Tracker.autorun(() => {
|
||||
if (handle.ready()) {
|
||||
// Only run conversion/migration logic once per board
|
||||
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
|
||||
this._boardProcessed = true;
|
||||
this._lastProcessedBoardId = currentBoardId;
|
||||
|
||||
|
||||
// Ensure default swimlane exists (only once per board)
|
||||
this.ensureDefaultSwimlane(currentBoardId);
|
||||
// Check if board needs conversion
|
||||
|
|
@ -67,7 +68,7 @@ BlazeComponent.extendComponent({
|
|||
if (!board) return;
|
||||
|
||||
const swimlanes = board.swimlanes();
|
||||
|
||||
|
||||
if (swimlanes.length === 0) {
|
||||
// Check if any swimlane exists in the database to avoid race conditions
|
||||
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
|
||||
|
|
@ -97,31 +98,334 @@ BlazeComponent.extendComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
this.isBoardReady.set(true);
|
||||
// Check if board needs migration based on migration version
|
||||
const needsMigration = !board.migrationVersion || board.migrationVersion < 1;
|
||||
|
||||
if (needsMigration) {
|
||||
// Start background migration for old boards
|
||||
this.isMigrating.set(true);
|
||||
await this.startBackgroundMigration(boardId);
|
||||
this.isMigrating.set(false);
|
||||
}
|
||||
|
||||
// Check if board needs conversion (for old structure)
|
||||
if (boardConverter.isBoardConverted(boardId)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been converted, skipping conversion`);
|
||||
}
|
||||
this.isBoardReady.set(true);
|
||||
} else {
|
||||
const needsConversion = boardConverter.needsConversion(boardId);
|
||||
|
||||
if (needsConversion) {
|
||||
this.isConverting.set(true);
|
||||
const success = await boardConverter.convertBoard(boardId);
|
||||
this.isConverting.set(false);
|
||||
|
||||
if (success) {
|
||||
this.isBoardReady.set(true);
|
||||
} else {
|
||||
console.error('Board conversion failed, setting ready to true anyway');
|
||||
this.isBoardReady.set(true); // Still show board even if conversion failed
|
||||
}
|
||||
} else {
|
||||
this.isBoardReady.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert shared lists to per-swimlane lists if needed
|
||||
await this.convertSharedListsToPerSwimlane(boardId);
|
||||
|
||||
// Fix missing lists migration (for cards with wrong listId references)
|
||||
await this.fixMissingLists(boardId);
|
||||
|
||||
// Fix duplicate lists created by WeKan 8.10
|
||||
await this.fixDuplicateLists(boardId);
|
||||
|
||||
// Start attachment migration in background if needed
|
||||
this.startAttachmentMigrationIfNeeded(boardId);
|
||||
} catch (error) {
|
||||
console.error('Error during board conversion check:', error);
|
||||
this.isConverting.set(false);
|
||||
this.isMigrating.set(false);
|
||||
this.isBoardReady.set(true); // Show board even if conversion check failed
|
||||
}
|
||||
},
|
||||
|
||||
async startBackgroundMigration(boardId) {
|
||||
try {
|
||||
// Start background migration using the cron system
|
||||
Meteor.call('boardMigration.startBoardMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start background migration:', error);
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Background migration started for board:', boardId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error starting background migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async convertSharedListsToPerSwimlane(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for shared lists conversion
|
||||
if (board.hasSharedListsConverted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for shared lists conversion`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all lists for this board
|
||||
const allLists = board.lists();
|
||||
const swimlanes = board.swimlanes();
|
||||
|
||||
if (swimlanes.length === 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no swimlanes, skipping shared lists conversion`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find shared lists (lists with empty swimlaneId or null swimlaneId)
|
||||
const sharedLists = allLists.filter(list => !list.swimlaneId || list.swimlaneId === '');
|
||||
|
||||
if (sharedLists.length === 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no shared lists to convert`);
|
||||
}
|
||||
// Mark as processed even if no shared lists
|
||||
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Converting ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Convert each shared list to per-swimlane lists
|
||||
for (const sharedList of sharedLists) {
|
||||
// Create a copy of the list for each swimlane
|
||||
for (const swimlane of swimlanes) {
|
||||
// Check if this list already exists in this swimlane
|
||||
const existingList = Lists.findOne({
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlane._id,
|
||||
title: sharedList.title
|
||||
});
|
||||
|
||||
if (!existingList) {
|
||||
// Double-check to avoid race conditions
|
||||
const doubleCheckList = ReactiveCache.getList({
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlane._id,
|
||||
title: sharedList.title
|
||||
});
|
||||
|
||||
if (!doubleCheckList) {
|
||||
// Create a new list in this swimlane
|
||||
const newListData = {
|
||||
title: sharedList.title,
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlane._id,
|
||||
sort: sharedList.sort || 0,
|
||||
archived: sharedList.archived || false, // Preserve archived state from original list
|
||||
createdAt: new Date(),
|
||||
modifiedAt: new Date()
|
||||
};
|
||||
|
||||
// Copy other properties if they exist
|
||||
if (sharedList.color) newListData.color = sharedList.color;
|
||||
if (sharedList.wipLimit) newListData.wipLimit = sharedList.wipLimit;
|
||||
if (sharedList.wipLimitEnabled) newListData.wipLimitEnabled = sharedList.wipLimitEnabled;
|
||||
if (sharedList.wipLimitSoft) newListData.wipLimitSoft = sharedList.wipLimitSoft;
|
||||
|
||||
Lists.insert(newListData);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
const archivedStatus = sharedList.archived ? ' (archived)' : ' (active)';
|
||||
console.log(`Created list "${sharedList.title}"${archivedStatus} for swimlane ${swimlane.title || swimlane._id}`);
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id} (double-check), skipping`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`List "${sharedList.title}" already exists in swimlane ${swimlane.title || swimlane._id}, skipping`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the original shared list completely
|
||||
Lists.remove(sharedList._id);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Removed shared list "${sharedList.title}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark board as processed
|
||||
Boards.update(boardId, { $set: { hasSharedListsConverted: true } });
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Successfully converted ${sharedLists.length} shared lists to per-swimlane lists for board ${boardId}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error converting shared lists to per-swimlane:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fixMissingLists(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for missing lists fix
|
||||
if (board.fixMissingListsCompleted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if migration is needed
|
||||
const needsMigration = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.needsMigration', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!needsMigration) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} does not need missing lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Execute the migration
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixMissingListsMigration.execute', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (result && result.success) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Successfully fixed missing lists for board ${boardId}: created ${result.createdLists} lists, updated ${result.updatedCards} cards`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing missing lists:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async fixDuplicateLists(boardId) {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Check if board has already been processed for duplicate lists fix
|
||||
if (board.fixDuplicateListsCompleted) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been processed for duplicate lists fix`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting duplicate lists fix for board ${boardId}`);
|
||||
}
|
||||
|
||||
// Execute the duplicate lists fix
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
Meteor.call('fixDuplicateLists.fixBoard', boardId, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (result && result.fixed > 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Successfully fixed ${result.fixed} duplicate lists for board ${boardId}: ${result.fixedSwimlanes} swimlanes, ${result.fixedLists} lists`);
|
||||
}
|
||||
|
||||
// Mark board as processed
|
||||
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
|
||||
} else if (process.env.DEBUG === 'true') {
|
||||
console.log(`No duplicate lists found for board ${boardId}`);
|
||||
// Still mark as processed to avoid repeated checks
|
||||
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
|
||||
} else {
|
||||
// Still mark as processed to avoid repeated checks
|
||||
Boards.update(boardId, { $set: { fixDuplicateListsCompleted: true } });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing duplicate lists:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async startAttachmentMigrationIfNeeded(boardId) {
|
||||
try {
|
||||
// Check if board has already been migrated
|
||||
if (attachmentMigrationManager.isBoardMigrated(boardId)) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has already been migrated, skipping`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are unconverted attachments
|
||||
const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
|
||||
|
||||
if (unconvertedAttachments.length > 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
|
||||
}
|
||||
await attachmentMigrationManager.startAttachmentMigration(boardId);
|
||||
} else {
|
||||
// No attachments to migrate, mark board as migrated
|
||||
// This will be handled by the migration manager itself
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Board ${boardId} has no attachments to migrate`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting attachment migration:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onlyShowCurrentCard() {
|
||||
const isMiniScreen = Utils.isMiniScreen();
|
||||
const currentCardId = Utils.getCurrentCardId(true);
|
||||
return isMiniScreen && currentCardId;
|
||||
},
|
||||
|
||||
openCards() {
|
||||
// In desktop mode, return array of all open cards
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCardIds = Session.get('openCards') || [];
|
||||
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
goHome() {
|
||||
FlowRouter.go('home');
|
||||
},
|
||||
|
|
@ -130,6 +434,10 @@ BlazeComponent.extendComponent({
|
|||
return this.isConverting.get();
|
||||
},
|
||||
|
||||
isMigrating() {
|
||||
return this.isMigrating.get();
|
||||
},
|
||||
|
||||
isBoardReady() {
|
||||
return this.isBoardReady.get();
|
||||
},
|
||||
|
|
@ -221,9 +529,9 @@ BlazeComponent.extendComponent({
|
|||
const popupObserver = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
mutation.addedNodes.forEach(function(node) {
|
||||
if (node.nodeType === 1 &&
|
||||
if (node.nodeType === 1 &&
|
||||
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
|
||||
!node.closest('.js-swimlanes') &&
|
||||
!node.closest('.js-swimlanes') &&
|
||||
!node.closest('.swimlane') &&
|
||||
!node.closest('.list') &&
|
||||
!node.closest('.minicard')) {
|
||||
|
|
@ -339,9 +647,9 @@ BlazeComponent.extendComponent({
|
|||
.js-add-card[tabindex] {
|
||||
outline: none;
|
||||
}
|
||||
/* Sidebar hamburger menu button in header */
|
||||
.js-toggle-sidebar .fa-bars {
|
||||
color: #fff !important;
|
||||
/* Hamburger menu */
|
||||
.fa-bars, .icon-hamburger {
|
||||
color: #222 !important;
|
||||
}
|
||||
/* Grey icons in card detail header */
|
||||
.card-detail-header .fa, .card-detail-header .icon {
|
||||
|
|
@ -540,60 +848,47 @@ BlazeComponent.extendComponent({
|
|||
isViewSwimlanes() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
|
||||
if (currentUser) {
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
|
||||
// If no board view is set, default to swimlanes
|
||||
if (!boardView) {
|
||||
boardView = 'board-view-swimlanes';
|
||||
}
|
||||
|
||||
|
||||
return boardView === 'board-view-swimlanes';
|
||||
},
|
||||
|
||||
isViewLists() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
|
||||
if (currentUser) {
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
|
||||
return boardView === 'board-view-lists';
|
||||
},
|
||||
|
||||
isViewCalendar() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
|
||||
if (currentUser) {
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
|
||||
return boardView === 'board-view-cal';
|
||||
},
|
||||
|
||||
isViewGantt() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-gantt';
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) {
|
||||
|
|
@ -602,7 +897,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const swimlanes = currentBoard.swimlanes();
|
||||
const hasSwimlanes = swimlanes && swimlanes.length > 0;
|
||||
|
|
@ -637,24 +932,27 @@ BlazeComponent.extendComponent({
|
|||
const currentBoardId = Session.get('currentBoard');
|
||||
const isBoardReady = this.isBoardReady.get();
|
||||
const isConverting = this.isConverting.get();
|
||||
const isMigrating = this.isMigrating.get();
|
||||
const boardView = Utils.boardView();
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('=== BOARD DEBUG STATE ===');
|
||||
console.log('currentBoardId:', currentBoardId);
|
||||
console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
|
||||
console.log('isBoardReady:', isBoardReady);
|
||||
console.log('isConverting:', isConverting);
|
||||
console.log('isMigrating:', isMigrating);
|
||||
console.log('boardView:', boardView);
|
||||
console.log('========================');
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
currentBoardId,
|
||||
hasCurrentBoard: !!currentBoard,
|
||||
currentBoardTitle: currentBoard ? currentBoard.title : 'none',
|
||||
isBoardReady,
|
||||
isConverting,
|
||||
isMigrating,
|
||||
boardView
|
||||
};
|
||||
},
|
||||
|
|
@ -1021,8 +1319,3 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
}).register('calendarView');
|
||||
/**
|
||||
* Gantt View Component
|
||||
* Displays cards as a Gantt chart with start/due dates
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -49,12 +49,6 @@ THEME - NEPHRITIS
|
|||
border-bottom: 2px solid #27ae60;
|
||||
border-right: 2px solid #27ae60;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-nephritis .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae60 !important;
|
||||
}
|
||||
.board-color-nephritis .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
|
|
@ -156,12 +150,6 @@ THEME - Pomegranate
|
|||
border-bottom: 2px solid #c0392b;
|
||||
border-right: 2px solid #c0392b;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar {
|
||||
background-color: #f5d5d2 !important;
|
||||
}
|
||||
.board-color-pomegranate .checklist-progress-bar .checklist-progress {
|
||||
background-color: #c0392b !important;
|
||||
}
|
||||
.board-color-pomegranate .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeae9;
|
||||
}
|
||||
|
|
@ -263,12 +251,6 @@ THEME - Belize
|
|||
border-bottom: 2px solid #2980b9;
|
||||
border-right: 2px solid #2980b9;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-belize .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2980b9 !important;
|
||||
}
|
||||
.board-color-belize .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
|
|
@ -370,12 +352,6 @@ THEME - Wisteria
|
|||
border-bottom: 2px solid #8e44ad;
|
||||
border-right: 2px solid #8e44ad;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar {
|
||||
background-color: #e8d9f0 !important;
|
||||
}
|
||||
.board-color-wisteria .checklist-progress-bar .checklist-progress {
|
||||
background-color: #8e44ad !important;
|
||||
}
|
||||
.board-color-wisteria .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #f4ecf7;
|
||||
}
|
||||
|
|
@ -477,12 +453,6 @@ THEME - Midnight
|
|||
border-bottom: 2px solid #2c3e50;
|
||||
border-right: 2px solid #2c3e50;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-midnight .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e50 !important;
|
||||
}
|
||||
.board-color-midnight .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
|
|
@ -584,12 +554,6 @@ THEME - Pumpkin
|
|||
border-bottom: 2px solid #e67e22;
|
||||
border-right: 2px solid #e67e22;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar {
|
||||
background-color: #f9e5d1 !important;
|
||||
}
|
||||
.board-color-pumpkin .checklist-progress-bar .checklist-progress {
|
||||
background-color: #e67e22 !important;
|
||||
}
|
||||
.board-color-pumpkin .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #fdf2e9;
|
||||
}
|
||||
|
|
@ -691,12 +655,6 @@ THEME - Moderate Pink
|
|||
border-bottom: 2px solid #cd5a91;
|
||||
border-right: 2px solid #cd5a91;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar {
|
||||
background-color: #f4dde8 !important;
|
||||
}
|
||||
.board-color-moderatepink .checklist-progress-bar .checklist-progress {
|
||||
background-color: #cd5a91 !important;
|
||||
}
|
||||
.board-color-moderatepink .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #faeef4;
|
||||
}
|
||||
|
|
@ -798,12 +756,6 @@ THEME - Strong Cyan
|
|||
border-bottom: 2px solid #00aecc;
|
||||
border-right: 2px solid #00aecc;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar {
|
||||
background-color: #ccf2f9 !important;
|
||||
}
|
||||
.board-color-strongcyan .checklist-progress-bar .checklist-progress {
|
||||
background-color: #00aecc !important;
|
||||
}
|
||||
.board-color-strongcyan .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
|
|
@ -905,12 +857,6 @@ THEME - Lime Green
|
|||
border-bottom: 2px solid #4bbf6b;
|
||||
border-right: 2px solid #4bbf6b;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar {
|
||||
background-color: #daf4de !important;
|
||||
}
|
||||
.board-color-limegreen .checklist-progress-bar .checklist-progress {
|
||||
background-color: #4bbf6b !important;
|
||||
}
|
||||
.board-color-limegreen .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #edf9f0;
|
||||
}
|
||||
|
|
@ -1013,12 +959,6 @@ THEME - Dark
|
|||
border-bottom: 2px solid #2c3e51;
|
||||
border-right: 2px solid #2c3e51;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar {
|
||||
background-color: #d2dae2 !important;
|
||||
}
|
||||
.board-color-dark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2c3e51 !important;
|
||||
}
|
||||
.board-color-dark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e6ecf1;
|
||||
}
|
||||
|
|
@ -1222,12 +1162,6 @@ THEME - Relax
|
|||
border-bottom: 2px solid #27ae61;
|
||||
border-right: 2px solid #27ae61;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar {
|
||||
background-color: #d4f1dd !important;
|
||||
}
|
||||
.board-color-relax .checklist-progress-bar .checklist-progress {
|
||||
background-color: #27ae61 !important;
|
||||
}
|
||||
.board-color-relax .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e7faef;
|
||||
}
|
||||
|
|
@ -1358,12 +1292,6 @@ THEME - Corteza
|
|||
border-bottom: 2px solid #568ba2;
|
||||
border-right: 2px solid #568ba2;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar {
|
||||
background-color: #dce6ec !important;
|
||||
}
|
||||
.board-color-corteza .checklist-progress-bar .checklist-progress {
|
||||
background-color: #568ba2 !important;
|
||||
}
|
||||
.board-color-corteza .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef3f6;
|
||||
}
|
||||
|
|
@ -1469,12 +1397,6 @@ THEME - Clear Blue
|
|||
border-bottom: 2px solid #499bea;
|
||||
border-right: 2px solid #499bea;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar {
|
||||
background-color: #daeefb !important;
|
||||
}
|
||||
.board-color-clearblue .checklist-progress-bar .checklist-progress {
|
||||
background-color: #499bea !important;
|
||||
}
|
||||
.board-color-clearblue .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e0fbff;
|
||||
}
|
||||
|
|
@ -1503,7 +1425,7 @@ THEME - Clear Blue
|
|||
}
|
||||
.board-color-clearblue .list {
|
||||
background: rgba(255,255,255,0.35);
|
||||
margin: 10px 0;
|
||||
margin: 10px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
|
@ -1738,12 +1660,6 @@ THEME - Natural
|
|||
border-bottom: 2px solid #596557;
|
||||
border-right: 2px solid #596557;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar {
|
||||
background-color: #dee0dd !important;
|
||||
}
|
||||
.board-color-natural .checklist-progress-bar .checklist-progress {
|
||||
background-color: #596557 !important;
|
||||
}
|
||||
.board-color-natural .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eef0ee;
|
||||
}
|
||||
|
|
@ -1854,12 +1770,6 @@ THEME - Modern
|
|||
border-bottom: 2px solid #2a80b8;
|
||||
border-right: 2px solid #2a80b8;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar {
|
||||
background-color: #d1e7f5 !important;
|
||||
}
|
||||
.board-color-modern .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a80b8 !important;
|
||||
}
|
||||
.board-color-modern .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e8f3fa;
|
||||
}
|
||||
|
|
@ -2152,12 +2062,6 @@ THEME - Modern Dark
|
|||
border-bottom: 2px solid #2a2a2a;
|
||||
border-right: 2px solid #2a2a2a;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar {
|
||||
background-color: #d1d1d1 !important;
|
||||
}
|
||||
.board-color-moderndark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #2a2a2a !important;
|
||||
}
|
||||
.board-color-moderndark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #eaeaea;
|
||||
}
|
||||
|
|
@ -2643,12 +2547,6 @@ THEME - Exodark
|
|||
border-bottom: 2px solid #dbdbdb!important;/*Fix contrast of checkbox*/
|
||||
border-right: 2px solid #dbdbdb!important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar {
|
||||
background-color: #cccccc !important;
|
||||
}
|
||||
.board-color-exodark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.board-color-exodark .is-multiselection-active .multi-selection-checkbox.is-checked + .minicard {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
|
|
@ -2690,7 +2588,7 @@ THEME - Exodark
|
|||
background: #222;
|
||||
}
|
||||
.board-color-exodark .list {
|
||||
margin: 10px 0;
|
||||
margin: 10px;
|
||||
color: #fff;
|
||||
border-radius: 15px;
|
||||
background-color: #1c1c1c;
|
||||
|
|
@ -3242,12 +3140,6 @@ THEME - Clean Dark
|
|||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar {
|
||||
background-color: #6b6b78 !important;
|
||||
}
|
||||
.board-color-cleandark .checklist-progress-bar .checklist-progress {
|
||||
background-color: #23232B !important;
|
||||
}
|
||||
|
||||
.board-color-cleandark .allBoards {
|
||||
white-space: nowrap;
|
||||
|
|
@ -4000,13 +3892,6 @@ THEME - Clean Light
|
|||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.board-color-cleanlight .checklist-progress-bar {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
.board-color-cleanlight .checklist-progress-bar .checklist-progress {
|
||||
background-color: #c0c0c0 !important;
|
||||
color: #010101 !important;
|
||||
}
|
||||
|
||||
.board-color-cleanlight .allBoards {
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -505,73 +505,73 @@
|
|||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
.mobile-mode .swimlane {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .swimlane-header {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
}
|
||||
.mobile-mode .swimlane .swimlane-header {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
padding: 1rem !important;
|
||||
font-size: clamp(18px, 2.5vw, 32px) !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
}
|
||||
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
.mobile-mode .swimlane .lists {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
.mobile-mode .list {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
margin: 0 0 2rem 0 !important;
|
||||
padding: 0 !important;
|
||||
float: none !important;
|
||||
clear: both !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: 2px solid #ccc !important;
|
||||
flex: none !important;
|
||||
flex-basis: auto !important;
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.mobile-mode .list:first-child {
|
||||
margin-left: 0 !important;
|
||||
|
|
@ -667,9 +667,9 @@
|
|||
flex-wrap: nowrap !important;
|
||||
align-items: stretch !important;
|
||||
justify-content: flex-start !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,39 +14,41 @@ template(name="boardHeaderBar")
|
|||
with currentBoard
|
||||
if currentUser.isBoardAdmin
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
| ✏️
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}" aria-label="{{#if isStarred}}{{_ 'star-board-short-unstar'}}{{else}}{{_ 'star-board-short-star'}}{{/if}}")
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
if showStarCounter
|
||||
span
|
||||
= currentBoard.stars
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
span {{_ currentBoard.permission}}
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
| 🔔
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
| 🔕
|
||||
span {{_ watchLevel}}
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
if showStarCounter
|
||||
span.board-star-counter {{currentBoard.stars}}
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
| {{sortCardsIcon}}
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
| ❌
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
| 🚪
|
||||
span {{_ 'log-in'}}
|
||||
|
||||
.board-header-btns.center
|
||||
|
|
@ -57,114 +59,99 @@ template(name="boardHeaderBar")
|
|||
if currentUser
|
||||
with currentBoard
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
| ✏️
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
|
||||
a.board-header-btn(
|
||||
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
|
||||
title="{{_ currentBoard.permission}}")
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
| {{#if currentBoard.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
|
||||
a.board-header-btn.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
if $eq watchLevel "watching"
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
if $eq watchLevel "tracking"
|
||||
i.fa.fa-bell
|
||||
| 🔔
|
||||
if $eq watchLevel "muted"
|
||||
i.fa.fa-bell-slash
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
| 🔕
|
||||
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
|
||||
i.fa.fa-sort
|
||||
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
|
||||
| {{sortCardsIcon}}
|
||||
if isSortActive
|
||||
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
|
||||
i.fa.fa-times-thin
|
||||
| ❌
|
||||
|
||||
else
|
||||
a.board-header-btn.js-log-in(
|
||||
title="{{_ 'log-in'}}")
|
||||
i.fa.fa-sign-in
|
||||
| 🚪
|
||||
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
| 📦
|
||||
|
||||
//if showSort
|
||||
//
|
||||
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
//
|
||||
i.fa(class="{{directionClass}}")
|
||||
//
|
||||
span {{_ 'sort'}}{{_ listSortShortDesc}}
|
||||
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
// i.fa(class="{{directionClass}}")
|
||||
// span {{_ 'sort'}}{{_ listSortShortDesc}}
|
||||
|
||||
a.board-header-btn.js-open-filter-view(
|
||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
|
||||
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
|
||||
i.fa.fa-filter
|
||||
span {{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}
|
||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||
| 🔽
|
||||
if Filter.isActive
|
||||
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
| ❌
|
||||
|
||||
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
|
||||
i.fa.fa-search
|
||||
span {{_ 'search'}}
|
||||
| 🔍
|
||||
|
||||
unless currentBoard.isTemplatesBoard
|
||||
a.board-header-btn.js-toggle-board-view
|
||||
i.fa.fa-caret-down
|
||||
a.board-header-btn.js-toggle-board-view(
|
||||
title="{{_ 'board-view'}}")
|
||||
| ▼
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
i.fa.fa-th-large
|
||||
| 🏊
|
||||
if $eq boardView 'board-view-lists'
|
||||
i.fa.fa-trello
|
||||
| 📋
|
||||
if $eq boardView 'board-view-cal'
|
||||
i.fa.fa-calendar
|
||||
if $eq boardView 'board-view-gantt'
|
||||
i.fa.fa-bar-chart
|
||||
span
|
||||
if $eq boardView 'board-view-swimlanes'
|
||||
| {{_ 'swimlanes'}}
|
||||
if $eq boardView 'board-view-lists'
|
||||
| {{_ 'lists'}}
|
||||
if $eq boardView 'board-view-cal'
|
||||
| {{_ 'calendar'}}
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| {{_ 'gantt'}}
|
||||
| 📅
|
||||
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
|
||||
i.fa.fa-check-square-o
|
||||
span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
|
||||
if MultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
i.fa.fa-times-thin
|
||||
| ☑️
|
||||
if MultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
| ❌
|
||||
|
||||
.separator
|
||||
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
|
||||
i.fa.fa-bars
|
||||
| ☰
|
||||
|
||||
template(name="boardVisibilityList")
|
||||
ul.pop-over-list
|
||||
li
|
||||
with "private"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-lock
|
||||
| 🔒
|
||||
| {{_ 'private'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'private-desc'}}
|
||||
if notAllowPrivateVisibilityOnly
|
||||
li
|
||||
with "public"
|
||||
a.js-select-visibility
|
||||
i.fa.fa-globe
|
||||
| 🌐
|
||||
| {{_ 'public'}}
|
||||
if visibilityCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'public-desc'}}
|
||||
|
||||
template(name="boardChangeVisibilityPopup")
|
||||
|
|
@ -175,26 +162,26 @@ template(name="boardChangeWatchPopup")
|
|||
li
|
||||
with "watching"
|
||||
a.js-select-watch
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
| {{_ 'watching'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'watching-info'}}
|
||||
li
|
||||
with "tracking"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell
|
||||
| 🔔
|
||||
| {{_ 'tracking'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'tracking-info'}}
|
||||
li
|
||||
with "muted"
|
||||
a.js-select-watch
|
||||
i.fa.fa-bell-slash
|
||||
| 🔕
|
||||
| {{_ 'muted'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span.sub-name {{_ 'muted-info'}}
|
||||
|
||||
template(name="boardChangeViewPopup")
|
||||
|
|
@ -202,31 +189,24 @@ template(name="boardChangeViewPopup")
|
|||
li
|
||||
with "board-view-swimlanes"
|
||||
a.js-open-swimlanes-view
|
||||
i.fa.fa-th-large
|
||||
| {{_ 'swimlanes'}}
|
||||
| 🏊
|
||||
| {{_ 'board-view-swimlanes'}}
|
||||
if $eq Utils.boardView "board-view-swimlanes"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
li
|
||||
with "board-view-lists"
|
||||
a.js-open-lists-view
|
||||
i.fa.fa-trello
|
||||
| 📋
|
||||
| {{_ 'board-view-lists'}}
|
||||
if $eq Utils.boardView "board-view-lists"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
li
|
||||
with "board-view-cal"
|
||||
a.js-open-cal-view
|
||||
i.fa.fa-calendar
|
||||
| 📅
|
||||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
i.fa.fa-check
|
||||
li
|
||||
with "board-view-gantt"
|
||||
a.js-open-gantt-view
|
||||
i.fa.fa-bar-chart
|
||||
| {{_ 'board-view-gantt'}}
|
||||
if $eq Utils.boardView "board-view-gantt"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
|
|
@ -238,11 +218,11 @@ template(name="createBoard")
|
|||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
span 🌐
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
span 🔒
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
|
|
@ -267,41 +247,11 @@ template(name="createBoardPopup")
|
|||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
span 🌐
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
// New popup for Template Container creation; shares the same form content
|
||||
template(name="createTemplateContainerPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-new-board-title(type="text" placeholder="{{_ 'bucket-example'}}" autofocus required)
|
||||
if visibilityMenuIsOpen.get
|
||||
+boardVisibilityList
|
||||
else
|
||||
p.quiet
|
||||
if $eq visibility.get 'public'
|
||||
span.fa.fa-globe.colorful
|
||||
= " "
|
||||
| {{{_ 'board-public-info'}}}
|
||||
else
|
||||
span.fa.fa-lock.colorful
|
||||
span 🔒
|
||||
= " "
|
||||
| {{{_ 'board-private-info'}}}
|
||||
a.js-change-visibility {{_ 'change'}}.
|
||||
|
|
@ -317,30 +267,19 @@ template(name="createTemplateContainerPopup")
|
|||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
//template(name="listsortPopup")
|
||||
//
|
||||
h2
|
||||
//
|
||||
| {{_ 'list-sort-by'}}
|
||||
//
|
||||
hr
|
||||
//
|
||||
ul.pop-over-list
|
||||
//
|
||||
each value in allowedSortValues
|
||||
//
|
||||
li
|
||||
//
|
||||
a.js-sort-by(name="{{value.name}}")
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
| {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
//
|
||||
| {{_ value.label }}{{_ value.shortLabel}}
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
i.fa.fa-check
|
||||
// h2
|
||||
// | {{_ 'list-sort-by'}}
|
||||
// hr
|
||||
// ul.pop-over-list
|
||||
// each value in allowedSortValues
|
||||
// li
|
||||
// a.js-sort-by(name="{{value.name}}")
|
||||
// if $eq sortby value.name
|
||||
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
// | {{_ value.label }}{{_ value.shortLabel}}
|
||||
// if $eq sortby value.name
|
||||
// | ✅
|
||||
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
label
|
||||
|
|
@ -360,21 +299,21 @@ template(name="cardsSortPopup")
|
|||
ul.pop-over-list
|
||||
li
|
||||
a.js-sort-due
|
||||
i.fa.fa-calendar
|
||||
| 📅
|
||||
| {{_ 'due-date'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-title
|
||||
i.fa.fa-sort-alpha-asc
|
||||
| 🔤
|
||||
| {{_ 'title-alphabetically'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-desc
|
||||
i.fa.fa-arrow-down
|
||||
| ⬇️
|
||||
| {{_ 'created-at-newest-first'}}
|
||||
hr
|
||||
li
|
||||
a.js-sort-created-asc
|
||||
i.fa.fa-arrow-up
|
||||
| ⬆️
|
||||
| {{_ 'created-at-oldest-first'}}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
|
||||
/*
|
||||
|
|
@ -10,7 +9,7 @@ const UPCLS = 'fa-sort-up';
|
|||
const sortCardsBy = new ReactiveVar('');
|
||||
|
||||
Template.boardChangeTitlePopup.events({
|
||||
async submit(event, templateInstance) {
|
||||
submit(event, templateInstance) {
|
||||
const newTitle = templateInstance
|
||||
.$('.js-board-name')
|
||||
.val()
|
||||
|
|
@ -20,8 +19,8 @@ Template.boardChangeTitlePopup.events({
|
|||
.val()
|
||||
.trim();
|
||||
if (newTitle) {
|
||||
await this.rename(newTitle);
|
||||
await this.setDescription(newDesc);
|
||||
this.rename(newTitle);
|
||||
this.setDescription(newDesc);
|
||||
Popup.back();
|
||||
}
|
||||
event.preventDefault();
|
||||
|
|
@ -73,10 +72,7 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
'click .js-edit-board-title': Popup.open('boardChangeTitle'),
|
||||
'click .js-star-board'() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
if (boardId) {
|
||||
Meteor.call('toggleBoardStar', boardId);
|
||||
}
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(Session.get('currentBoard'));
|
||||
},
|
||||
'click .js-open-board-menu': Popup.open('boardMenu'),
|
||||
'click .js-change-visibility': Popup.open('boardChangeVisibility'),
|
||||
|
|
@ -182,7 +178,7 @@ Template.boardHeaderBar.helpers({
|
|||
if (!sortBy) {
|
||||
return '🃏'; // Card icon when nothing is selected
|
||||
}
|
||||
|
||||
|
||||
// Determine which sort option is active based on sortBy object
|
||||
if (sortBy.dueAt) {
|
||||
return '📅'; // Due date icon
|
||||
|
|
@ -191,7 +187,7 @@ Template.boardHeaderBar.helpers({
|
|||
} else if (sortBy.createdAt) {
|
||||
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
|
||||
}
|
||||
|
||||
|
||||
return '🃏'; // Default card icon
|
||||
},
|
||||
});
|
||||
|
|
@ -209,10 +205,6 @@ Template.boardChangeViewPopup.events({
|
|||
Utils.setBoardView('board-view-cal');
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-open-gantt-view'() {
|
||||
Utils.setBoardView('board-view-gantt');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
|
|
@ -299,15 +291,6 @@ const CreateBoard = BlazeComponent.extendComponent({
|
|||
},
|
||||
);
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
|
||||
} else {
|
||||
|
|
@ -326,15 +309,6 @@ const CreateBoard = BlazeComponent.extendComponent({
|
|||
boardId: this.boardId.get(),
|
||||
});
|
||||
|
||||
// Assign to space if one was selected
|
||||
const spaceId = Session.get('createBoardInWorkspace');
|
||||
if (spaceId) {
|
||||
Meteor.call('assignBoardToWorkspace', this.boardId.get(), spaceId, (err) => {
|
||||
if (err) console.error('Error assigning board to space:', err);
|
||||
});
|
||||
Session.set('createBoardInWorkspace', null); // Clear after use
|
||||
}
|
||||
|
||||
Utils.goBoardId(this.boardId.get());
|
||||
}
|
||||
},
|
||||
|
|
@ -356,18 +330,11 @@ const CreateBoard = BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('createBoardPopup');
|
||||
|
||||
(class CreateTemplateContainerPopup extends CreateBoard {
|
||||
onRendered() {
|
||||
// Always pre-check the template container checkbox for this popup
|
||||
$('#add-template-container').addClass('is-checked');
|
||||
}
|
||||
}).register('createTemplateContainerPopup');
|
||||
|
||||
(class HeaderBarCreateBoard extends CreateBoard {
|
||||
async onSubmit(event) {
|
||||
onSubmit(event) {
|
||||
super.onSubmit(event);
|
||||
// Immediately star boards crated with the headerbar popup.
|
||||
await ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(this.boardId.get());
|
||||
}
|
||||
}.register('headerBarCreateBoardPopup'));
|
||||
|
||||
|
|
|
|||
|
|
@ -8,273 +8,6 @@
|
|||
padding: 1vh 0;
|
||||
}
|
||||
|
||||
/* Two-column layout for All Boards */
|
||||
.boards-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.boards-left-menu {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.boards-left-menu ul.menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.boards-left-menu .menu-item {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.boards-left-menu .menu-item a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.boards-left-menu .menu-item .menu-label {
|
||||
flex: 1;
|
||||
}
|
||||
.boards-left-menu .menu-item .menu-count {
|
||||
background: #ddd;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.boards-left-menu .menu-item.active a,
|
||||
.boards-left-menu .menu-item a:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.boards-left-menu .menu-item.active .menu-count {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
/* Drag-over state for menu items (for dropping boards on Remaining) */
|
||||
.boards-left-menu .menu-item a.drag-over {
|
||||
background: #d0e8ff;
|
||||
border: 2px dashed #2196F3;
|
||||
}
|
||||
|
||||
.workspaces-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.workspaces-header .js-add-space {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.workspace-tree {
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.workspace-node {
|
||||
margin: 2px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workspace-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node.dragging > .workspace-node-content {
|
||||
opacity: 0.5;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-node.drag-over > .workspace-node-content {
|
||||
background: #d0e8ff;
|
||||
border: 2px dashed #2196F3;
|
||||
}
|
||||
|
||||
.workspace-drag-handle {
|
||||
cursor: grab;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.workspace-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.workspace-node .js-select-space {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workspace-node .workspace-count {
|
||||
background: #ddd;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workspace-node .js-edit-space,
|
||||
.workspace-node .js-add-subspace {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.workspace-node .js-edit-space:hover,
|
||||
.workspace-node .js-add-subspace:hover {
|
||||
opacity: 1;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.workspace-node.active > .workspace-node-content .js-select-space,
|
||||
.workspace-node > .workspace-node-content:hover .js-select-space {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.workspace-node.active .workspace-count {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
.boards-right-grid {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.boards-path-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.boards-path-header .path-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.boards-path-header .multiselection-hint {
|
||||
background: #FFF3CD;
|
||||
color: #856404;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
border: 1px solid #FFE69C;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.boards-path-header .path-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.boards-path-header .path-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.boards-path-header .path-text {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn {
|
||||
padding: 6px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn:hover {
|
||||
background: #f0f0f0;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn.emphasis {
|
||||
background: #2196F3;
|
||||
color: #fff;
|
||||
border-color: #2196F3;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn.emphasis:hover {
|
||||
background: #1976D2;
|
||||
box-shadow: 0 3px 12px rgba(33, 150, 243, 0.7);
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn-close {
|
||||
padding: 4px 10px;
|
||||
background: #f44336;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-left: 10px; /* Extra space between MultiSelection toggle and Remove Filter */
|
||||
}
|
||||
|
||||
.boards-path-header .board-header-btn-close:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -373,35 +106,23 @@
|
|||
.board-list li.starred .is-star-active,
|
||||
.board-list li.starred .is-not-star-active {
|
||||
opacity: 1;
|
||||
color: #ffd700;
|
||||
}
|
||||
/* Show star icon on hover even for non-starred boards */
|
||||
.board-list li:hover .is-star-active,
|
||||
.board-list li:hover .is-not-star-active {
|
||||
opacity: 1;
|
||||
}
|
||||
.board-list .board-list-item {
|
||||
overflow: hidden;
|
||||
background-color: inherit; /* Inherit board color from parent li.js-board */
|
||||
background-color: #999;
|
||||
color: #f6f6f6;
|
||||
min-height: 100px;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
border-radius: 0; /* No border-radius - parent .js-board has it */
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
padding: 36px 8px 32px 8px; /* Top padding for drag handle, bottom for checkbox */
|
||||
margin: 0; /* No margin - moved to parent .js-board */
|
||||
padding: 8px;
|
||||
margin: 8px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.board-list .board-list-item > .js-open-board {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
.board-list .board-list-item.template-container {
|
||||
border: 4px solid #fff;
|
||||
}
|
||||
|
|
@ -429,27 +150,13 @@
|
|||
.board-list .js-add-board .label {
|
||||
font-weight: normal;
|
||||
line-height: 56px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #999; /* Darker background for better text contrast */
|
||||
border-radius: 3px;
|
||||
padding: 36px 8px 32px 8px;
|
||||
color: #fff; /* White text */
|
||||
}
|
||||
.board-list .js-add-board .label i {
|
||||
color: #fff; /* White icon */
|
||||
}
|
||||
.board-list .js-add-board .label:hover {
|
||||
background-color: #808080; /* Even darker on hover */
|
||||
}
|
||||
.board-list .js-add-board .label:hover i {
|
||||
color: #fff; /* Keep icon white on hover */
|
||||
.board-list .js-add-board :hover {
|
||||
background-color: #939393;
|
||||
}
|
||||
.board-list .is-star-active,
|
||||
.board-list .is-not-star-active {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
font-size: 14px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
|
|
@ -457,6 +164,7 @@
|
|||
padding: 9px 9px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition-duration: 0.15s;
|
||||
transition-property: color, font-size, background;
|
||||
}
|
||||
|
|
@ -530,107 +238,6 @@
|
|||
.board-list li:hover a .is-not-star-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Board drag handle - always visible and positioned at top */
|
||||
.board-list .board-handle {
|
||||
position: absolute;
|
||||
padding: 4px 6px;
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: grab;
|
||||
opacity: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.board-list .board-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.board-list .board-handle:hover {
|
||||
background: rgba(255, 255, 0, 0.8) !important;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Multiselection checkbox on board items */
|
||||
.board-list .board-list-item .multi-selection-checkbox {
|
||||
position: absolute !important;
|
||||
bottom: 4px !important;
|
||||
left: 4px !important;
|
||||
top: auto !important;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #fff;
|
||||
background: rgba(0,0,0,0.5);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
z-index: 11;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
transform: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox:hover {
|
||||
background: rgba(0,0,0,0.7);
|
||||
transform: scale(1.15) !important;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #3cb500;
|
||||
border-color: #3cb500;
|
||||
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
top: auto !important;
|
||||
left: 4px !important;
|
||||
transform: none !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked::after {
|
||||
content: '✓';
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Grey checkboxes when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #7a7a7a;
|
||||
border-color: #7a7a7a;
|
||||
box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
|
||||
}
|
||||
|
||||
body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #7a7a7a;
|
||||
box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
|
||||
}
|
||||
|
||||
.board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #3cb500;
|
||||
outline-offset: -4px;
|
||||
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Visual hint when multiselection is active */
|
||||
.board-list.is-multiselection-active .board-list-item {
|
||||
border: 2px dashed rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.board-backgrounds-list .board-background-select {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
|
|
@ -664,19 +271,8 @@ body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checke
|
|||
}
|
||||
.board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
font-size: 25px;
|
||||
color: #3cb500;
|
||||
color: #fff;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
/* Prevent Grey Icons from affecting checkmarks in background color list */
|
||||
body.grey-icons-enabled .checkmark-no-grey {
|
||||
filter: none !important;
|
||||
-webkit-filter: none !important;
|
||||
}
|
||||
|
||||
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
|
||||
.board-list.mobile-view {
|
||||
height: calc(100vh - 120px);
|
||||
|
|
@ -1143,62 +739,9 @@ body.grey-icons-enabled .checkmark-no-grey {
|
|||
#resetBtn {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn {
|
||||
background: #f44336;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
#resetBtn.filter-reset-btn .reset-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.js-board {
|
||||
display: block;
|
||||
background-color: #999; /* Default gray background if no color class is applied */
|
||||
border-radius: 3px; /* Rounded corners for board items */
|
||||
overflow: hidden; /* Ensure children respect rounded corners */
|
||||
margin: 8px; /* Space between board items */
|
||||
}
|
||||
|
||||
/* Reset background for add-board button */
|
||||
.js-add-board {
|
||||
background-color: transparent !important;
|
||||
margin: 8px !important; /* Keep margin for add-board */
|
||||
}
|
||||
|
||||
/* Apply board colors to li.js-board parent instead of just the link */
|
||||
.board-list .board-color-nephritis { background-color: #27ae60; }
|
||||
.board-list .board-color-pomegranate { background-color: #c0392b; }
|
||||
.board-list .board-color-belize { background-color: #2980b9; }
|
||||
.board-list .board-color-wisteria { background-color: #8e44ad; }
|
||||
.board-list .board-color-midnight { background-color: #2c3e50; }
|
||||
.board-list .board-color-pumpkin { background-color: #e67e22; }
|
||||
.board-list .board-color-moderatepink { background-color: #cd5a91; }
|
||||
.board-list .board-color-strongcyan { background-color: #00aecc; }
|
||||
.board-list .board-color-limegreen { background-color: #4bbf6b; }
|
||||
.board-list .board-color-dark { background-color: #2c3e51; }
|
||||
.board-list .board-color-relax { background-color: #27ae61; }
|
||||
.board-list .board-color-corteza { background-color: #568ba2; }
|
||||
.board-list .board-color-clearblue { background-color: #3498db; }
|
||||
.board-list .board-color-natural { background-color: #596557; }
|
||||
.board-list .board-color-modern { background-color: #2a80b8; }
|
||||
.board-list .board-color-moderndark { background-color: #2a2a2a; }
|
||||
.board-list .board-color-exodark { background-color: #222; }
|
||||
|
||||
.minicard-members {
|
||||
padding: 6px 0 6px 8px;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -2,238 +2,158 @@ template(name="boardList")
|
|||
.wrapper
|
||||
.board-list-header
|
||||
|
||||
.boards-layout
|
||||
// Left menu
|
||||
.boards-left-menu
|
||||
ul.menu
|
||||
li(class="menu-item {{#if isSelectedMenu 'starred'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="starred")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-star
|
||||
| {{_ 'allboards.starred'}}
|
||||
span.menu-count {{menuItemCount 'starred'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'templates'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="templates")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'allboards.templates'}}
|
||||
span.menu-count {{menuItemCount 'templates'}}
|
||||
li(class="menu-item {{#if isSelectedMenu 'remaining'}}active{{/if}}")
|
||||
a.js-select-menu(data-type="remaining")
|
||||
span.menu-label
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder
|
||||
| {{_ 'allboards.remaining'}}
|
||||
span.menu-count {{menuItemCount 'remaining'}}
|
||||
.workspaces-header
|
||||
span
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder-open
|
||||
| {{_ 'allboards.workspaces'}}
|
||||
a.js-add-workspace(title="{{_ 'allboards.add-workspace'}}") +
|
||||
// Workspaces tree
|
||||
+workspaceTree(nodes=workspacesTree selectedWorkspaceId=selectedWorkspaceId)
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each teamsDatas
|
||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||
|
||||
// Existing filter by orgs/teams (kept)
|
||||
ul.AllBoardTeamsOrgs
|
||||
li.AllBoardTeams
|
||||
if userHasTeams
|
||||
select.js-AllBoardTeams#jsAllBoardTeams("multiple")
|
||||
option(value="-1") {{_ 'teams'}} :
|
||||
each teamsDatas
|
||||
option(value="{{teamId}}") {{_ teamDisplayName}}
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
|
||||
li.AllBoardOrgs
|
||||
if userHasOrgs
|
||||
select.js-AllBoardOrgs#jsAllBoardOrgs("multiple")
|
||||
option(value="-1") {{_ 'organizations'}} :
|
||||
each orgsDatas
|
||||
option(value="{{orgId}}") {{orgDisplayName}}
|
||||
//li.AllBoardTemplates
|
||||
// if userHasTemplates
|
||||
// select.js-AllBoardTemplates#jsAllBoardTemplates("multiple")
|
||||
// option(value="-1") {{_ 'templates'}} :
|
||||
// each templatesDatas
|
||||
// option(value="{{templateId}}") {{_ templateDisplayName}}
|
||||
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
span.emoji-icon
|
||||
i.fa.fa-search
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
button#resetBtn.filter-reset-btn
|
||||
span.reset-icon
|
||||
span.emoji-icon
|
||||
i.fa.fa-times-thin
|
||||
span {{_ 'filter-clear'}}
|
||||
li.AllBoardBtns
|
||||
div.AllBoardButtonsContainer
|
||||
if userHasOrgsOrTeams
|
||||
i.fa.fa-filter
|
||||
input#filterBtn(type="button" value="{{_ 'filter'}}")
|
||||
input#resetBtn(type="button" value="{{_ 'filter-clear'}}")
|
||||
|
||||
// Right boards grid
|
||||
.boards-right-grid
|
||||
.boards-path-header
|
||||
.path-left
|
||||
span.path-icon.emoji-icon {{currentMenuPath.icon}}
|
||||
span.path-text {{currentMenuPath.text}}
|
||||
if BoardMultiSelection.isActive
|
||||
span.multiselection-hint
|
||||
span.emoji-icon
|
||||
i.fa.fa-thumb-tack
|
||||
| {{_ 'multi-selection-active'}}
|
||||
.path-right
|
||||
if canModifyBoards
|
||||
if hasBoardsSelected
|
||||
button.js-archive-selected-boards.board-header-btn
|
||||
span.emoji-icon
|
||||
i.fa.fa-archive
|
||||
span {{_ 'archive-board'}}
|
||||
button.js-duplicate-selected-boards.board-header-btn
|
||||
span.emoji-icon
|
||||
i.fa.fa-clipboard
|
||||
span {{_ 'duplicate-board'}}
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
|
||||
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-check-square-o
|
||||
if BoardMultiSelection.isActive
|
||||
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-times
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
|
||||
li.js-add-board
|
||||
if isSelectedMenu 'templates'
|
||||
a.board-list-item.label(title="{{_ 'add-template-container'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-template-container'}}
|
||||
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
li.js-add-board
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
| {{_ 'add-board'}}
|
||||
each boards
|
||||
li(class="{{_id}}" class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board
|
||||
if isInvited
|
||||
.board-list-item
|
||||
span.details
|
||||
span.board-list-item-name= title
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc {{_ 'just-invited'}}
|
||||
button.js-accept-invite.primary {{_ 'accept'}}
|
||||
button.js-decline-invite {{_ 'decline'}}
|
||||
else
|
||||
if $eq type "template-container"
|
||||
a.js-open-board.template-container.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
+viewer
|
||||
= title
|
||||
i.fa.js-star-board(
|
||||
class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
i.fa.js-has-spenttime-cards(
|
||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
i.fa.board-handle(
|
||||
class="fa-arrows"
|
||||
title="{{_ 'drag-board'}}")
|
||||
if isSandstorm
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if isAdministrable
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if currentUser.isAdmin
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-board'}}
|
||||
each boards
|
||||
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
|
||||
if isInvited
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.details
|
||||
span.board-list-item-name= title
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
p.board-list-item-desc {{_ 'just-invited'}}
|
||||
button.js-accept-invite.primary {{_ 'accept'}}
|
||||
button.js-decline-invite {{_ 'decline'}}
|
||||
else
|
||||
if $eq type "template-container"
|
||||
.template-container.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
+viewer
|
||||
= title
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
span.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
else
|
||||
.board-list-item
|
||||
if BoardMultiSelection.isActive
|
||||
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
|
||||
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
span.board-handle(title="{{_ 'drag-board'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
||||
+viewer
|
||||
= title
|
||||
unless currentSetting.hideBoardMemberList
|
||||
if allowsBoardMemberList
|
||||
.minicard-members
|
||||
each member in boardMembers _id
|
||||
a.name
|
||||
+userAvatar(userId=member noRemove=true)
|
||||
unless currentSetting.hideCardCounterList
|
||||
if allowsCardCounterList
|
||||
.minicard-lists.flex.flex-wrap
|
||||
each list in boardLists _id
|
||||
.item
|
||||
| {{ list }}
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
span.js-has-spenttime-cards(
|
||||
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock-o
|
||||
a.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
span.emoji-icon
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
|
||||
+viewer
|
||||
= title
|
||||
unless currentSetting.hideBoardMemberList
|
||||
if allowsBoardMemberList
|
||||
.minicard-members
|
||||
each member in boardMembers _id
|
||||
a.name
|
||||
+userAvatar(userId=member noRemove=true)
|
||||
unless currentSetting.hideCardCounterList
|
||||
if allowsCardCounterList
|
||||
.minicard-lists.flex.flex-wrap
|
||||
each list in boardLists _id
|
||||
.item
|
||||
| {{ list }}
|
||||
a.js-star-board(
|
||||
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
|
||||
title="{{_ 'star-board-title'}}")
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
p.board-list-item-desc
|
||||
+viewer
|
||||
= description
|
||||
if hasSpentTimeCards
|
||||
i.fa.js-has-spenttime-cards(
|
||||
class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}"
|
||||
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
|
||||
i.fa.board-handle(
|
||||
class="fa-arrows"
|
||||
title="{{_ 'drag-board'}}")
|
||||
if isSandstorm
|
||||
a.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
| 📋
|
||||
a.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
| 📦
|
||||
else if isAdministrable
|
||||
a.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
| 📋
|
||||
a.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
| 📦
|
||||
else if currentUser.isAdmin
|
||||
a.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
| 📋
|
||||
a.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
| 📦
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ title }}
|
||||
//.board-header-btns.right
|
||||
//
|
||||
a.board-header-btn.js-open-archived-board
|
||||
//
|
||||
i.fa.fa-archive
|
||||
//
|
||||
span {{_ 'archives'}}
|
||||
//
|
||||
a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
//
|
||||
i.fa.fa-clone
|
||||
//
|
||||
span {{_ 'templates'}}
|
||||
|
||||
// Recursive template for workspaces tree
|
||||
template(name="workspaceTree")
|
||||
if nodes
|
||||
ul.workspace-tree.js-workspace-tree
|
||||
each nodes
|
||||
li.workspace-node(class="{{#if $eq id selectedWorkspaceId}}active{{/if}}" data-workspace-id="{{id}}" draggable="true")
|
||||
.workspace-node-content
|
||||
span.workspace-drag-handle
|
||||
span.emoji-icon
|
||||
i.fa.fa-arrows
|
||||
|
||||
a.js-select-workspace(data-id="{{id}}")
|
||||
span.workspace-icon
|
||||
if icon
|
||||
+viewer
|
||||
= icon
|
||||
else
|
||||
span.emoji-icon
|
||||
i.fa.fa-folder
|
||||
span.workspace-name= name
|
||||
a.js-edit-workspace(data-id="{{id}}" title="{{_ 'allboards.edit-workspace'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-pencil-square-o
|
||||
span.workspace-count {{workspaceCount id}}
|
||||
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
|
||||
if children
|
||||
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
|
||||
// a.board-header-btn.js-open-archived-board
|
||||
// i.fa.fa-archive
|
||||
// span {{_ 'archives'}}
|
||||
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
|
||||
// i.fa.fa-clone
|
||||
// span {{_ 'templates'}}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import getSlug from 'limax';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
|
||||
|
|
@ -16,10 +14,7 @@ Template.boardList.helpers({
|
|||
return Utils.isMiniScreen() && Session.get('currentBoard'); */
|
||||
return true;
|
||||
},
|
||||
BoardMultiSelection() {
|
||||
return BoardMultiSelection;
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
Template.boardListHeaderBar.events({
|
||||
'click .js-open-archived-board'() {
|
||||
|
|
@ -27,7 +22,8 @@ Template.boardListHeaderBar.events({
|
|||
},
|
||||
});
|
||||
|
||||
Template.boardList.events({});
|
||||
Template.boardList.events({
|
||||
});
|
||||
|
||||
Template.boardListHeaderBar.helpers({
|
||||
title() {
|
||||
|
|
@ -49,85 +45,17 @@ BlazeComponent.extendComponent({
|
|||
onCreated() {
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('tableVisibilityModeSettings');
|
||||
this.selectedMenu = new ReactiveVar('starred');
|
||||
this.selectedWorkspaceIdVar = new ReactiveVar(null);
|
||||
this.workspacesTreeVar = new ReactiveVar([]);
|
||||
let currUser = ReactiveCache.getCurrentUser();
|
||||
let userLanguage;
|
||||
if (currUser && currUser.profile) {
|
||||
userLanguage = currUser.profile.language;
|
||||
userLanguage = currUser.profile.language
|
||||
}
|
||||
if (userLanguage) {
|
||||
TAPi18n.setLanguage(userLanguage);
|
||||
}
|
||||
// Load workspaces tree reactively
|
||||
this.autorun(() => {
|
||||
const u = ReactiveCache.getCurrentUser();
|
||||
const tree = (u && u.profile && u.profile.boardWorkspacesTree) || [];
|
||||
this.workspacesTreeVar.set(tree);
|
||||
});
|
||||
},
|
||||
|
||||
reorderWorkspaces(draggedSpaceId, targetSpaceId) {
|
||||
const tree = this.workspacesTreeVar.get();
|
||||
|
||||
// Helper to remove a space from tree
|
||||
const removeSpace = (nodes, id) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === id) {
|
||||
const removed = nodes.splice(i, 1)[0];
|
||||
return { tree: nodes, removed };
|
||||
}
|
||||
if (nodes[i].children) {
|
||||
const result = removeSpace(nodes[i].children, id);
|
||||
if (result.removed) {
|
||||
return { tree: nodes, removed: result.removed };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { tree: nodes, removed: null };
|
||||
};
|
||||
|
||||
// Helper to insert a space after target
|
||||
const insertAfter = (nodes, targetId, spaceToInsert) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === targetId) {
|
||||
nodes.splice(i + 1, 0, spaceToInsert);
|
||||
return true;
|
||||
}
|
||||
if (nodes[i].children) {
|
||||
if (insertAfter(nodes[i].children, targetId, spaceToInsert)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Clone the tree
|
||||
const newTree = EJSON.clone(tree);
|
||||
|
||||
// Remove the dragged space
|
||||
const { tree: treeAfterRemoval, removed } = removeSpace(
|
||||
newTree,
|
||||
draggedSpaceId,
|
||||
);
|
||||
|
||||
if (removed) {
|
||||
// Insert after target
|
||||
insertAfter(treeAfterRemoval, targetSpaceId, removed);
|
||||
|
||||
// Save the new tree
|
||||
Meteor.call('setWorkspacesTree', treeAfterRemoval, (err) => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
|
||||
// The old sortable code has been removed to prevent conflicts
|
||||
/* OLD SORTABLE CODE - DISABLED
|
||||
const itemsSelector = '.js-board:not(.placeholder)';
|
||||
|
||||
const $boards = this.$('.js-boards');
|
||||
|
|
@ -144,21 +72,28 @@ BlazeComponent.extendComponent({
|
|||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
},
|
||||
async stop(evt, ui) {
|
||||
stop(evt, ui) {
|
||||
// To attribute the new index number, we need to get the DOM element
|
||||
// of the previous and the following card -- if any.
|
||||
const prevBoardDom = ui.item.prev('.js-board').get(0);
|
||||
const nextBoardDom = ui.item.next('.js-board').get(0);
|
||||
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
|
||||
const nextBoardBom = ui.item.next('.js-board').get(0);
|
||||
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardBom, 1);
|
||||
|
||||
const boardDomElement = ui.item.get(0);
|
||||
const board = Blaze.getData(boardDomElement);
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
// (especially when we move the last card of a list, or when multiple
|
||||
// users move some cards at the same time). To prevent these UX glitches
|
||||
// we ask sortable to gracefully cancel the move, and to put back the
|
||||
// DOM in its initial state. The card move is then handled reactively by
|
||||
// Blaze with the below query.
|
||||
$boards.sortable('cancel');
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
|
||||
await currentUser.setBoardSortIndex(board._id, sortIndex.base);
|
||||
}
|
||||
board.move(sortIndex.base);
|
||||
},
|
||||
});
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member or is comment only
|
||||
this.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$boards.sortable({
|
||||
|
|
@ -166,121 +101,56 @@ BlazeComponent.extendComponent({
|
|||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
},
|
||||
userHasTeams() {
|
||||
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) return true;
|
||||
else return false;
|
||||
if (ReactiveCache.getCurrentUser()?.teams?.length > 0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
},
|
||||
teamsDatas() {
|
||||
const teams = ReactiveCache.getCurrentUser()?.teams;
|
||||
const teams = ReactiveCache.getCurrentUser()?.teams
|
||||
if (teams)
|
||||
return teams.sort((a, b) =>
|
||||
a.teamDisplayName.localeCompare(b.teamDisplayName),
|
||||
);
|
||||
else return [];
|
||||
return teams.sort((a, b) => a.teamDisplayName.localeCompare(b.teamDisplayName));
|
||||
else
|
||||
return [];
|
||||
},
|
||||
userHasOrgs() {
|
||||
if (ReactiveCache.getCurrentUser()?.orgs?.length > 0) return true;
|
||||
else return false;
|
||||
if (ReactiveCache.getCurrentUser()?.orgs?.length > 0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
},
|
||||
orgsDatas() {
|
||||
const orgs = ReactiveCache.getCurrentUser()?.orgs;
|
||||
if (orgs)
|
||||
return orgs.sort((a, b) =>
|
||||
a.orgDisplayName.localeCompare(b.orgDisplayName),
|
||||
);
|
||||
else return [];
|
||||
return orgs.sort((a, b) => a.orgDisplayName.localeCompare(b.orgDisplayName));
|
||||
else
|
||||
return [];
|
||||
},
|
||||
userHasOrgsOrTeams() {
|
||||
const ret = this.userHasOrgs() || this.userHasTeams();
|
||||
return ret;
|
||||
},
|
||||
currentMenuPath() {
|
||||
try {
|
||||
const selectedMenuVar = this.selectedMenu;
|
||||
if (!selectedMenuVar || typeof selectedMenuVar.get !== 'function') {
|
||||
return { icon: '🗂️', text: 'Workspaces' };
|
||||
}
|
||||
const sel = selectedMenuVar.get();
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
|
||||
// Helper function to safely get translation or fallback
|
||||
const safeTranslate = (key, fallback) => {
|
||||
try {
|
||||
return TAPi18n.__(key) || fallback;
|
||||
} catch (e) {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to find space by id in tree
|
||||
const findSpaceById = (nodes, targetId, path = []) => {
|
||||
if (!nodes || !Array.isArray(nodes)) return null;
|
||||
for (const node of nodes) {
|
||||
if (node.id === targetId) {
|
||||
return [...path, node];
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const result = findSpaceById(node.children, targetId, [
|
||||
...path,
|
||||
node,
|
||||
]);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (sel === 'starred') {
|
||||
return { icon: '⭐', text: safeTranslate('allboards.starred', 'Starred') };
|
||||
} else if (sel === 'templates') {
|
||||
return { icon: '📋', text: safeTranslate('allboards.templates', 'Templates') };
|
||||
} else if (sel === 'remaining') {
|
||||
return { icon: '📂', text: safeTranslate('allboards.remaining', 'Remaining') };
|
||||
} else {
|
||||
// sel is a workspaceId, build path
|
||||
if (!this.workspacesTreeVar || typeof this.workspacesTreeVar.get !== 'function') {
|
||||
return { icon: '🗂️', text: safeTranslate('allboards.workspaces', 'Workspaces') };
|
||||
}
|
||||
const tree = this.workspacesTreeVar.get();
|
||||
const spacePath = findSpaceById(tree, sel);
|
||||
if (spacePath && spacePath.length > 0) {
|
||||
const pathText = spacePath.map((s) => s.name).join(' / ');
|
||||
return {
|
||||
icon: '🗂️',
|
||||
text: `${safeTranslate('allboards.workspaces', 'Workspaces')} / ${pathText}`,
|
||||
};
|
||||
}
|
||||
return { icon: '🗂️', text: safeTranslate('allboards.workspaces', 'Workspaces') };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in currentMenuPath:', error);
|
||||
return { icon: '🗂️', text: 'Workspaces' };
|
||||
}
|
||||
},
|
||||
boards() {
|
||||
let query = {
|
||||
// { type: 'board' },
|
||||
// { type: { $in: ['board','template-container'] } },
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: { $in: ['board', 'template-container'] } },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } },
|
||||
],
|
||||
{ $or: [] },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } }
|
||||
]
|
||||
};
|
||||
const membershipOrs = [];
|
||||
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne(
|
||||
'tableVisibilityMode-allowPrivateOnly',
|
||||
);
|
||||
let allowPrivateVisibilityOnly = TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly');
|
||||
|
||||
if (FlowRouter.getRouteName() === 'home') {
|
||||
membershipOrs.push({ 'members.userId': Meteor.userId() });
|
||||
query.$and[2].$or.push({ 'members.userId': Meteor.userId() });
|
||||
|
||||
if (
|
||||
allowPrivateVisibilityOnly !== undefined &&
|
||||
allowPrivateVisibilityOnly.booleanValue
|
||||
) {
|
||||
query.$and.push({ permission: 'private' });
|
||||
if (allowPrivateVisibilityOnly !== undefined && allowPrivateVisibilityOnly.booleanValue) {
|
||||
query.$and.push({ 'permission': 'private' });
|
||||
}
|
||||
const currUser = ReactiveCache.getCurrentUser();
|
||||
|
||||
|
|
@ -292,7 +162,7 @@ BlazeComponent.extendComponent({
|
|||
// }
|
||||
|
||||
//query.$and[2].$or.push({'orgs': {$elemMatch : {orgId: orgsIds[0]}}});
|
||||
membershipOrs.push({ 'orgs.orgId': { $in: orgsIds } });
|
||||
query.$and[2].$or.push({ 'orgs.orgId': { $in: orgsIds } });
|
||||
}
|
||||
|
||||
let teamIdsUserBelongs = currUser?.teamIdsUserBelongs() || '';
|
||||
|
|
@ -302,15 +172,10 @@ BlazeComponent.extendComponent({
|
|||
// query.$or[2].$or.push({'teams.teamId': teamsIds[i]});
|
||||
// }
|
||||
//query.$and[2].$or.push({'teams': { $elemMatch : {teamId: teamsIds[0]}}});
|
||||
membershipOrs.push({ 'teams.teamId': { $in: teamsIds } });
|
||||
query.$and[2].$or.push({ 'teams.teamId': { $in: teamsIds } });
|
||||
}
|
||||
if (membershipOrs.length) {
|
||||
query.$and.splice(2, 0, { $or: membershipOrs });
|
||||
}
|
||||
} else if (
|
||||
allowPrivateVisibilityOnly !== undefined &&
|
||||
!allowPrivateVisibilityOnly.booleanValue
|
||||
) {
|
||||
}
|
||||
else if (allowPrivateVisibilityOnly !== undefined && !allowPrivateVisibilityOnly.booleanValue) {
|
||||
query = {
|
||||
archived: false,
|
||||
//type: { $in: ['board','template-container'] },
|
||||
|
|
@ -319,38 +184,10 @@ BlazeComponent.extendComponent({
|
|||
};
|
||||
}
|
||||
|
||||
const boards = ReactiveCache.getBoards(query, {});
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let list = boards;
|
||||
// Apply left menu filtering
|
||||
const sel = this.selectedMenu.get();
|
||||
const assignments =
|
||||
(currentUser &&
|
||||
currentUser.profile &&
|
||||
currentUser.profile.boardWorkspaceAssignments) ||
|
||||
{};
|
||||
if (sel === 'starred') {
|
||||
list = list.filter((b) => currentUser && currentUser.hasStarred(b._id));
|
||||
} else if (sel === 'templates') {
|
||||
list = list.filter((b) => b.type === 'template-container');
|
||||
} else if (sel === 'remaining') {
|
||||
// Show boards not in any workspace AND not templates
|
||||
// Keep starred boards visible in Remaining too
|
||||
list = list.filter(
|
||||
(b) => !assignments[b._id] && b.type !== 'template-container',
|
||||
);
|
||||
} else {
|
||||
// assume sel is a workspaceId
|
||||
// Keep starred boards visible in their workspace too
|
||||
list = list.filter((b) => assignments[b._id] === sel);
|
||||
}
|
||||
|
||||
if (currentUser && typeof currentUser.sortBoardsForUser === 'function') {
|
||||
return currentUser.sortBoardsForUser(list);
|
||||
}
|
||||
return list
|
||||
.slice()
|
||||
.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
const ret = ReactiveCache.getBoards(query, {
|
||||
sort: { sort: 1 /* boards default sorting */ },
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
boardLists(boardId) {
|
||||
/* Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
|
||||
|
|
@ -398,82 +235,11 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-select-menu'(evt) {
|
||||
const type = evt.currentTarget.getAttribute('data-type');
|
||||
this.selectedWorkspaceIdVar.set(null);
|
||||
this.selectedMenu.set(type);
|
||||
},
|
||||
'click .js-select-workspace'(evt) {
|
||||
const id = evt.currentTarget.getAttribute('data-id');
|
||||
this.selectedWorkspaceIdVar.set(id);
|
||||
this.selectedMenu.set(id);
|
||||
},
|
||||
'click .js-add-workspace'(evt) {
|
||||
evt.preventDefault();
|
||||
const name = prompt(
|
||||
TAPi18n.__('allboards.add-workspace-prompt') || 'New Space name',
|
||||
);
|
||||
if (name && name.trim()) {
|
||||
Meteor.call(
|
||||
'createWorkspace',
|
||||
{ parentId: null, name: name.trim() },
|
||||
(err, res) => {
|
||||
if (err) console.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
'click .js-add-board'(evt) {
|
||||
// Store the currently selected workspace/menu for board creation
|
||||
const selectedWorkspaceId = this.selectedWorkspaceIdVar.get();
|
||||
const selectedMenu = this.selectedMenu.get();
|
||||
|
||||
if (selectedWorkspaceId) {
|
||||
Session.set('createBoardInWorkspace', selectedWorkspaceId);
|
||||
} else {
|
||||
Session.set('createBoardInWorkspace', null);
|
||||
}
|
||||
|
||||
// Open different popup based on context
|
||||
if (selectedMenu === 'templates') {
|
||||
Popup.open('createTemplateContainer')(evt);
|
||||
} else {
|
||||
Popup.open('createBoard')(evt);
|
||||
}
|
||||
},
|
||||
'click .js-add-board': Popup.open('createBoard'),
|
||||
'click .js-star-board'(evt) {
|
||||
const boardId = this.currentData()._id;
|
||||
ReactiveCache.getCurrentUser().toggleBoardStar(boardId);
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const boardId = this.currentData()._id;
|
||||
if (boardId) {
|
||||
Meteor.call('toggleBoardStar', boardId);
|
||||
}
|
||||
},
|
||||
// HTML5 DnD from boards to spaces
|
||||
'dragstart .js-board'(evt) {
|
||||
const boardId = this.currentData()._id;
|
||||
|
||||
// Support multi-drag
|
||||
if (
|
||||
BoardMultiSelection.isActive() &&
|
||||
BoardMultiSelection.isSelected(boardId)
|
||||
) {
|
||||
const selectedIds = BoardMultiSelection.getSelectedBoardIds();
|
||||
try {
|
||||
evt.originalEvent.dataTransfer.setData(
|
||||
'text/plain',
|
||||
JSON.stringify(selectedIds),
|
||||
);
|
||||
evt.originalEvent.dataTransfer.setData(
|
||||
'application/x-board-multi',
|
||||
'true',
|
||||
);
|
||||
} catch (e) {}
|
||||
} else {
|
||||
try {
|
||||
evt.originalEvent.dataTransfer.setData('text/plain', boardId);
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
'click .js-clone-board'(evt) {
|
||||
if (confirm(TAPi18n.__('duplicate-board-confirm'))) {
|
||||
|
|
@ -524,115 +290,47 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
});
|
||||
},
|
||||
'click .js-multiselection-activate'(evt) {
|
||||
evt.preventDefault();
|
||||
if (BoardMultiSelection.isActive()) {
|
||||
BoardMultiSelection.disable();
|
||||
} else {
|
||||
BoardMultiSelection.activate();
|
||||
}
|
||||
},
|
||||
'click .js-multiselection-reset'(evt) {
|
||||
evt.preventDefault();
|
||||
BoardMultiSelection.disable();
|
||||
},
|
||||
'click .js-toggle-board-multi-selection'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const boardId = this.currentData()._id;
|
||||
BoardMultiSelection.toogle(boardId);
|
||||
},
|
||||
'click .js-archive-selected-boards'(evt) {
|
||||
evt.preventDefault();
|
||||
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
|
||||
if (
|
||||
selectedBoards.length > 0 &&
|
||||
confirm(TAPi18n.__('archive-board-confirm'))
|
||||
) {
|
||||
selectedBoards.forEach((boardId) => {
|
||||
Meteor.call('archiveBoard', boardId);
|
||||
});
|
||||
BoardMultiSelection.reset();
|
||||
}
|
||||
},
|
||||
'click .js-duplicate-selected-boards'(evt) {
|
||||
evt.preventDefault();
|
||||
const selectedBoards = BoardMultiSelection.getSelectedBoardIds();
|
||||
if (
|
||||
selectedBoards.length > 0 &&
|
||||
confirm(TAPi18n.__('duplicate-board-confirm'))
|
||||
) {
|
||||
selectedBoards.forEach((boardId) => {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (board) {
|
||||
Meteor.call(
|
||||
'copyBoard',
|
||||
boardId,
|
||||
{
|
||||
sort: ReactiveCache.getBoards({ archived: false }).length,
|
||||
type: 'board',
|
||||
title: board.title,
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) console.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
BoardMultiSelection.reset();
|
||||
}
|
||||
},
|
||||
'click #resetBtn'(event) {
|
||||
let allBoards = document.getElementsByClassName('js-board');
|
||||
let allBoards = document.getElementsByClassName("js-board");
|
||||
let currBoard;
|
||||
for (let i = 0; i < allBoards.length; i++) {
|
||||
currBoard = allBoards[i];
|
||||
currBoard.style.display = 'block';
|
||||
currBoard.style.display = "block";
|
||||
}
|
||||
},
|
||||
'click #filterBtn'(event) {
|
||||
event.preventDefault();
|
||||
let selectedTeams = document.querySelectorAll(
|
||||
'#jsAllBoardTeams option:checked',
|
||||
);
|
||||
let selectedTeamsValues = Array.from(selectedTeams).map(
|
||||
function (elt) {
|
||||
return elt.value;
|
||||
},
|
||||
);
|
||||
let index = selectedTeamsValues.indexOf('-1');
|
||||
let selectedTeams = document.querySelectorAll('#jsAllBoardTeams option:checked');
|
||||
let selectedTeamsValues = Array.from(selectedTeams).map(function (elt) { return elt.value });
|
||||
let index = selectedTeamsValues.indexOf("-1");
|
||||
if (index > -1) {
|
||||
selectedTeamsValues.splice(index, 1);
|
||||
}
|
||||
|
||||
let selectedOrgs = document.querySelectorAll(
|
||||
'#jsAllBoardOrgs option:checked',
|
||||
);
|
||||
let selectedOrgsValues = Array.from(selectedOrgs).map(function (elt) {
|
||||
return elt.value;
|
||||
});
|
||||
index = selectedOrgsValues.indexOf('-1');
|
||||
let selectedOrgs = document.querySelectorAll('#jsAllBoardOrgs option:checked');
|
||||
let selectedOrgsValues = Array.from(selectedOrgs).map(function (elt) { return elt.value });
|
||||
index = selectedOrgsValues.indexOf("-1");
|
||||
if (index > -1) {
|
||||
selectedOrgsValues.splice(index, 1);
|
||||
}
|
||||
|
||||
if (selectedTeamsValues.length > 0 || selectedOrgsValues.length > 0) {
|
||||
const query = {
|
||||
$and: [{ archived: false }, { type: 'board' }],
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: 'board' },
|
||||
{ $or: [] }
|
||||
]
|
||||
};
|
||||
const ors = [];
|
||||
if (selectedTeamsValues.length > 0) {
|
||||
ors.push({ 'teams.teamId': { $in: selectedTeamsValues } });
|
||||
query.$and[2].$or.push({ 'teams.teamId': { $in: selectedTeamsValues } });
|
||||
}
|
||||
if (selectedOrgsValues.length > 0) {
|
||||
ors.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
|
||||
}
|
||||
if (ors.length) {
|
||||
query.$and.push({ $or: ors });
|
||||
query.$and[2].$or.push({ 'orgs.orgId': { $in: selectedOrgsValues } });
|
||||
}
|
||||
|
||||
let filteredBoards = ReactiveCache.getBoards(query, {});
|
||||
let allBoards = document.getElementsByClassName('js-board');
|
||||
let allBoards = document.getElementsByClassName("js-board");
|
||||
let currBoard;
|
||||
if (filteredBoards.length > 0) {
|
||||
let currBoardId;
|
||||
|
|
@ -644,312 +342,21 @@ BlazeComponent.extendComponent({
|
|||
return board._id == currBoardId;
|
||||
});
|
||||
|
||||
if (found !== undefined) currBoard.style.display = 'block';
|
||||
else currBoard.style.display = 'none';
|
||||
if (found !== undefined)
|
||||
currBoard.style.display = "block";
|
||||
else
|
||||
currBoard.style.display = "none";
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < allBoards.length; i++) {
|
||||
currBoard = allBoards[i];
|
||||
currBoard.style.display = 'none';
|
||||
currBoard.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-edit-workspace'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const workspaceId = evt.currentTarget.getAttribute('data-id');
|
||||
|
||||
// Find the space in the tree
|
||||
const findSpace = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node;
|
||||
if (node.children) {
|
||||
const found = findSpace(node.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const tree = this.workspacesTreeVar.get();
|
||||
const space = findSpace(tree, workspaceId);
|
||||
|
||||
if (space) {
|
||||
const newName = prompt(
|
||||
TAPi18n.__('allboards.edit-workspace-name') || 'Space name:',
|
||||
space.name,
|
||||
);
|
||||
const newIcon = prompt(
|
||||
TAPi18n.__('allboards.edit-workspace-icon') ||
|
||||
'Space icon (markdown):',
|
||||
space.icon || '📁',
|
||||
);
|
||||
|
||||
if (newName !== null && newName.trim()) {
|
||||
// Update space in tree
|
||||
const updateSpaceInTree = (nodes, id, updates) => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
return { ...node, ...updates };
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: updateSpaceInTree(node.children, id, updates),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const updatedTree = updateSpaceInTree(tree, workspaceId, {
|
||||
name: newName.trim(),
|
||||
icon: newIcon || '📁',
|
||||
});
|
||||
|
||||
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-add-subworkspace'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const parentId = evt.currentTarget.getAttribute('data-id');
|
||||
const name = prompt(
|
||||
TAPi18n.__('allboards.add-subworkspace-prompt') || 'Subspace name:',
|
||||
);
|
||||
|
||||
if (name && name.trim()) {
|
||||
Meteor.call(
|
||||
'createWorkspace',
|
||||
{ parentId, name: name.trim() },
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
'dragstart .workspace-node'(evt) {
|
||||
const workspaceId =
|
||||
evt.currentTarget.getAttribute('data-workspace-id');
|
||||
evt.originalEvent.dataTransfer.effectAllowed = 'move';
|
||||
evt.originalEvent.dataTransfer.setData(
|
||||
'application/x-workspace-id',
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
// Create a better drag image
|
||||
const dragImage = evt.currentTarget.cloneNode(true);
|
||||
dragImage.style.position = 'absolute';
|
||||
dragImage.style.top = '-9999px';
|
||||
dragImage.style.opacity = '0.8';
|
||||
document.body.appendChild(dragImage);
|
||||
evt.originalEvent.dataTransfer.setDragImage(dragImage, 0, 0);
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||
|
||||
evt.currentTarget.classList.add('dragging');
|
||||
},
|
||||
'dragend .workspace-node'(evt) {
|
||||
evt.currentTarget.classList.remove('dragging');
|
||||
document.querySelectorAll('.workspace-node').forEach((el) => {
|
||||
el.classList.remove('drag-over');
|
||||
});
|
||||
},
|
||||
'dragover .workspace-node'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const draggingEl = document.querySelector('.workspace-node.dragging');
|
||||
const targetEl = evt.currentTarget;
|
||||
|
||||
// Allow dropping boards on any space
|
||||
// Or allow dropping spaces on other spaces (but not on itself or descendants)
|
||||
if (
|
||||
!draggingEl ||
|
||||
(targetEl !== draggingEl && !draggingEl.contains(targetEl))
|
||||
) {
|
||||
evt.originalEvent.dataTransfer.dropEffect = 'move';
|
||||
targetEl.classList.add('drag-over');
|
||||
}
|
||||
},
|
||||
'dragleave .workspace-node'(evt) {
|
||||
evt.currentTarget.classList.remove('drag-over');
|
||||
},
|
||||
'drop .workspace-node'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const targetEl = evt.currentTarget;
|
||||
targetEl.classList.remove('drag-over');
|
||||
|
||||
// Check what's being dropped - board or workspace
|
||||
const draggedWorkspaceId = evt.originalEvent.dataTransfer.getData(
|
||||
'application/x-workspace-id',
|
||||
);
|
||||
const isMultiBoard = evt.originalEvent.dataTransfer.getData(
|
||||
'application/x-board-multi',
|
||||
);
|
||||
const boardData =
|
||||
evt.originalEvent.dataTransfer.getData('text/plain');
|
||||
|
||||
if (draggedWorkspaceId && !boardData) {
|
||||
// This is a workspace reorder operation
|
||||
const targetWorkspaceId =
|
||||
targetEl.getAttribute('data-workspace-id');
|
||||
|
||||
if (draggedWorkspaceId !== targetWorkspaceId) {
|
||||
this.reorderWorkspaces(draggedWorkspaceId, targetWorkspaceId);
|
||||
}
|
||||
} else if (boardData) {
|
||||
// This is a board assignment operation
|
||||
// Get the workspace ID directly from the dropped workspace-node's data-workspace-id attribute
|
||||
const workspaceId = targetEl.getAttribute('data-workspace-id');
|
||||
|
||||
if (workspaceId) {
|
||||
if (isMultiBoard) {
|
||||
// Multi-board drag
|
||||
try {
|
||||
const boardIds = JSON.parse(boardData);
|
||||
boardIds.forEach((boardId) => {
|
||||
Meteor.call('assignBoardToWorkspace', boardId, workspaceId);
|
||||
});
|
||||
} catch (e) {
|
||||
// Error parsing multi-board data
|
||||
}
|
||||
} else {
|
||||
// Single board drag
|
||||
Meteor.call('assignBoardToWorkspace', boardData, workspaceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'dragover .js-select-menu'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const menuType = evt.currentTarget.getAttribute('data-type');
|
||||
// Only allow drop on "remaining" menu to unassign boards from spaces
|
||||
if (menuType === 'remaining') {
|
||||
evt.originalEvent.dataTransfer.dropEffect = 'move';
|
||||
evt.currentTarget.classList.add('drag-over');
|
||||
}
|
||||
},
|
||||
'dragleave .js-select-menu'(evt) {
|
||||
evt.currentTarget.classList.remove('drag-over');
|
||||
},
|
||||
'drop .js-select-menu'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const menuType = evt.currentTarget.getAttribute('data-type');
|
||||
evt.currentTarget.classList.remove('drag-over');
|
||||
|
||||
// Only handle drops on "remaining" menu
|
||||
if (menuType !== 'remaining') return;
|
||||
|
||||
const isMultiBoard = evt.originalEvent.dataTransfer.getData(
|
||||
'application/x-board-multi',
|
||||
);
|
||||
const boardData =
|
||||
evt.originalEvent.dataTransfer.getData('text/plain');
|
||||
|
||||
if (boardData) {
|
||||
if (isMultiBoard) {
|
||||
// Multi-board drag - unassign all from workspaces
|
||||
try {
|
||||
const boardIds = JSON.parse(boardData);
|
||||
boardIds.forEach((boardId) => {
|
||||
Meteor.call('unassignBoardFromWorkspace', boardId);
|
||||
});
|
||||
} catch (e) {
|
||||
// Error parsing multi-board data
|
||||
}
|
||||
} else {
|
||||
// Single board drag - unassign from workspace
|
||||
Meteor.call('unassignBoardFromWorkspace', boardData);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
// Helpers for templates
|
||||
workspacesTree() {
|
||||
return this.workspacesTreeVar.get();
|
||||
},
|
||||
selectedWorkspaceId() {
|
||||
return this.selectedWorkspaceIdVar.get();
|
||||
},
|
||||
isSelectedMenu(type) {
|
||||
return this.selectedMenu.get() === type;
|
||||
},
|
||||
isSpaceSelected(id) {
|
||||
return this.selectedWorkspaceIdVar.get() === id;
|
||||
},
|
||||
menuItemCount(type) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const assignments =
|
||||
(currentUser &&
|
||||
currentUser.profile &&
|
||||
currentUser.profile.boardWorkspaceAssignments) ||
|
||||
{};
|
||||
|
||||
// Get all boards for counting
|
||||
let query = {
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: { $in: ['board', 'template-container'] } },
|
||||
{ $or: [{ 'members.userId': Meteor.userId() }] },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } },
|
||||
],
|
||||
};
|
||||
const allBoards = ReactiveCache.getBoards(query, {});
|
||||
|
||||
if (type === 'starred') {
|
||||
return allBoards.filter(
|
||||
(b) => currentUser && currentUser.hasStarred(b._id),
|
||||
).length;
|
||||
} else if (type === 'templates') {
|
||||
return allBoards.filter((b) => b.type === 'template-container').length;
|
||||
} else if (type === 'remaining') {
|
||||
// Count boards not in any workspace AND not templates
|
||||
// Include starred boards (they appear in both Starred and Remaining)
|
||||
return allBoards.filter(
|
||||
(b) => !assignments[b._id] && b.type !== 'template-container',
|
||||
).length;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
workspaceCount(workspaceId) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const assignments =
|
||||
(currentUser &&
|
||||
currentUser.profile &&
|
||||
currentUser.profile.boardWorkspaceAssignments) ||
|
||||
{};
|
||||
|
||||
// Get all boards for counting
|
||||
let query = {
|
||||
$and: [
|
||||
{ archived: false },
|
||||
{ type: { $in: ['board', 'template-container'] } },
|
||||
{ $or: [{ 'members.userId': Meteor.userId() }] },
|
||||
{ title: { $not: { $regex: /^\^.*\^$/ } } },
|
||||
],
|
||||
};
|
||||
const allBoards = ReactiveCache.getBoards(query, {});
|
||||
|
||||
// Count boards directly assigned to this space (not including children)
|
||||
return allBoards.filter((b) => assignments[b._id] === workspaceId).length;
|
||||
},
|
||||
canModifyBoards() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
return currentUser && !currentUser.isCommentOnly();
|
||||
},
|
||||
hasBoardsSelected() {
|
||||
return BoardMultiSelection.count() > 0;
|
||||
},
|
||||
}).register('boardList');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ template(name="miniboard")
|
|||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragBoard'}}")
|
||||
i.fa.fa-arrows
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
|
|
|
|||
|
|
@ -164,32 +164,32 @@
|
|||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-header .btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-filters .btn-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
.entity-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-details {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
|
|
@ -203,60 +203,60 @@
|
|||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-positions-content {
|
||||
background-color: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item {
|
||||
background-color: #2d3748;
|
||||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item:hover {
|
||||
background-color: #4a5568;
|
||||
border-color: #718096;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-header {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-item-header i {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.entity-name {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.entity-id {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-description {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-title {
|
||||
background-color: #4a5568;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-date {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.no-original-positions {
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
|
||||
.no-original-positions i {
|
||||
color: #718096;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<i class="fa fa-history"></i>
|
||||
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
|
||||
</button>
|
||||
|
||||
|
||||
{{#if isShowingOriginalPositions}}
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
|
||||
<i class="fa fa-refresh"></i> Refresh
|
||||
|
|
@ -22,22 +22,22 @@
|
|||
{{else}}
|
||||
<div class="original-positions-filters">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'all'}}">
|
||||
All
|
||||
</button>
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'swimlane'}}">
|
||||
<i class="fa fa-bars"></i> Swimlanes
|
||||
</button>
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'list'}}">
|
||||
<i class="fa fa-columns"></i> Lists
|
||||
</button>
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'card'}}">
|
||||
<i class="fa fa-sticky-note"></i> Cards
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
if (!boardId) return;
|
||||
|
||||
this.isLoading.set(true);
|
||||
|
||||
|
||||
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
if (error) {
|
||||
|
|
@ -57,11 +57,11 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
getFilteredHistory() {
|
||||
const history = this.getBoardHistory();
|
||||
const filterType = this.filterType.get();
|
||||
|
||||
|
||||
if (filterType === 'all') {
|
||||
return history;
|
||||
}
|
||||
|
||||
|
||||
return history.filter(item => item.entityType === filterType);
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
getEntityOriginalPositionDescription(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
let description = `Position: ${position.sort || 0}`;
|
||||
|
||||
|
||||
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
|
||||
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||
} else if (entity.entityType === 'card') {
|
||||
|
|
@ -104,7 +104,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
description += ` in list ${entity.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,12 +55,6 @@
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.attachment-actions a {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.attachment-actions a:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.add-attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -112,9 +106,6 @@
|
|||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 4em;
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
/* Upload progress indicators for drag-and-drop uploads */
|
||||
|
|
@ -250,6 +241,10 @@
|
|||
.js-card-details.is-dragging-over {
|
||||
border: 2px dashed #007bff !important;
|
||||
background: rgba(0, 123, 255, 0.05) !important;
|
||||
}
|
||||
top: 0;
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
.attachment-arrow {
|
||||
font-size: 4em;
|
||||
|
|
@ -258,20 +253,6 @@
|
|||
align-self: center;
|
||||
margin: 0 20px;
|
||||
}
|
||||
#prev-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-left: 70px;
|
||||
}
|
||||
#next-attachment {
|
||||
font-size: 4em;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin-right: 70px;
|
||||
}
|
||||
#viewer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -285,13 +266,6 @@
|
|||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#video-viewer {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
#audio-viewer {
|
||||
max-width: 100%;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 40vw;
|
||||
height: 100%;
|
||||
|
|
@ -326,19 +300,9 @@
|
|||
}
|
||||
#prev-attachment {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-left: 0;
|
||||
}
|
||||
#next-attachment {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
margin-right: 0;
|
||||
}
|
||||
#pdf-viewer {
|
||||
width: 100%;
|
||||
|
|
@ -372,3 +336,36 @@
|
|||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attachment migration styles */
|
||||
.attachment-item.migrating {
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attachment-migration-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.migration-spinner {
|
||||
font-size: 24px;
|
||||
color: #007cba;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,11 +34,10 @@ template(name="attachmentViewer")
|
|||
#viewer-overlay.hidden
|
||||
#viewer-top-bar
|
||||
span#attachment-name
|
||||
a#viewer-close
|
||||
i.fa.fa-times-thin
|
||||
a#viewer-close ❌
|
||||
|
||||
#viewer-container
|
||||
i.fa.fa-caret-left#prev-attachment
|
||||
| ◀️
|
||||
#viewer-content
|
||||
img#image-viewer.hidden
|
||||
video#video-viewer.hidden(controls="true")
|
||||
|
|
@ -46,7 +45,7 @@ template(name="attachmentViewer")
|
|||
object#pdf-viewer.hidden(type="application/pdf")
|
||||
span.pdf-preview-error {{_ 'preview-pdf-not-supported' }}
|
||||
object#txt-viewer.hidden(type="text/plain")
|
||||
i.fa.fa-caret-right#next-attachment
|
||||
| ▶️
|
||||
|
||||
template(name="attachmentGallery")
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ template(name="attachmentGallery")
|
|||
|
||||
if canModifyCard
|
||||
a.attachment-item.add-attachment.js-add-attachment
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
each attachments
|
||||
|
||||
|
|
@ -88,21 +87,22 @@ template(name="attachmentGallery")
|
|||
span.file-size ({{fileSize size}})
|
||||
.attachment-actions
|
||||
a.js-download(href="{{link}}?download=true", download="{{name}}", title="{{_ 'download'}}")
|
||||
i.fa.fa-arrow-down
|
||||
| ⬇️
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.js-rename(title="{{_ 'rename'}}")
|
||||
i.fa.fa-pencil-square-o
|
||||
| ✏️
|
||||
a.js-confirm-delete(title="{{_ 'delete'}}")
|
||||
i.fa.fa-trash
|
||||
| 🗑️
|
||||
a.js-open-attachment-menu(data-attachment-link="{{link}}", title="{{_ 'attachmentActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
| ☰
|
||||
|
||||
// Migration spinner overlay
|
||||
if isAttachmentMigrating _id
|
||||
.attachment-migration-overlay
|
||||
.migration-spinner
|
||||
i.fa.fa-cog.fa-spin
|
||||
| ⚙️
|
||||
.migration-text {{_ 'migrating-attachment'}}
|
||||
|
||||
template(name="attachmentActionsPopup")
|
||||
|
|
@ -110,12 +110,16 @@ template(name="attachmentActionsPopup")
|
|||
li
|
||||
if isImage
|
||||
a(class="{{#if isCover}}js-remove-cover{{else}}js-add-cover{{/if}}")
|
||||
i.fa.fa-picture-o
|
||||
| {{#if isCover}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}
|
||||
| 📖
|
||||
| 🖼️
|
||||
if isCover
|
||||
| {{_ 'remove-cover'}}
|
||||
else
|
||||
| {{_ 'add-cover'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if isImage
|
||||
a(class="{{#if isBackgroundImage}}js-remove-background-image{{else}}js-add-background-image{{/if}}")
|
||||
i.fa.fa-picture-o
|
||||
| 🖼️
|
||||
if isBackgroundImage
|
||||
| {{_ 'remove-background-image'}}
|
||||
else
|
||||
|
|
@ -123,19 +127,19 @@ template(name="attachmentActionsPopup")
|
|||
|
||||
if $neq versions.original.storage "fs"
|
||||
a.js-move-storage-fs
|
||||
i.fa.fa-arrow-right
|
||||
| ▶️
|
||||
| {{_ 'attachment-move-storage-fs'}}
|
||||
|
||||
if $neq versions.original.storage "gridfs"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-gridfs
|
||||
i.fa.fa-arrow-right
|
||||
| ▶️
|
||||
| {{_ 'attachment-move-storage-gridfs'}}
|
||||
|
||||
if $neq versions.original.storage "s3"
|
||||
if versions.original.storage
|
||||
a.js-move-storage-s3
|
||||
i.fa.fa-arrow-right
|
||||
| ▶️
|
||||
| {{_ 'attachment-move-storage-s3'}}
|
||||
|
||||
template(name="attachmentRenamePopup")
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ template(name="cardCustomFieldsPopup")
|
|||
span.full-name
|
||||
= name
|
||||
if hasCustomField
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
hr
|
||||
a.quiet-button.full.js-settings
|
||||
i.fa.fa-cog
|
||||
| ⚙️
|
||||
span {{_ 'settings'}}
|
||||
|
||||
template(name="cardCustomField")
|
||||
|
|
@ -55,11 +55,10 @@ template(name="cardCustomField-number")
|
|||
template(name="cardCustomField-checkbox")
|
||||
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
else
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if data.value}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Cards from '/models/cards';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
|
|
@ -112,7 +112,6 @@ CardCustomField.register('cardCustomField');
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -95,61 +95,37 @@ template(name="minicardCustomFieldDate")
|
|||
| {{showWeek}}
|
||||
|
||||
template(name="editCardReceivedDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
form.edit-card-received-date
|
||||
.datepicker
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="editCardStartDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
form.edit-card-start-date
|
||||
.datepicker
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="editCardDueDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
form.edit-card-due-date
|
||||
.datepicker
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="editCardEndDatePopup")
|
||||
.datepicker-container
|
||||
form.edit-date
|
||||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
if error.get
|
||||
.warning {{_ error.get}}
|
||||
button.primary.wide.left.js-submit-date(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right.js-delete-date {{_ 'delete'}}
|
||||
form.edit-card-end-date
|
||||
.datepicker
|
||||
.clear-date
|
||||
a.js-clear-date {{_ 'clear'}}
|
||||
.datepicker-actions
|
||||
button.primary.wide.left(type="submit") {{_ 'save'}}
|
||||
button.js-delete-date.negate.wide.right {{_ 'delete'}}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar,
|
||||
diff
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
|
@ -47,6 +47,11 @@ import {
|
|||
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
// DatePicker base class handles initialization with native HTML inputs
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setStart(formatDateTime(date));
|
||||
}
|
||||
|
|
@ -63,6 +68,11 @@ import {
|
|||
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
// DatePicker base class handles initialization with native HTML inputs
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setDue(formatDateTime(date));
|
||||
}
|
||||
|
|
@ -79,6 +89,11 @@ import {
|
|||
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
super.onRendered();
|
||||
// DatePicker base class handles initialization with native HTML inputs
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setEnd(formatDateTime(date));
|
||||
}
|
||||
|
|
@ -143,7 +158,7 @@ class CardReceivedDate extends CardDate {
|
|||
const startAt = this.data().getStart();
|
||||
const theDate = this.date.get();
|
||||
const now = this.now.get();
|
||||
|
||||
|
||||
// Received date logic: if received date is after start, due, or end dates, it's overdue
|
||||
if (
|
||||
(startAt && isAfter(theDate, startAt)) ||
|
||||
|
|
@ -187,7 +202,7 @@ class CardStartDate extends CardDate {
|
|||
const endAt = this.data().getEnd();
|
||||
const theDate = this.date.get();
|
||||
const now = this.now.get();
|
||||
|
||||
|
||||
// Start date logic: if start date is after due or end dates, it's overdue
|
||||
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
|
||||
classes += 'overdue';
|
||||
|
|
@ -230,7 +245,7 @@ class CardDueDate extends CardDate {
|
|||
const endAt = this.data().getEnd();
|
||||
const theDate = this.date.get();
|
||||
const now = this.now.get();
|
||||
|
||||
|
||||
// If there's an end date and it's before the due date, task is completed early
|
||||
if (endAt && isBefore(endAt, theDate)) {
|
||||
classes += 'completed-early';
|
||||
|
|
@ -242,7 +257,7 @@ class CardDueDate extends CardDate {
|
|||
// Due date logic based on current time
|
||||
else {
|
||||
const daysDiff = diff(theDate, now, 'days');
|
||||
|
||||
|
||||
if (daysDiff < 0) {
|
||||
// Due date is in the past - overdue
|
||||
classes += 'overdue';
|
||||
|
|
@ -254,7 +269,7 @@ class CardDueDate extends CardDate {
|
|||
classes += 'not-due';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +301,7 @@ class CardEndDate extends CardDate {
|
|||
let classes = 'end-date ';
|
||||
const dueAt = this.data().getDue();
|
||||
const theDate = this.date.get();
|
||||
|
||||
|
||||
if (!dueAt) {
|
||||
// No due date set - just show as completed
|
||||
classes += 'completed';
|
||||
|
|
@ -371,7 +386,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
|||
template() {
|
||||
return 'minicardReceivedDate';
|
||||
}
|
||||
|
||||
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
|
|
@ -383,7 +398,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
|||
template() {
|
||||
return 'minicardStartDate';
|
||||
}
|
||||
|
||||
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
|
|
@ -395,7 +410,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
|||
template() {
|
||||
return 'minicardDueDate';
|
||||
}
|
||||
|
||||
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
|
|
@ -407,7 +422,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
|||
template() {
|
||||
return 'minicardEndDate';
|
||||
}
|
||||
|
||||
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
|
|
@ -419,7 +434,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
|||
template() {
|
||||
return 'minicardCustomFieldDate';
|
||||
}
|
||||
|
||||
|
||||
showDate() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
async 'submit .js-card-description'(event) {
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
await this.data().setDescription(description);
|
||||
this.data().setDescription(description);
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
/* Date Format Selector */
|
||||
.card-details-item-date-format {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .card-details-item-title {
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.card-details-item-date-format .js-date-format-selector:focus {
|
||||
|
|
@ -29,18 +27,18 @@
|
|||
}
|
||||
|
||||
.assignee {
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
position: relative;
|
||||
float: left;
|
||||
height: clamp(24px, 3.5vw, 36px);
|
||||
width: clamp(24px, 3.5vw, 36px);
|
||||
margin: 0.3vh;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin: .3vh;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
text-decoration: none;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.04);
|
||||
}
|
||||
.assignee .avatar {
|
||||
overflow: hidden;
|
||||
|
|
@ -53,18 +51,12 @@
|
|||
background-color: #dbdbdb;
|
||||
color: #444;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
.assignee .avatar.avatar-image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.assignee .assignee-presence-status {
|
||||
background-color: #b3b3b3;
|
||||
|
|
@ -75,6 +67,7 @@
|
|||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
border: 1px solid #fff;
|
||||
z-index: 15;
|
||||
}
|
||||
.assignee .assignee-presence-status.active {
|
||||
|
|
@ -98,7 +91,6 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 2px #bfbfbf inset;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.assignee.add-assignee:hover,
|
||||
.assignee.add-assignee.is-active {
|
||||
|
|
@ -110,83 +102,22 @@
|
|||
background-color: rgba(0,0,0,0.875);
|
||||
color: #fff;
|
||||
border-radius: 0.7vw;
|
||||
font-size: 0.98em;
|
||||
}
|
||||
|
||||
.card-details {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: min(600px, 80vw);
|
||||
will-change: flex-basis;
|
||||
overflow-y: auto;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
background: #f7f7f7;
|
||||
border-radius: 0 0 0.4vw 0.4vw;
|
||||
border-radius: bottom 0.4vw;
|
||||
z-index: 30;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 0.9vh 0 #b3b3b3;
|
||||
transition: flex-basis 0.1s, box-shadow 0.15s;
|
||||
transition: flex-basis 0.1s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Desktop mode: position card below board header */
|
||||
body.desktop-mode .card-details:not(.card-details-popup) {
|
||||
position: fixed;
|
||||
width: auto;
|
||||
max-width: 800px;
|
||||
flex-basis: auto;
|
||||
border-radius: 8px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Default position for first card or when dragged */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Stagger positions for multiple cards using nth-of-type */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
|
||||
top: 80px;
|
||||
left: 50px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
|
||||
top: 110px;
|
||||
left: 80px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
|
||||
top: 140px;
|
||||
left: 110px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
|
||||
top: 170px;
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
/* For expanded cards, set dimensions */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Collapsed card state - hide content and set height to title row only */
|
||||
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
|
||||
display: none !important;
|
||||
}
|
||||
.card-details.card-details-collapsed {
|
||||
height: auto !important;
|
||||
bottom: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
body.desktop-mode .card-details.card-details-collapsed {
|
||||
bottom: auto !important;
|
||||
}
|
||||
.card-details .mCustomScrollBox {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
|
@ -196,47 +127,18 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
}
|
||||
.card-details .card-details-header {
|
||||
margin: 0 -20px 5px;
|
||||
padding: 8px 20px;
|
||||
padding: 7px 20px;
|
||||
background: #ededed;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 500;
|
||||
display: flow-root;
|
||||
min-height: 44px;
|
||||
}
|
||||
.card-details .card-details-header .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Collapse toggle triangle */
|
||||
.card-details .card-details-header .card-collapse-toggle {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
padding: 7px 10px;
|
||||
margin-left: -10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #000;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .card-drag-handle {
|
||||
font-size: 20px;
|
||||
padding: 8px 10px;
|
||||
margin-right: 10px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .close-card-details,
|
||||
.card-details .card-details-header .maximize-card-details,
|
||||
.card-details .card-details-header .minimize-card-details,
|
||||
|
|
@ -254,19 +156,11 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: -8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
transition: color 0.13s;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web,
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
font-size: 24px;
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-right: 40px;
|
||||
}
|
||||
.card-details .card-details-header .card-copy-button {
|
||||
font-size: 17px;
|
||||
|
|
@ -281,44 +175,12 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
.card-details .card-details-header .card-details-menu {
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu-mobile-web {
|
||||
font-size: 17px;
|
||||
padding: 10px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
.card-details .card-details-header .card-zoom-in,
|
||||
.card-details .card-details-header .card-zoom-out {
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Unify all card text to match title size */
|
||||
.card-details {
|
||||
font-size: 1em;
|
||||
}
|
||||
.card-details p,
|
||||
.card-details span,
|
||||
.card-details div,
|
||||
.card-details a,
|
||||
.card-details label,
|
||||
.card-details input,
|
||||
.card-details textarea,
|
||||
.card-details select,
|
||||
.card-details button,
|
||||
.card-details .card-details-item-title,
|
||||
.card-details .card-label,
|
||||
.card-details .viewer {
|
||||
font-size: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card-details .card-details-header .card-details-watch {
|
||||
font-size: 17px;
|
||||
padding-left: 7px;
|
||||
|
|
@ -326,13 +188,9 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
}
|
||||
.card-details .card-details-header .card-details-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.35em;
|
||||
font-size: 1.33em;
|
||||
margin: 7px 0 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.card-details .card-details-header .linked-card-location {
|
||||
font-style: italic;
|
||||
|
|
@ -347,10 +205,10 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
.card-details .card-details-header form.inlined-form .copied-tooltip {
|
||||
padding: 0 10px;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list {
|
||||
font-size: 0.9em;
|
||||
font-size: 0.85em;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.card-details .card-details-header .card-details-list a.card-details-list-title {
|
||||
|
|
@ -360,7 +218,7 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
display: inline-block;
|
||||
background: #e6e6e6;
|
||||
border-radius: 3px;
|
||||
padding: 0 5px;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
.card-details .card-details-header .copied-tooltip {
|
||||
margin-right: 10px;
|
||||
|
|
@ -371,13 +229,11 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
}
|
||||
.card-details .card-description textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
.card-details .card-details-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 15px 0;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.card-details .card-details-items .card-details-item {
|
||||
margin-right: 0.5em;
|
||||
|
|
@ -428,28 +284,15 @@ body.desktop-mode .card-details.card-details-collapsed {
|
|||
position: fixed;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
/* Override for mobile mode even on larger screens */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
.card-details-maximized {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: calc(100% - 20px);
|
||||
will-change: flex-basis;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
background: #f7f7f7;
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-radius: bottom 3px;
|
||||
z-index: 100;
|
||||
animation: flexGrowIn 0.1s;
|
||||
box-shadow: 0 0 7px 0 #b3b3b3;
|
||||
|
|
@ -492,52 +335,19 @@ input[type="submit"].attachment-add-link-submit {
|
|||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.card-details {
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: calc(100% - 1px);
|
||||
padding: 0px 20px 0px 20px;
|
||||
margin: 0px;
|
||||
transition: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure card details are above everything on mobile */
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
overflow-y: revert;
|
||||
overflow-x: revert;
|
||||
}
|
||||
.card-details .card-details-canvas {
|
||||
width: 100%;
|
||||
padding-left: 0px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details {
|
||||
margin-right: 0px;
|
||||
display: block !important;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
margin-right: 40px;
|
||||
|
|
@ -563,62 +373,6 @@ input[type="submit"].attachment-add-link-submit {
|
|||
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
|
||||
margin: 0;
|
||||
}
|
||||
/* iPhone mobile: enlarge header buttons and increase spacing */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header {
|
||||
padding-right: 16px;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
|
||||
font-size: 2em !important; /* 2x bigger */
|
||||
padding: 0.3em !important;
|
||||
margin-right: 0.75em !important; /* 2x space compared to default */
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
/* Avoid clipping of the close button on the right edge */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
|
||||
margin-right: 0.75em !important;
|
||||
}
|
||||
/* Enlarge the header title too */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-canvas {
|
||||
width: 100% !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
}
|
||||
.card-details-white {
|
||||
background: #fff !important;
|
||||
|
|
@ -727,15 +481,13 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
|||
.vote-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.vote-title .js-edit-date {
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.vote-result {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.js-show-positive-votes {
|
||||
cursor: pointer;
|
||||
|
|
@ -746,33 +498,29 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
|||
.poker-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.poker-title .js-edit-date {
|
||||
align-self: flex-start;
|
||||
margin-left: 6px;
|
||||
align-self: baseline;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.poker-result {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.js-show-positive-poker-votes {
|
||||
cursor: pointer;
|
||||
}
|
||||
.poker-deck {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.poker-card-result {
|
||||
width: 34px;
|
||||
width: 32px;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
padding: 4px 2px;
|
||||
padding: 4px 2px 4px 2px;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.winner {
|
||||
font-weight: bold;
|
||||
|
|
@ -783,7 +531,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
|||
}
|
||||
.responsive-table {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.poker-table {
|
||||
display: table;
|
||||
|
|
@ -846,15 +593,11 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
|||
margin: auto;
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
border-radius: 2px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
.estimation-add button {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin: auto;
|
||||
border-radius: 2px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
.poker-card {
|
||||
width: 48px;
|
||||
|
|
@ -873,7 +616,6 @@ body.mobile-mode .card-details .card-details-header .close-card-details-mobile-w
|
|||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.12s;
|
||||
}
|
||||
.poker-card .inner {
|
||||
display: table-cell;
|
||||
|
|
|
|||
|
|
@ -5,65 +5,48 @@ template(name="cardDetails")
|
|||
|
||||
+attachmentViewer
|
||||
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
i.fa.fa-times-thin
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| ❌
|
||||
if canModifyCard
|
||||
if cardMaximized
|
||||
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
| 🔽
|
||||
else
|
||||
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| 🔼
|
||||
if canModifyCard
|
||||
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
| ☰
|
||||
a.card-copy-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
if canModifyCard
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
i.fa.fa-arrows
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
else
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
i.fa.fa-times-thin
|
||||
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
|
||||
i.fa.fa-search-minus
|
||||
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
|
||||
i.fa.fa-search-plus
|
||||
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
|
||||
if mobileMode
|
||||
i.fa.fa-desktop
|
||||
else
|
||||
i.fa.fa-mobile
|
||||
if cardMaximized
|
||||
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
else
|
||||
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
unless isPopup
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
if canModifyCard
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
| ☰
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
id="cardURL_copy"
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
h2.card-details-title.js-card-title(
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
|
||||
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
span.card-number
|
||||
|
|
@ -71,7 +54,7 @@ template(name="cardDetails")
|
|||
= getTitle
|
||||
if isWatching
|
||||
i.card-details-watch
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
.card-details-path
|
||||
each parentList
|
||||
| >
|
||||
|
|
@ -93,7 +76,7 @@ template(name="cardDetails")
|
|||
if hasActiveUploads
|
||||
.card-details-upload-progress
|
||||
.upload-progress-header
|
||||
i.fa.fa-upload
|
||||
| 📤
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
|
|
@ -102,11 +85,11 @@ template(name="cardDetails")
|
|||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
i.fa.fa-exclamation-triangle
|
||||
| ⚠️
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.card-details-left
|
||||
|
|
@ -115,7 +98,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsLabels
|
||||
.card-details-item.card-details-item-labels
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-tags
|
||||
| 🏷️
|
||||
| {{_ 'labels'}}
|
||||
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
|
||||
each labels
|
||||
|
|
@ -125,14 +108,14 @@ template(name="cardDetails")
|
|||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
hr
|
||||
|
||||
.card-details-item.card-details-item-date-format
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-calendar
|
||||
| 📅
|
||||
| {{_ 'date-format'}}
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
|
|
@ -143,7 +126,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-out
|
||||
| 📥
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate
|
||||
|
|
@ -151,12 +134,12 @@ template(name="cardDetails")
|
|||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-received-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-start
|
||||
| 🚀
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate
|
||||
|
|
@ -164,12 +147,12 @@ template(name="cardDetails")
|
|||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-start-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| ⏰
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate
|
||||
|
|
@ -177,12 +160,12 @@ template(name="cardDetails")
|
|||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-due-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-end
|
||||
| 🏁
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate
|
||||
|
|
@ -190,7 +173,7 @@ template(name="cardDetails")
|
|||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-end-date
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
if currentBoard.hasAnyAllowsUser
|
||||
hr
|
||||
|
|
@ -198,7 +181,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsCreator
|
||||
.card-details-item.card-details-item-creator
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| 👤
|
||||
| {{_ 'creator'}}
|
||||
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
|
|
@ -208,7 +191,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsMembers
|
||||
.card-details-item.card-details-item-members
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-users
|
||||
| 👤s
|
||||
| {{_ 'members'}}
|
||||
each userId in getMembers
|
||||
+userAvatar(userId=userId cardId=_id)
|
||||
|
|
@ -216,30 +199,30 @@ template(name="cardDetails")
|
|||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
//if assigneeSelected
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| 👤
|
||||
| {{_ 'assignee'}}
|
||||
each userId in getAssignees
|
||||
+userAvatar(userId=userId cardId=_id assignee=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
if currentUser.isWorker
|
||||
unless assigneeSelected
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
//.card-details-items
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-shopping-cart
|
||||
| 🛒
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
|
|
@ -259,7 +242,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user-plus
|
||||
| 👤-plus
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
|
|
@ -282,7 +265,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sort-numeric-asc
|
||||
| 🔢
|
||||
| {{_ 'sort'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
|
|
@ -295,7 +278,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsShowLists
|
||||
.card-details-item.card-details-show-lists
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list
|
||||
| 📋
|
||||
| {{_ 'list'}}
|
||||
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
|
||||
each currentBoard.lists
|
||||
|
|
@ -321,7 +304,7 @@ template(name="cardDetails")
|
|||
hr
|
||||
.card-details-item.card-details-item-customfield
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list
|
||||
| 📋-alt
|
||||
= definition.name
|
||||
+cardCustomField
|
||||
|
||||
|
|
@ -339,7 +322,7 @@ template(name="cardDetails")
|
|||
.vote-title
|
||||
div.flex
|
||||
h3
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'vote-question'}}
|
||||
if getVoteEnd
|
||||
+voteEndDate
|
||||
|
|
@ -351,14 +334,13 @@ template(name="cardDetails")
|
|||
.card-label.card-label-green {{ voteCountPositive }}
|
||||
.card-label.card-label-red {{ voteCountNegative }}
|
||||
unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
|
||||
.card-label.card-label-gray
|
||||
| {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
+viewer
|
||||
= getVoteQuestion
|
||||
if showVotingButtons
|
||||
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}")
|
||||
if voteState
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'vote-for-it'}}
|
||||
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}")
|
||||
if $eq voteState false
|
||||
|
|
@ -370,7 +352,7 @@ template(name="cardDetails")
|
|||
.poker-title
|
||||
div.flex
|
||||
h3
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'poker-question'}}
|
||||
if getPokerEnd
|
||||
+pokerEndDate
|
||||
|
|
@ -378,60 +360,59 @@ template(name="cardDetails")
|
|||
.poker-result
|
||||
if expiredPoker
|
||||
unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
|
||||
.card-label.card-label-gray
|
||||
| {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
.card-label.card-label-gray {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
|
||||
if showPlanningPokerButtons
|
||||
.poker-result
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-one(class="{{#if $eq pokerState 'one'}}poker-voted{{/if}}") {{_ 'poker-one'}}
|
||||
if $eq pokerState "one"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-two(class="{{#if $eq pokerState 'two'}}poker-voted{{/if}}") {{_ 'poker-two'}}
|
||||
if $eq pokerState "two"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-three(class="{{#if $eq pokerState 'three'}}poker-voted{{/if}}") {{_ 'poker-three'}}
|
||||
if $eq pokerState "three"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-five(class="{{#if $eq pokerState 'five'}}poker-voted{{/if}}") {{_ 'poker-five'}}
|
||||
if $eq pokerState "five"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-eight(class="{{#if $eq pokerState 'eight'}}poker-voted{{/if}}") {{_ 'poker-eight'}}
|
||||
if $eq pokerState "eight"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-thirteen(class="{{#if $eq pokerState 'thirteen'}}poker-voted{{/if}}") {{_ 'poker-thirteen'}}
|
||||
if $eq pokerState "thirteen"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-twenty(class="{{#if $eq pokerState 'twenty'}}poker-voted{{/if}}") {{_ 'poker-twenty'}}
|
||||
if $eq pokerState "twenty"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-forty(class="{{#if $eq pokerState 'forty'}}poker-voted{{/if}}") {{_ 'poker-forty'}}
|
||||
if $eq pokerState "forty"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-one-hundred(class="{{#if $eq pokerState 'oneHundred'}}poker-voted{{/if}}") {{_ 'poker-oneHundred'}}
|
||||
if $eq pokerState "oneHundred"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
.poker-deck
|
||||
.poker-card
|
||||
span.inner.js-poker.js-poker-vote-unsure(class="{{#if $eq pokerState 'unsure'}}poker-voted{{/if}}") {{_ 'poker-unsure'}}
|
||||
if $eq pokerState "unsure"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
button.card-details-blue.js-poker-finish(class="{{#if $eq voteState false}}poker-voted{{/if}}") {{_ 'poker-finish'}}
|
||||
|
|
@ -561,7 +542,7 @@ template(name="cardDetails")
|
|||
button.card-details-red.js-poker-replay(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'poker-replay'}}
|
||||
div.estimation-add
|
||||
button.js-poker-estimation
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
| {{_ 'set-estimation'}}
|
||||
input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
|
||||
|
||||
|
|
@ -571,7 +552,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-file-text-o
|
||||
| 📝
|
||||
| {{_ 'description'}}
|
||||
if currentBoard.allowsDescriptionText
|
||||
+inlinedCardDescription(classNames="card-description js-card-description")
|
||||
|
|
@ -582,7 +563,7 @@ template(name="cardDetails")
|
|||
else
|
||||
if currentBoard.allowsDescriptionText
|
||||
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
| ✏️
|
||||
a.js-open-inlined-form(title="{{_ 'edit'}}" value=title)
|
||||
if getDescription
|
||||
+viewer
|
||||
|
|
@ -612,7 +593,7 @@ template(name="cardDetails")
|
|||
if currentBoard.allowsAttachments
|
||||
hr
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-paperclip
|
||||
| 📎
|
||||
| {{_ 'attachments'}}
|
||||
if Meteor.settings.public.attachmentsUploadMaxSize
|
||||
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
|
||||
|
|
@ -628,24 +609,22 @@ template(name="cardDetails")
|
|||
unless currentUser.isNoComments
|
||||
.comment-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-comment-o
|
||||
| 💬
|
||||
| {{_ 'comments'}}
|
||||
|
||||
if currentBoard.allowsComments
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isNoComments
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
+commentForm
|
||||
+commentForm
|
||||
+comments
|
||||
hr
|
||||
|
||||
.card-details-right
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
unless currentUser.isNoComments
|
||||
.activity-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-history
|
||||
| 📜
|
||||
| {{ _ 'activities'}}
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'show-activities'}}")
|
||||
|
|
@ -655,7 +634,7 @@ template(name="cardDetails")
|
|||
input.toggle-switch(type="checkbox" id="toggleShowActivitiesCard")
|
||||
label.toggle-label(for="toggleShowActivitiesCard")
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
unless currentUser.isNoComments
|
||||
if isLoaded.get
|
||||
if isLinkedCard
|
||||
+activities(card=this mode="linkedcard")
|
||||
|
|
@ -696,10 +675,10 @@ template(name="cardDetailsActionsPopup")
|
|||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
i.fa.fa-eye
|
||||
| 👁️-slash
|
||||
| {{_ 'watch'}}
|
||||
hr
|
||||
if canModifyCard
|
||||
|
|
@ -710,16 +689,16 @@ template(name="cardDetailsActionsPopup")
|
|||
//li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
li
|
||||
a.js-start-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'card-edit-voting'}}
|
||||
li
|
||||
a.js-start-planning-poker
|
||||
i.fa.fa-thumbs-up
|
||||
| 👍
|
||||
| {{_ 'card-edit-planning-poker'}}
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list
|
||||
| 📋-alt
|
||||
| {{_ 'card-edit-custom-fields'}}
|
||||
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
|
||||
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
|
||||
|
|
@ -727,260 +706,114 @@ template(name="cardDetailsActionsPopup")
|
|||
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
|
||||
li
|
||||
a.js-spent-time
|
||||
i.fa.fa-clock-o
|
||||
| 🕐
|
||||
| {{_ 'editCardSpentTimePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
i.fa.fa-paint-brush
|
||||
| 🎨
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-toggle-show-list-on-minicard
|
||||
if showListOnMinicard
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
| {{_ 'hide-list-on-minicard'}}
|
||||
else
|
||||
i.fa.fa-eye
|
||||
| 👁️-slash
|
||||
| {{_ 'show-list-on-minicard'}}
|
||||
if canModifyCard
|
||||
hr
|
||||
else
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
hr
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-export-card
|
||||
i.fa.fa-upload
|
||||
| 📤
|
||||
| {{_ 'export-card'}}
|
||||
unless canModifyCard
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
if canModifyCard
|
||||
hr
|
||||
ul.pop-over-list
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
| ⬆️
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
| ⬇️
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
if currentUser.isBoardAdmin
|
||||
li
|
||||
a.js-move-card
|
||||
i.fa.fa-arrow-right
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
li
|
||||
a.js-copy-card
|
||||
i.fa.fa-clipboard
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
a.js-move-card
|
||||
| ➡️
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
i.fa.fa-copy
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-archive
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-card'}}
|
||||
li
|
||||
a.js-copy-card
|
||||
| 📋
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-copy-checklist-cards
|
||||
| 📋
|
||||
| 📋
|
||||
| {{_ 'copyManyCardsPopup-title'}}
|
||||
unless archived
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
a.js-archive
|
||||
| ➡️
|
||||
| 📦
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
| 🔗
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
|
||||
template(name="exportCardPopup")
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrlCardPDF}}",, download="{{exportFilenameCardPDF}}")
|
||||
i.fa.fa-upload
|
||||
| 📤
|
||||
| {{_ 'export-card-pdf'}}
|
||||
|
||||
template(name="moveCardPopup")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyCardPopup")
|
||||
label(for='copy-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= getTitle
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyManyCardsPopup")
|
||||
label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
| {{_ 'copyManyCardsPopup-format'}}
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="convertChecklistItemToCardPopup")
|
||||
label(for='convert-checklist-item-to-card-title') {{_ 'title'}}:
|
||||
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
|
||||
= item.title
|
||||
+copyAndMoveCard
|
||||
|
||||
template(name="copyAndMoveCard")
|
||||
unless currentUser.isWorker
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards(autofocus)
|
||||
each boards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
each swimlanes
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
each lists
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
each cards
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
|
||||
|
||||
div
|
||||
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
|
||||
label(for="position-above") {{_ 'above-selected-card'}}
|
||||
div
|
||||
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
|
||||
label(for="position-below") {{_ 'below-selected-card'}}
|
||||
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-done {{_ 'done'}}
|
||||
|
|
@ -991,13 +824,13 @@ template(name="cardMembersPopup")
|
|||
each members
|
||||
li.item(class="{{#if isCardMember}}active{{/if}}")
|
||||
a.name.js-select-member(href="#")
|
||||
+userAvatar(userId=userId)
|
||||
+userAvatar(userId=user._id)
|
||||
span.full-name
|
||||
= userData.profile.fullname
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
= user.profile.fullname
|
||||
| (<span class="username">{{ user.username }}</span>)
|
||||
if isCardMember
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
||||
template(name="cardAssigneesPopup")
|
||||
input.card-assignees-filter(type="text" placeholder="{{_ 'search'}}")
|
||||
unless currentUser.isWorker
|
||||
|
|
@ -1005,13 +838,12 @@ template(name="cardAssigneesPopup")
|
|||
each members
|
||||
li.item(class="{{#if isCardAssignee}}active{{/if}}")
|
||||
a.name.js-select-assignee(href="#")
|
||||
+userAvatar(userId=userId)
|
||||
+userAvatar(userId=user._id)
|
||||
span.full-name
|
||||
= userData.profile.fullname
|
||||
if userData.username
|
||||
| (#{userData.username})
|
||||
= user.profile.fullname
|
||||
| (<span class="username">{{ user.username }}</span>)
|
||||
if isCardAssignee
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
if currentUser.isWorker
|
||||
ul.pop-over-list.js-card-assignee-list
|
||||
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
|
||||
|
|
@ -1019,10 +851,10 @@ template(name="cardAssigneesPopup")
|
|||
+userAvatar(userId=currentUser._id)
|
||||
span.full-name
|
||||
= currentUser.profile.fullname
|
||||
if currentUser.username
|
||||
| (#{currentUser.username})
|
||||
| (<span class="username">{{ currentUser.username }}</span>)
|
||||
if currentUser.isCardAssignee
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
||||
template(name="cardAssigneePopup")
|
||||
.board-assignee-menu
|
||||
.mini-profile-info
|
||||
|
|
@ -1045,7 +877,7 @@ template(name="cardMorePopup")
|
|||
span.clearfix
|
||||
span {{_ 'link-card'}}
|
||||
= ' '
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
|
||||
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
|
||||
.copied-tooltip {{_ 'copied'}}
|
||||
|
|
@ -1083,14 +915,13 @@ template(name="cardMorePopup")
|
|||
|
||||
template(name="setCardColorPopup")
|
||||
form.edit-label
|
||||
.palette-colors
|
||||
each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
.palette-colors: each colors
|
||||
unless $eq color 'white'
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
| ✅
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="cardDeletePopup")
|
||||
p {{_ "card-delete-pop"}}
|
||||
|
|
@ -1122,12 +953,12 @@ template(name="cardStartVotingPopup")
|
|||
.materialCheckBox#vote-public(name="vote-public" class="{{#if votePublic}}is-checked{{/if}}")
|
||||
span {{_ 'vote-public'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-clock-o
|
||||
| ⏰
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
unless getVoteEnd
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
if getVoteEnd
|
||||
+voteEndDate
|
||||
|
||||
|
|
@ -1168,12 +999,12 @@ template(name="cardStartPlanningPokerPopup")
|
|||
.materialCheckBox#poker-allow-non-members(name="poker-allow-non-members" class="{{#if pokerAllowNonBoardMembers}}is-checked{{/if}}")
|
||||
span {{_ 'allowNonBoardMembers'}}
|
||||
.check-div.flex
|
||||
i.fa.fa-clock-o
|
||||
| ⏰
|
||||
a.js-end-date
|
||||
span
|
||||
| {{_ 'card-end'}}
|
||||
unless getPokerEnd
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
if getPokerEnd
|
||||
+pokerEndDate
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Cards from '/models/cards';
|
||||
import Boards from '/models/boards';
|
||||
|
|
@ -32,7 +31,6 @@ import CardComments from '/models/cardComments';
|
|||
import { ALLOWED_COLORS } from '/config/const';
|
||||
import { UserAvatar } from '../users/userAvatar';
|
||||
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
||||
import { handleFileUpload } from './attachments';
|
||||
import uploadProgressManager from '../../lib/uploadProgressManager';
|
||||
|
||||
|
|
@ -65,11 +63,7 @@ BlazeComponent.extendComponent({
|
|||
const boardBody = this.parentComponent().parentComponent();
|
||||
//in Miniview parent is Board, not BoardBody.
|
||||
if (boardBody !== null) {
|
||||
// Only show overlay in mobile mode, not in desktop mode
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (isMobile) {
|
||||
boardBody.showOverlay.set(true);
|
||||
}
|
||||
boardBody.showOverlay.set(true);
|
||||
boardBody.mouseHasEnterCardDetails = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +81,6 @@ BlazeComponent.extendComponent({
|
|||
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
if (!card || typeof card.findWatcher !== 'function') return false;
|
||||
return card.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
|
|
@ -100,23 +93,6 @@ BlazeComponent.extendComponent({
|
|||
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
|
||||
},
|
||||
|
||||
showActivities() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
return user && user.hasShowActivities();
|
||||
},
|
||||
|
||||
cardCollapsed() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile) {
|
||||
return !!user.profile.cardCollapsed;
|
||||
}
|
||||
if (Users.getPublicCardCollapsed) {
|
||||
const stored = Users.getPublicCardCollapsed();
|
||||
if (typeof stored === 'boolean') return stored;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if (result === null || result === undefined) {
|
||||
|
|
@ -169,9 +145,8 @@ BlazeComponent.extendComponent({
|
|||
* @return is the list id the current list id ?
|
||||
*/
|
||||
isCurrentListId(listId) {
|
||||
const data = this.data();
|
||||
if (!data || typeof data.listId === 'undefined') return false;
|
||||
return data.listId == listId;
|
||||
const ret = this.data().listId == listId;
|
||||
return ret;
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
|
|
@ -321,110 +296,12 @@ BlazeComponent.extendComponent({
|
|||
return [
|
||||
{
|
||||
...events,
|
||||
'click .js-card-collapse-toggle'() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
|
||||
if (user) {
|
||||
Meteor.call('setCardCollapsed', !currentState);
|
||||
} else if (Users.setPublicCardCollapsed) {
|
||||
Users.setPublicCardCollapsed(!currentState);
|
||||
}
|
||||
},
|
||||
'mousedown .js-card-drag-handle'(event) {
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startLeft = $card.offset().left;
|
||||
const startTop = $card.offset().top;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
$card.css({
|
||||
left: startLeft + deltaX + 'px',
|
||||
top: startTop + deltaY + 'px'
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
$(document).off('mousemove', onMouseMove);
|
||||
$(document).off('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
$(document).on('mousemove', onMouseMove);
|
||||
$(document).on('mouseup', onMouseUp);
|
||||
},
|
||||
'mousedown .js-card-title-drag-handle'(event) {
|
||||
// Allow dragging from title for ReadOnly users
|
||||
// Don't interfere with text selection
|
||||
if (event.target.tagName === 'A' || $(event.target).closest('a').length > 0) {
|
||||
return; // Don't drag if clicking on links
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startLeft = $card.offset().left;
|
||||
const startTop = $card.offset().top;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
$card.css({
|
||||
left: startLeft + deltaX + 'px',
|
||||
top: startTop + deltaY + 'px'
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
$(document).off('mousemove', onMouseMove);
|
||||
$(document).off('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
$(document).on('mousemove', onMouseMove);
|
||||
$(document).on('mouseup', onMouseUp);
|
||||
},
|
||||
'click .js-close-card-details'() {
|
||||
// Get board ID from either the card data or current board in session
|
||||
const card = this.currentData() || this.data();
|
||||
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
|
||||
const cardId = card && card._id;
|
||||
|
||||
if (boardId) {
|
||||
// In desktop mode, remove from openCards array
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile && cardId) {
|
||||
const openCards = Session.get('openCards') || [];
|
||||
const filtered = openCards.filter(id => id !== cardId);
|
||||
Session.set('openCards', filtered);
|
||||
|
||||
// If this was the current card, clear it
|
||||
if (Session.get('currentCard') === cardId) {
|
||||
Session.set('currentCard', null);
|
||||
}
|
||||
// Don't navigate away in desktop mode - just close the card
|
||||
return;
|
||||
}
|
||||
|
||||
// Mobile mode: Clear the current card session to close the card
|
||||
Session.set('currentCard', null);
|
||||
|
||||
// Navigate back to board without card
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (board) {
|
||||
FlowRouter.go('board', {
|
||||
id: board._id,
|
||||
slug: board.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
Utils.goBoardId(this.data().boardId);
|
||||
},
|
||||
'click .js-copy-link'(event) {
|
||||
event.preventDefault();
|
||||
const url = this.data().absoluteUrl();
|
||||
const promise = Utils.copyTextToClipboard(url);
|
||||
const promise = Utils.copyTextToClipboard(event.target.href);
|
||||
|
||||
const $tooltip = this.$('.card-details-header .copied-tooltip');
|
||||
Utils.showCopied(promise, $tooltip);
|
||||
|
|
@ -434,46 +311,18 @@ BlazeComponent.extendComponent({
|
|||
Meteor.call('changeDateFormat', dateFormat);
|
||||
},
|
||||
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
|
||||
// Mobile: switch to desktop popup view (maximize)
|
||||
'click .js-mobile-switch-to-desktop'(event) {
|
||||
event.preventDefault();
|
||||
// Switch global mode to desktop so the card appears as desktop popup
|
||||
Utils.setMobileMode(false);
|
||||
},
|
||||
'click .js-card-zoom-in'(event) {
|
||||
event.preventDefault();
|
||||
const current = Utils.getCardZoom();
|
||||
const newZoom = Math.min(3.0, current + 0.1);
|
||||
Utils.setCardZoom(newZoom);
|
||||
},
|
||||
'click .js-card-zoom-out'(event) {
|
||||
event.preventDefault();
|
||||
const current = Utils.getCardZoom();
|
||||
const newZoom = Math.max(0.5, current - 0.1);
|
||||
Utils.setCardZoom(newZoom);
|
||||
},
|
||||
'click .js-card-mobile-desktop-toggle'(event) {
|
||||
event.preventDefault();
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
'click .js-card-mobile-desktop-toggle'(event) {
|
||||
event.preventDefault();
|
||||
const currentMode = Utils.getMobileMode();
|
||||
Utils.setMobileMode(!currentMode);
|
||||
},
|
||||
async 'submit .js-card-description'(event) {
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
await this.data().setDescription(description);
|
||||
this.data().setDescription(description);
|
||||
},
|
||||
async 'submit .js-card-details-title'(event) {
|
||||
'submit .js-card-details-title'(event) {
|
||||
event.preventDefault();
|
||||
const title = this.currentComponent().getValue().trim();
|
||||
if (title) {
|
||||
await this.data().setTitle(title);
|
||||
this.data().setTitle(title);
|
||||
} else {
|
||||
await this.data().setTitle('');
|
||||
this.data().setTitle('');
|
||||
}
|
||||
},
|
||||
'submit .js-card-details-assigner'(event) {
|
||||
|
|
@ -500,23 +349,23 @@ BlazeComponent.extendComponent({
|
|||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
async 'submit .js-card-details-sort'(event) {
|
||||
'submit .js-card-details-sort'(event) {
|
||||
event.preventDefault();
|
||||
const sort = parseFloat(this.currentComponent()
|
||||
.getValue()
|
||||
.trim());
|
||||
if (!Number.isNaN(sort)) {
|
||||
let card = this.data();
|
||||
await card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
||||
}
|
||||
},
|
||||
async 'change .js-select-card-details-lists'(event) {
|
||||
'change .js-select-card-details-lists'(event) {
|
||||
let card = this.data();
|
||||
const listSelect = this.$('.js-select-card-details-lists')[0];
|
||||
const listId = listSelect.options[listSelect.selectedIndex].value;
|
||||
|
||||
const minOrder = card.getMinSort(listId, card.swimlaneId);
|
||||
await card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
|
||||
card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
|
||||
},
|
||||
'click .js-go-to-linked-card'() {
|
||||
Utils.goCardId(this.data().linkedId);
|
||||
|
|
@ -554,8 +403,11 @@ BlazeComponent.extendComponent({
|
|||
Session.set('cardDetailsIsDragging', false);
|
||||
Session.set('cardDetailsIsMouseDown', false);
|
||||
},
|
||||
async 'click #toggleHideCheckedChecklistItems'() {
|
||||
await this.data().toggleHideCheckedChecklistItems();
|
||||
'click #toggleShowActivitiesCard'() {
|
||||
this.data().toggleShowActivities();
|
||||
},
|
||||
'click #toggleHideCheckedChecklistItems'() {
|
||||
this.data().toggleHideCheckedChecklistItems();
|
||||
},
|
||||
'click #toggleCustomFieldsGridButton'() {
|
||||
Meteor.call('toggleCustomFieldsGrid');
|
||||
|
|
@ -578,57 +430,56 @@ BlazeComponent.extendComponent({
|
|||
) {
|
||||
newState = forIt;
|
||||
}
|
||||
// Use secure server method; direct client updates to vote are blocked
|
||||
Meteor.call('cards.vote', this.data()._id, newState);
|
||||
this.data().setVote(Meteor.userId(), newState);
|
||||
},
|
||||
'click .js-poker'(e) {
|
||||
let newState = null;
|
||||
if ($(e.target).hasClass('js-poker-vote-one')) {
|
||||
newState = 'one';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-two')) {
|
||||
newState = 'two';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-three')) {
|
||||
newState = 'three';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-five')) {
|
||||
newState = 'five';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-eight')) {
|
||||
newState = 'eight';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-thirteen')) {
|
||||
newState = 'thirteen';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-twenty')) {
|
||||
newState = 'twenty';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-forty')) {
|
||||
newState = 'forty';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
|
||||
newState = 'oneHundred';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
if ($(e.target).hasClass('js-poker-vote-unsure')) {
|
||||
newState = 'unsure';
|
||||
Meteor.call('cards.pokerVote', this.data()._id, newState);
|
||||
this.data().setPoker(Meteor.userId(), newState);
|
||||
}
|
||||
},
|
||||
'click .js-poker-finish'(e) {
|
||||
if ($(e.target).hasClass('js-poker-finish')) {
|
||||
e.preventDefault();
|
||||
const now = new Date();
|
||||
Meteor.call('cards.setPokerEnd', this.data()._id, now);
|
||||
const now = formatDateTime(new Date());
|
||||
this.data().setPokerEnd(now);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -636,9 +487,9 @@ BlazeComponent.extendComponent({
|
|||
if ($(e.target).hasClass('js-poker-replay')) {
|
||||
e.preventDefault();
|
||||
this.currentCard = this.currentData();
|
||||
Meteor.call('cards.replayPoker', this.currentCard._id);
|
||||
Meteor.call('cards.unsetPokerEnd', this.currentCard._id);
|
||||
Meteor.call('cards.unsetPokerEstimation', this.currentCard._id);
|
||||
this.currentCard.replayPoker();
|
||||
this.data().unsetPokerEnd();
|
||||
this.data().unsetPokerEstimation();
|
||||
}
|
||||
},
|
||||
'click .js-poker-estimation'(event) {
|
||||
|
|
@ -649,9 +500,9 @@ BlazeComponent.extendComponent({
|
|||
this.find('#pokerEstimation').value = '';
|
||||
|
||||
if (ruleTitle) {
|
||||
Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10));
|
||||
this.data().setPokerEstimation(parseInt(ruleTitle, 10));
|
||||
} else {
|
||||
Meteor.call('cards.unsetPokerEstimation', this.data()._id);
|
||||
this.data().setPokerEstimation('');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -831,7 +682,6 @@ Template.editCardSortOrderForm.onRendered(function () {
|
|||
|
||||
Template.cardDetailsActionsPopup.helpers({
|
||||
isWatching() {
|
||||
if (!this || typeof this.findWatcher !== 'function') return false;
|
||||
return this.findWatcher(Meteor.userId());
|
||||
},
|
||||
|
||||
|
|
@ -862,21 +712,21 @@ Template.cardDetailsActionsPopup.events({
|
|||
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
||||
'click .js-copy-checklist-cards': Popup.open('copyManyCards'),
|
||||
'click .js-set-card-color': Popup.open('setCardColor'),
|
||||
async 'click .js-move-card-to-top'(event) {
|
||||
'click .js-move-card-to-top'(event) {
|
||||
event.preventDefault();
|
||||
const minOrder = this.getMinSort();
|
||||
await this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
|
||||
Popup.back();
|
||||
},
|
||||
async 'click .js-move-card-to-bottom'(event) {
|
||||
'click .js-move-card-to-bottom'(event) {
|
||||
event.preventDefault();
|
||||
const maxOrder = this.getMaxSort();
|
||||
await this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-archive': Popup.afterConfirm('cardArchive', async function () {
|
||||
'click .js-archive': Popup.afterConfirm('cardArchive', function () {
|
||||
Popup.close();
|
||||
await this.archive();
|
||||
this.archive();
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
'click .js-more': Popup.open('cardMore'),
|
||||
|
|
@ -928,12 +778,6 @@ Template.cardMembersPopup.onCreated(function () {
|
|||
});
|
||||
|
||||
Template.cardMembersPopup.events({
|
||||
'click .js-select-member'(event) {
|
||||
const card = Utils.getCurrentCard();
|
||||
const memberId = this.userId;
|
||||
card.toggleMember(memberId);
|
||||
event.preventDefault();
|
||||
},
|
||||
'keyup .card-members-filter'(event) {
|
||||
const members = filterMembers(event.target.value);
|
||||
Template.instance().members.set(members);
|
||||
|
|
@ -941,23 +785,8 @@ Template.cardMembersPopup.events({
|
|||
});
|
||||
|
||||
Template.cardMembersPopup.helpers({
|
||||
isCardMember() {
|
||||
const card = Template.parentData();
|
||||
const cardMembers = card.getMembers();
|
||||
|
||||
return _.contains(cardMembers, this.userId);
|
||||
},
|
||||
|
||||
members() {
|
||||
const members = Template.instance().members.get();
|
||||
const uniqueMembers = _.uniq(members, 'userId');
|
||||
return _.sortBy(uniqueMembers, member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
return user ? user.profile.fullname : '';
|
||||
});
|
||||
},
|
||||
userData() {
|
||||
return ReactiveCache.getUser(this.userId);
|
||||
return _.sortBy(Template.instance().members.get(),'fullname');
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1006,81 +835,36 @@ Template.editCardAssignerForm.events({
|
|||
});
|
||||
|
||||
/** Move Card Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
async setDone(cardId, options) {
|
||||
// Capture DOM values immediately before any async operations
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card selected, move to end
|
||||
const maxSort = card.getMaxSort(options.listId, options.swimlaneId);
|
||||
sortIndex = maxSort !== null ? maxSort + 1 : 0;
|
||||
}
|
||||
|
||||
await card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
const minOrder = card.getMinSort(listId, swimlaneId);
|
||||
card.move(boardId, swimlaneId, listId, minOrder - 1);
|
||||
}
|
||||
}).register('moveCardPopup');
|
||||
|
||||
/** Copy Card Dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
async setDone(cardId, options) {
|
||||
// Capture DOM values immediately before any async operations
|
||||
const textarea = this.$('#copy-card-title');
|
||||
const title = textarea.val().trim();
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
// const textarea = $('#copy-card-title');
|
||||
const textarea = this.$('#copy-card-title');
|
||||
const title = textarea.val().trim();
|
||||
|
||||
if (title) {
|
||||
const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title});
|
||||
|
||||
// Position the copied card (newCard may be null for cross-board copies
|
||||
// if the client hasn't received the publication update yet)
|
||||
if (newCardId) {
|
||||
const newCard = ReactiveCache.getCard(newCardId);
|
||||
if (newCard) {
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card selected, copy to end
|
||||
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
|
||||
sortIndex = maxSort !== null ? maxSort + 1 : 0;
|
||||
}
|
||||
|
||||
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
}
|
||||
}
|
||||
// insert new card to the top of new list
|
||||
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, true, {title: title});
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
|
|
@ -1092,46 +876,29 @@ Template.editCardAssignerForm.events({
|
|||
}).register('copyCardPopup');
|
||||
|
||||
/** Convert Checklist-Item to card dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
async setDone(cardId, options) {
|
||||
// Capture DOM values immediately before any async operations
|
||||
const textarea = this.$('#copy-card-title');
|
||||
const title = textarea.val().trim();
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
const textarea = this.$('#copy-card-title');
|
||||
const title = textarea.val().trim();
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
title: title,
|
||||
listId: options.listId,
|
||||
boardId: options.boardId,
|
||||
swimlaneId: options.swimlaneId,
|
||||
listId: listId,
|
||||
boardId: boardId,
|
||||
swimlaneId: swimlaneId,
|
||||
sort: 0,
|
||||
});
|
||||
const newCard = ReactiveCache.getCard(_id);
|
||||
|
||||
let sortIndex = 0;
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
|
||||
sortIndex = maxSort !== null ? maxSort + 1 : 0;
|
||||
}
|
||||
|
||||
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
const card = ReactiveCache.getCard(_id);
|
||||
const minOrder = card.getMinSort();
|
||||
card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
|
||||
|
||||
Filter.addException(_id);
|
||||
}
|
||||
|
|
@ -1139,46 +906,22 @@ Template.editCardAssignerForm.events({
|
|||
}).register('convertChecklistItemToCardPopup');
|
||||
|
||||
/** Copy many cards dialog */
|
||||
(class extends DialogWithBoardSwimlaneListCard {
|
||||
(class extends DialogWithBoardSwimlaneList {
|
||||
getDialogOptions() {
|
||||
const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
async setDone(cardId, options) {
|
||||
// Capture DOM values immediately before any async operations
|
||||
const textarea = this.$('#copy-card-title');
|
||||
const title = textarea.val().trim();
|
||||
const position = this.$('input[name="position"]:checked').val();
|
||||
|
||||
setDone(boardId, swimlaneId, listId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
||||
const card = this.data();
|
||||
|
||||
const textarea = this.$('#copy-card-title');
|
||||
const title = textarea.val().trim();
|
||||
|
||||
if (title) {
|
||||
const titleList = JSON.parse(title);
|
||||
for (const obj of titleList) {
|
||||
const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description});
|
||||
|
||||
// Position the copied card
|
||||
if (newCardId) {
|
||||
const newCard = ReactiveCache.getCard(newCardId);
|
||||
let sortIndex = 0;
|
||||
|
||||
if (cardId) {
|
||||
const targetCard = ReactiveCache.getCard(cardId);
|
||||
if (targetCard) {
|
||||
if (position === 'above') {
|
||||
sortIndex = targetCard.sort - 0.5;
|
||||
} else {
|
||||
sortIndex = targetCard.sort + 0.5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
|
||||
sortIndex = maxSort !== null ? maxSort + 1 : 0;
|
||||
}
|
||||
|
||||
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
||||
}
|
||||
const newCardId = Meteor.call('copyCard', card._id, boardId, swimlaneId, listId, false, {title: obj.title, description: obj.description});
|
||||
|
||||
// In case the filter is active we need to add the newly inserted card in
|
||||
// the list of exceptions -- cards that are not filtered. Otherwise the
|
||||
|
|
@ -1213,14 +956,14 @@ BlazeComponent.extendComponent({
|
|||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
async 'click .js-submit'(event) {
|
||||
'click .js-submit'(event) {
|
||||
event.preventDefault();
|
||||
await this.currentCard.setColor(this.currentColor.get());
|
||||
this.currentCard.setColor(this.currentColor.get());
|
||||
Popup.back();
|
||||
},
|
||||
async 'click .js-remove-color'(event) {
|
||||
'click .js-remove-color'(event) {
|
||||
event.preventDefault();
|
||||
await this.currentCard.setColor(null);
|
||||
this.currentCard.setColor(null);
|
||||
Popup.back();
|
||||
},
|
||||
},
|
||||
|
|
@ -1228,51 +971,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('setCardColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentColor = new ReactiveVar(null);
|
||||
},
|
||||
|
||||
colors() {
|
||||
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
|
||||
},
|
||||
|
||||
isSelected(color) {
|
||||
return this.currentColor.get() === color;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-palette-color'(event) {
|
||||
// Extract color from class name like "card-details-red"
|
||||
const classes = $(event.currentTarget).attr('class').split(' ');
|
||||
const colorClass = classes.find(cls => cls.startsWith('card-details-'));
|
||||
const color = colorClass ? colorClass.replace('card-details-', '') : null;
|
||||
this.currentColor.set(color);
|
||||
},
|
||||
async 'click .js-submit'(event) {
|
||||
event.preventDefault();
|
||||
const color = this.currentColor.get();
|
||||
// Use MultiSelection to get selected cards and set color on each
|
||||
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
||||
await card.setColor(color);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
async 'click .js-remove-color'(event) {
|
||||
event.preventDefault();
|
||||
// Use MultiSelection to get selected cards and remove color from each
|
||||
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
||||
await card.setColor(null);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('setSelectionColorPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentCard = this.currentData();
|
||||
|
|
@ -1407,15 +1105,20 @@ BlazeComponent.extendComponent({
|
|||
'is-checked',
|
||||
);
|
||||
const endString = this.currentCard.getVoteEnd();
|
||||
Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers);
|
||||
|
||||
this.currentCard.setVoteQuestion(
|
||||
voteQuestion,
|
||||
publicVote,
|
||||
allowNonBoardMembers,
|
||||
);
|
||||
if (endString) {
|
||||
Meteor.call('cards.setVoteEnd', this.currentCard._id, endString);
|
||||
this.currentCard.setVoteEnd(endString);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => {
|
||||
event.preventDefault();
|
||||
Meteor.call('cards.unsetVote', this.currentCard._id);
|
||||
this.currentCard.unsetVote();
|
||||
Popup.back();
|
||||
}),
|
||||
'click a.js-toggle-vote-public'(event) {
|
||||
|
|
@ -1473,13 +1176,13 @@ BlazeComponent.extendComponent({
|
|||
'DD/MM/YYYY HH:mm',
|
||||
'DD-MM-YYYY HH:mm'
|
||||
];
|
||||
|
||||
|
||||
let parsedDate = null;
|
||||
for (const format of formats) {
|
||||
parsedDate = parseDate(dateString, [format], true);
|
||||
if (parsedDate) break;
|
||||
}
|
||||
|
||||
|
||||
// Fallback to native Date parsing
|
||||
if (!parsedDate) {
|
||||
parsedDate = new Date(dateString);
|
||||
|
|
@ -1614,10 +1317,10 @@ BlazeComponent.extendComponent({
|
|||
];
|
||||
}
|
||||
_storeDate(newDate) {
|
||||
Meteor.call('cards.setVoteEnd', this.card._id, newDate);
|
||||
this.card.setVoteEnd(newDate);
|
||||
}
|
||||
_deleteDate() {
|
||||
Meteor.call('cards.unsetVoteEnd', this.card._id);
|
||||
this.card.unsetVoteEnd();
|
||||
}
|
||||
}.register('editVoteEndDatePopup'));
|
||||
|
||||
|
|
@ -1639,14 +1342,17 @@ BlazeComponent.extendComponent({
|
|||
);
|
||||
const endString = this.currentCard.getPokerEnd();
|
||||
|
||||
Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers);
|
||||
this.currentCard.setPokerQuestion(
|
||||
pokerQuestion,
|
||||
allowNonBoardMembers,
|
||||
);
|
||||
if (endString) {
|
||||
Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString));
|
||||
this.currentCard.setPokerEnd(endString);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => {
|
||||
Meteor.call('cards.unsetPoker', this.currentCard._id);
|
||||
this.currentCard.unsetPoker();
|
||||
Popup.back();
|
||||
}),
|
||||
'click a.js-toggle-poker-allow-non-members'(event) {
|
||||
|
|
@ -1725,13 +1431,13 @@ BlazeComponent.extendComponent({
|
|||
'DD/MM/YYYY HH:mm',
|
||||
'DD-MM-YYYY HH:mm'
|
||||
];
|
||||
|
||||
|
||||
let parsedDate = null;
|
||||
for (const format of formats) {
|
||||
parsedDate = parseDate(dateString, [format], true);
|
||||
if (parsedDate) break;
|
||||
}
|
||||
|
||||
|
||||
// Fallback to native Date parsing
|
||||
if (!parsedDate) {
|
||||
parsedDate = new Date(dateString);
|
||||
|
|
@ -1867,17 +1573,17 @@ BlazeComponent.extendComponent({
|
|||
];
|
||||
}
|
||||
_storeDate(newDate) {
|
||||
Meteor.call('cards.setPokerEnd', this.card._id, newDate);
|
||||
this.card.setPokerEnd(newDate);
|
||||
}
|
||||
_deleteDate() {
|
||||
Meteor.call('cards.unsetPokerEnd', this.card._id);
|
||||
this.card.unsetPokerEnd();
|
||||
}
|
||||
}.register('editPokerEndDatePopup'));
|
||||
|
||||
// Close the card details pane by pressing escape
|
||||
EscapeActions.register(
|
||||
'detailsPane',
|
||||
async () => {
|
||||
() => {
|
||||
// if card description diverges from database due to editing
|
||||
// ask user whether changes should be applied
|
||||
if (ReactiveCache.getCurrentUser()) {
|
||||
|
|
@ -1885,7 +1591,7 @@ EscapeActions.register(
|
|||
currentDescription = document.getElementsByClassName("editor js-new-description-input").item(0)
|
||||
if (currentDescription?.value && !(currentDescription.value === Utils.getCurrentCard().getDescription())) {
|
||||
if (confirm(TAPi18n.__('rescue-card-description-dialogue'))) {
|
||||
await Utils.getCurrentCard().setDescription(document.getElementsByClassName("editor js-new-description-input").item(0).value);
|
||||
Utils.getCurrentCard().setDescription(document.getElementsByClassName("editor js-new-description-input").item(0).value);
|
||||
// Save it!
|
||||
console.log(document.getElementsByClassName("editor js-new-description-input").item(0).value);
|
||||
console.log("current description", Utils.getCurrentCard().getDescription());
|
||||
|
|
@ -1942,15 +1648,10 @@ Template.cardAssigneesPopup.helpers({
|
|||
},
|
||||
|
||||
members() {
|
||||
const members = Template.instance().members.get();
|
||||
const uniqueMembers = _.uniq(members, 'userId');
|
||||
return _.sortBy(uniqueMembers, member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
return user ? user.profile.fullname : '';
|
||||
});
|
||||
return _.sortBy(Template.instance().members.get(),'fullname');
|
||||
},
|
||||
|
||||
userData() {
|
||||
user() {
|
||||
return ReactiveCache.getUser(this.userId);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,12 +37,10 @@ textarea.js-edit-checklist-item {
|
|||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff;
|
||||
background-color: #666;
|
||||
color: #fff !important;
|
||||
background-color: #2196f3 !important;
|
||||
padding: 0.01em 16px;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
|
|
@ -74,6 +72,10 @@ textarea.js-edit-checklist-item {
|
|||
padding-top: 3px;
|
||||
float: left;
|
||||
}
|
||||
.checklist-title span.fa.checklist-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
#card-details-overlay {
|
||||
top: 0;
|
||||
bottom: -600px;
|
||||
|
|
@ -150,6 +152,10 @@ textarea.js-edit-checklist-item {
|
|||
padding-top: 2px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.checklist-item span.fa.checklistitem-handle.fa-arrows::before {
|
||||
content: "↕️" !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
.js-delete-checklist-item,
|
||||
.js-convert-checklist-item-to-card {
|
||||
margin: 0 0 0.5em 1.33em;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
template(name="checklists")
|
||||
.checklists-title
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
| {{_ 'checklists'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId
|
||||
position="top")
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
if currentUser.isBoardMember
|
||||
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
|
||||
//span.toggle-switch-title
|
||||
|
|
@ -29,7 +28,7 @@ template(name="checklists")
|
|||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=false)
|
||||
else
|
||||
a.add-checklist.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
template(name="checklistDetail")
|
||||
.js-checklist.checklist.nodragscroll
|
||||
|
|
@ -39,7 +38,7 @@ template(name="checklistDetail")
|
|||
.checklist-title
|
||||
span
|
||||
if canModifyCard
|
||||
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
|
||||
if canModifyCard
|
||||
h4.title.js-open-inlined-form.is-editable
|
||||
|
|
@ -64,12 +63,13 @@ template(name="checklistDeletePopup")
|
|||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="addChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-add-checklist-item(rows='1' autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
a.js-close-inlined-form(title="{{_ 'close-add-checklist-item'}}")
|
||||
| ❌
|
||||
if showNewlineBecomesNewChecklistItem
|
||||
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
|
||||
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
|
||||
|
|
@ -82,7 +82,7 @@ template(name="addChecklistItemForm")
|
|||
| {{_ 'originOrder'}}
|
||||
|
||||
template(name="editChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
|
||||
if $eq type 'item'
|
||||
|
|
@ -91,12 +91,13 @@ template(name="editChecklistItemForm")
|
|||
= checklist.title
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
a.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
|
||||
| ❌
|
||||
span(title=createdAt) {{ moment createdAt }}
|
||||
if canModifyCard
|
||||
a.js-delete-checklist-item {{_ "delete"}}...
|
||||
a.js-convert-checklist-item-to-card
|
||||
i.fa.fa-copy
|
||||
| 📋
|
||||
| {{_ 'convertChecklistItemToCardPopup-title'}}
|
||||
|
||||
template(name="checklistItems")
|
||||
|
|
@ -106,7 +107,7 @@ template(name="checklistItems")
|
|||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
.checklist-items.js-checklist-items
|
||||
each item in checklist.items
|
||||
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
|
||||
|
|
@ -118,7 +119,7 @@ template(name="checklistItems")
|
|||
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true)
|
||||
else
|
||||
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
template(name='checklistItemDetail')
|
||||
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
|
||||
|
|
@ -126,8 +127,7 @@ template(name='checklistItemDetail')
|
|||
if canModifyCard
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
||||
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
|
@ -141,16 +141,16 @@ template(name="checklistActionsPopup")
|
|||
ul.pop-over-list
|
||||
li
|
||||
a.js-delete-checklist.delete-checklist
|
||||
i.fa.fa-trash
|
||||
| 🗑️
|
||||
| {{_ "delete"}} ...
|
||||
a.js-move-checklist.move-checklist
|
||||
i.fa.fa-arrow-right
|
||||
| ➡️
|
||||
| {{_ "moveChecklist"}} ...
|
||||
a.js-copy-checklist.copy-checklist
|
||||
i.fa.fa-copy
|
||||
| 📋
|
||||
| {{_ "copyChecklist"}} ...
|
||||
a.js-hide-checked-checklist-items
|
||||
i.fa.fa-eye-slash
|
||||
| 🙈
|
||||
| {{_ "hideCheckedChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
|
||||
if checklist.hideCheckedChecklistItems
|
||||
|
|
@ -159,7 +159,7 @@ template(name="checklistActionsPopup")
|
|||
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
|
||||
a.js-hide-all-checklist-items
|
||||
i.fa.fa-ban
|
||||
| 🚫
|
||||
| {{_ "hideAllChecklistItems"}} ...
|
||||
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
|
||||
if checklist.hideAllChecklistItems
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ BlazeComponent.extendComponent({
|
|||
textarea.focus();
|
||||
},
|
||||
|
||||
async deleteItem() {
|
||||
deleteItem() {
|
||||
const checklist = this.currentData().checklist;
|
||||
const item = this.currentData().item;
|
||||
if (checklist && item && item._id) {
|
||||
|
|
@ -372,9 +372,9 @@ BlazeComponent.extendComponent({
|
|||
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
async setDone(cardId, options) {
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
await this.data().checklist.move(cardId);
|
||||
this.data().checklist.move(cardId);
|
||||
}
|
||||
}).register('moveChecklistPopup');
|
||||
|
||||
|
|
@ -384,8 +384,8 @@ BlazeComponent.extendComponent({
|
|||
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
async setDone(cardId, options) {
|
||||
setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
await this.data().checklist.copy(cardId);
|
||||
this.data().checklist.copy(cardId);
|
||||
}
|
||||
}).register('copyChecklistPopup');
|
||||
|
|
|
|||
|
|
@ -223,13 +223,9 @@
|
|||
.card-label-edit-button:hover {
|
||||
background: #dbdbdb;
|
||||
}
|
||||
ul.edit-labels-pop-over span.label-handle {
|
||||
ul.edit-labels-pop-over span.fa.label-handle {
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
ul.edit-labels-pop-over span.label-handle + .card-label {
|
||||
ul.edit-labels-pop-over span.fa.label-handle + .card-label {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ template(name="formLabel")
|
|||
.palette-colors: each labels
|
||||
span.card-label.palette-color.js-palette-color(class="card-label-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
||||
template(name="createLabelPopup")
|
||||
form.create-label
|
||||
|
|
@ -28,7 +28,8 @@ template(name="cardLabelsPopup")
|
|||
ul.edit-labels-pop-over
|
||||
each board.labels
|
||||
li.js-card-label-item
|
||||
a.card-label-edit-button.fa.fa-pencil.js-edit-label
|
||||
a.card-label-edit-button.js-edit-label
|
||||
| ✏️
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
|
||||
span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
|
||||
|
|
@ -36,5 +37,5 @@ template(name="cardLabelsPopup")
|
|||
+viewer
|
||||
= name
|
||||
if(isLabelSelected ../_id)
|
||||
i.card-label-selectable-icon.fa.fa-check
|
||||
| ✅
|
||||
a.quiet-button.full.js-add-label {{_ 'label-create'}}
|
||||
|
|
|
|||
|
|
@ -125,8 +125,19 @@ Template.createLabelPopup.events({
|
|||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
board.addLabel(name, color);
|
||||
|
||||
// Find the selected color by looking for the palette color that contains the checkmark
|
||||
let selectedColor = null;
|
||||
templateInstance.$('.js-palette-color').each(function() {
|
||||
if ($(this).text().includes('✅')) {
|
||||
selectedColor = Blaze.getData(this).color;
|
||||
return false; // break out of loop
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedColor) {
|
||||
board.addLabel(name, selectedColor);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
|
@ -144,8 +155,19 @@ Template.editLabelPopup.events({
|
|||
.$('#labelName')
|
||||
.val()
|
||||
.trim();
|
||||
const color = Blaze.getData(templateInstance.find('.fa-check')).color;
|
||||
board.editLabel(this._id, name, color);
|
||||
|
||||
// Find the selected color by looking for the palette color that contains the checkmark
|
||||
let selectedColor = null;
|
||||
templateInstance.$('.js-palette-color').each(function() {
|
||||
if ($(this).text().includes('✅')) {
|
||||
selectedColor = Blaze.getData(this).color;
|
||||
return false; // break out of loop
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedColor) {
|
||||
board.editLabel(this._id, name, selectedColor);
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@
|
|||
}
|
||||
.minicard-details-menu-with-handle {
|
||||
float: right;
|
||||
padding-left: 0.7vw;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
padding-right: 4vw;
|
||||
padding-left: 0.7vw;
|
||||
}
|
||||
.minicard-details-menu {
|
||||
float: right;
|
||||
|
|
@ -98,7 +97,6 @@
|
|||
}
|
||||
.minicard .minicard-labels {
|
||||
float: none;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-labels .minicard-label {
|
||||
width: clamp(12px, 1.5vw, 16px);
|
||||
|
|
@ -113,7 +111,6 @@
|
|||
}
|
||||
.minicard .minicard-custom-fields {
|
||||
display: block;
|
||||
margin-right: 6vw;
|
||||
}
|
||||
.minicard .minicard-custom-field {
|
||||
display: flex;
|
||||
|
|
@ -136,25 +133,18 @@
|
|||
width: clamp(20px, 2.5vw, 28px);
|
||||
height: clamp(20px, 2.5vw, 28px);
|
||||
position: absolute;
|
||||
right: 0vw;
|
||||
top: 4vh;
|
||||
right: 0.7vw;
|
||||
top: 0.7vh;
|
||||
display: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@media only screen {
|
||||
.minicard .handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.minicard .handle .drag-handle {
|
||||
.minicard .handle .fa-arrows {
|
||||
font-size: clamp(16px, 3vw, 20px);
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
width: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
.minicard .minicard-title {
|
||||
margin-right: 1.5vw;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
|
|
@ -174,10 +164,6 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin-right: 6vw;
|
||||
clear: both;
|
||||
}
|
||||
.minicard .date {
|
||||
margin-right: 0.4vw;
|
||||
|
|
@ -311,6 +297,19 @@
|
|||
background-color: #1976d2 !important;
|
||||
}
|
||||
|
||||
/* Font Awesome icons in minicard dates */
|
||||
.minicard .card-date i.fa {
|
||||
margin-right: 0.3vw;
|
||||
font-size: 0.9em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Font Awesome icons in minicard spent time */
|
||||
.minicard .card-time i.fa {
|
||||
margin-right: 0.3vw;
|
||||
font-size: 0.9em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.minicard .badges {
|
||||
float: left;
|
||||
margin-top: 1vh;
|
||||
|
|
@ -742,80 +741,7 @@
|
|||
gap: 0.3vw;
|
||||
}
|
||||
|
||||
/* Checklist display on minicard */
|
||||
.minicard-checklist {
|
||||
width: 100%;
|
||||
margin-top: 0.5vh;
|
||||
margin-bottom: 0.5vh;
|
||||
padding: 0.3vh 0.5vw;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 0.3vw;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3vh;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-title {
|
||||
.minicard-list-name i.fa {
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
color: #4d4d4d;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu {
|
||||
font-size: 1.2em;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 0 0.3vw;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-menu:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
margin-bottom: 0.2vh;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.3vw;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
padding: 0.2vh 0;
|
||||
border-radius: 0.2vw;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item.is-checked {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .check-box-unicode {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8em;
|
||||
margin-top: 0.1vh;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item:hover .check-box-unicode {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-item .item-title {
|
||||
flex: 1;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ template(name="minicard")
|
|||
class="{{#if isLinkedCard}}linked-card{{/if}}"
|
||||
class="{{#if isLinkedBoard}}linked-board{{/if}}"
|
||||
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
|
||||
if canMoveCard
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
.handle
|
||||
i.fa.fa-arrows
|
||||
if canModifyCard
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") ☰
|
||||
.handle
|
||||
| ↕️
|
||||
.dates
|
||||
if getReceived
|
||||
.date
|
||||
|
|
@ -33,7 +30,7 @@ template(name="minicard")
|
|||
if hasActiveUploads
|
||||
.minicard-upload-progress
|
||||
.upload-progress-header
|
||||
i.fa.fa-upload
|
||||
| 📤
|
||||
span {{_ 'uploading-files'}} ({{uploadCount}})
|
||||
each uploads
|
||||
.upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
|
||||
|
|
@ -42,11 +39,11 @@ template(name="minicard")
|
|||
.upload-progress-fill(style="width: {{progress}}%")
|
||||
if $eq status 'error'
|
||||
.upload-progress-error
|
||||
i.fa.fa-warning
|
||||
| ⚠️
|
||||
span {{_ 'upload-failed'}}
|
||||
else if $eq status 'completed'
|
||||
.upload-progress-success
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
span {{_ 'upload-completed'}}
|
||||
|
||||
.minicard-title
|
||||
|
|
@ -58,15 +55,12 @@ template(name="minicard")
|
|||
| {{ parentCardName }}
|
||||
if isLinkedBoard
|
||||
a.js-linked-link
|
||||
span.linked-icon
|
||||
i.fa.fa-folder
|
||||
span.linked-icon | 📁
|
||||
else if isLinkedCard
|
||||
a.js-linked-link
|
||||
span.linked-icon
|
||||
i.fa.fa-id-card
|
||||
span.linked-icon | 🃏
|
||||
if getArchived
|
||||
span.linked-icon.linked-archived
|
||||
i.fa.fa-archive
|
||||
span.linked-icon.linked-archived | 📦
|
||||
+viewer
|
||||
if currentBoard.allowsCardNumber
|
||||
span.card-number
|
||||
|
|
@ -147,53 +141,45 @@ template(name="minicard")
|
|||
if canModifyCard
|
||||
if comments.length
|
||||
.badge(title="{{_ 'card-comments-title' comments.length }}")
|
||||
span.badge-icon.badge-comment.badge-text
|
||||
i.fa.fa-comment-o
|
||||
span.badge-icon.badge-comment.badge-text 💬
|
||||
= ' '
|
||||
= comments.length
|
||||
//span.badge-comment.badge-text
|
||||
//|
|
||||
{{_ 'comment'}}
|
||||
//| {{_ 'comment'}}
|
||||
if getDescription
|
||||
unless currentBoard.allowsDescriptionTextOnMinicard
|
||||
.badge.badge-state-image-only(title=getDescription)
|
||||
span.badge-icon
|
||||
i.fa.fa-file-text-o
|
||||
span.badge-icon 📝
|
||||
if getVoteQuestion
|
||||
.badge.badge-state-image-only(title=getVoteQuestion)
|
||||
span.badge-icon(class="{{#if voteState}}text-green{{/if}}")
|
||||
i.fa.fa-thumbs-up
|
||||
span.badge-icon(class="{{#if voteState}}text-green{{/if}}") 👍
|
||||
span.badge-text {{ voteCountPositive }}
|
||||
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}")
|
||||
i.fa.fa-thumbs-down
|
||||
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") 👎
|
||||
span.badge-text {{ voteCountNegative }}
|
||||
if getPokerQuestion
|
||||
.badge.badge-state-image-only(title=getPokerQuestion)
|
||||
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}")
|
||||
i.fa.fa-check-square
|
||||
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") ✅
|
||||
if expiredPoker
|
||||
span.badge-text {{ getPokerEstimation }}
|
||||
if attachments.length
|
||||
if currentBoard.allowsBadgeAttachmentOnMinicard
|
||||
.badge
|
||||
span.badge-icon
|
||||
i.fa.fa-paperclip
|
||||
span.badge-icon 📎
|
||||
span.badge-text= attachments.length
|
||||
if checklists.length
|
||||
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
|
||||
span.badge-icon ☑️
|
||||
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||
if allSubtasks.count
|
||||
.badge
|
||||
span.badge-icon
|
||||
i.fa.fa-globe
|
||||
span.badge-icon 🌐
|
||||
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
|
||||
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
if currentBoard.allowsCardSortingByNumberOnMinicard
|
||||
.badge
|
||||
span.badge-icon
|
||||
i.fa.fa-sort-numeric-asc
|
||||
span.badge-icon 🔢
|
||||
span.badge-text.check-list-sort {{ sort }}
|
||||
if shouldShowChecklistAtMinicard
|
||||
each shouldShowChecklistAtMinicard
|
||||
+minicardChecklist(checklist=. card=..)
|
||||
if currentBoard.allowsDescriptionTextOnMinicard
|
||||
if getDescription
|
||||
.minicard-description
|
||||
|
|
@ -201,7 +187,7 @@ template(name="minicard")
|
|||
| {{ getDescription }}
|
||||
if shouldShowListOnMinicard
|
||||
.minicard-list-name
|
||||
i.fa.fa-list
|
||||
| 📋
|
||||
| {{ listName }}
|
||||
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
|
||||
.parent-subtext
|
||||
|
|
@ -215,13 +201,55 @@ template(name="editCardSortOrderPopup")
|
|||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
|
||||
|
||||
template(name="minicardChecklist")
|
||||
.minicard-checklist
|
||||
.checklist-header
|
||||
.checklist-title= checklist.title
|
||||
if canModifyCard
|
||||
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
each visibleItems
|
||||
+checklistItemDetail(item = . checklist = checklist card = card)
|
||||
template(name="minicardDetailsActionsPopup")
|
||||
ul.pop-over-list
|
||||
if canModifyCard
|
||||
li
|
||||
a.js-move-card
|
||||
| ➡️
|
||||
| {{_ 'moveCardPopup-title'}}
|
||||
li
|
||||
a.js-copy-card
|
||||
| 📋
|
||||
| {{_ 'copyCardPopup-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-archive
|
||||
| ➡️
|
||||
| 📦
|
||||
| {{_ 'archive-card'}}
|
||||
hr
|
||||
li
|
||||
a.js-move-card-to-top
|
||||
| ⬆️
|
||||
| {{_ 'moveCardToTop-title'}}
|
||||
li
|
||||
a.js-move-card-to-bottom
|
||||
| ⬇️
|
||||
| {{_ 'moveCardToBottom-title'}}
|
||||
hr
|
||||
li
|
||||
a.js-add-labels
|
||||
| 🏷️
|
||||
| {{_ 'card-edit-labels'}}
|
||||
li
|
||||
a.js-due-date
|
||||
| 📥
|
||||
| {{_ 'editCardDueDatePopup-title'}}
|
||||
li
|
||||
a.js-set-card-color
|
||||
| 🎨
|
||||
| {{_ 'setCardColorPopup-title'}}
|
||||
li
|
||||
a.js-link
|
||||
| 🔗
|
||||
| {{_ 'link-card'}}
|
||||
li
|
||||
a.js-toggle-watch-card
|
||||
if isWatching
|
||||
| 👁️
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
| 👁️-slash
|
||||
| {{_ 'watch'}}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,13 +91,6 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
async toggleChecklistItem() {
|
||||
const item = this.currentData();
|
||||
if (item && item._id) {
|
||||
await item.toggleItem();
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -115,11 +108,7 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||
'click .minicard-labels' : this.cardLabelsPopup,
|
||||
'click .js-open-minicard-details-menu'(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
Popup.open('cardDetailsActions').call(this, event);
|
||||
},
|
||||
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
||||
// Drag and drop file upload handlers
|
||||
'dragover .minicard'(event) {
|
||||
// Only prevent default for file drags to avoid interfering with sortable
|
||||
|
|
@ -181,43 +170,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('minicard');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'minicardChecklist';
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-open-checklist-menu'(event) {
|
||||
const data = this.currentData();
|
||||
const checklist = data.checklist || data;
|
||||
const card = data.card || this.data();
|
||||
const context = { currentData: () => ({ checklist, card }) };
|
||||
Popup.open('checklistActions').call(context, event);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
visibleItems() {
|
||||
const checklist = this.currentData().checklist || this.currentData();
|
||||
const items = checklist.items();
|
||||
|
||||
return items.filter(item => {
|
||||
// Hide finished items if hideCheckedChecklistItems is true
|
||||
if (item.isFinished && checklist.hideCheckedChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
// Hide all items if hideAllChecklistItems is true
|
||||
if (checklist.hideAllChecklistItems) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
}).register('minicardChecklist');
|
||||
|
||||
Template.minicard.helpers({
|
||||
hiddenMinicardLabelText() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
|
|
@ -257,29 +209,9 @@ Template.minicard.helpers({
|
|||
// Show list name if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific card has the setting enabled
|
||||
const currentBoard = this.board();
|
||||
const currentBoard = this.currentBoard;
|
||||
if (!currentBoard) return false;
|
||||
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
|
||||
},
|
||||
|
||||
shouldShowChecklistAtMinicard() {
|
||||
// Return checklists that should be shown on minicard
|
||||
const currentBoard = this.board();
|
||||
if (!currentBoard) return [];
|
||||
|
||||
const checklists = this.checklists();
|
||||
const visibleChecklists = [];
|
||||
|
||||
checklists.forEach(checklist => {
|
||||
// Show checklist if either:
|
||||
// 1. Board-wide setting is enabled, OR
|
||||
// 2. This specific checklist has the setting enabled
|
||||
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
|
||||
visibleChecklists.push(checklist);
|
||||
}
|
||||
});
|
||||
|
||||
return visibleChecklists;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -310,3 +242,35 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
}).register('editCardSortOrderPopup');
|
||||
|
||||
Template.minicardDetailsActionsPopup.events({
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-move-card': Popup.open('moveCard'),
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
'click .js-set-card-color': Popup.open('setCardColor'),
|
||||
'click .js-add-labels': Popup.open('cardLabels'),
|
||||
'click .js-link': Popup.open('linkCard'),
|
||||
'click .js-move-card-to-top'(event) {
|
||||
event.preventDefault();
|
||||
const minOrder = this.getMinSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-move-card-to-bottom'(event) {
|
||||
event.preventDefault();
|
||||
const maxOrder = this.getMaxSort();
|
||||
this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-archive': Popup.afterConfirm('cardArchive', function () {
|
||||
Popup.close();
|
||||
this.archive();
|
||||
Utils.goBoardId(this.boardId);
|
||||
}),
|
||||
'click .js-toggle-watch-card'() {
|
||||
const currentCard = this;
|
||||
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
|
||||
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
|
||||
if (!err && ret) Popup.back();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ template(name="resultCard")
|
|||
= getBoard.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
| NULL
|
||||
if getBoard.archived
|
||||
i.fa.fa-archive
|
||||
| 📦
|
||||
li.result-card-context.result-card-context-separator
|
||||
= ' '
|
||||
| {{_ 'context-separator'}}
|
||||
|
|
@ -25,9 +25,9 @@ template(name="resultCard")
|
|||
= getSwimlane.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
| NULL
|
||||
if getSwimlane.archived
|
||||
i.fa.fa-archive
|
||||
| 📦
|
||||
li.result-card-context.result-card-context-separator
|
||||
= ' '
|
||||
| {{_ 'context-separator'}}
|
||||
|
|
@ -39,6 +39,6 @@ template(name="resultCard")
|
|||
= getList.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| {{_ 'no-name'}}
|
||||
| NULL
|
||||
if getList.archived
|
||||
i.fa.fa-archive
|
||||
| 📦
|
||||
|
|
|
|||
|
|
@ -87,15 +87,6 @@ textarea.js-edit-subtask-item {
|
|||
top: 0;
|
||||
bottom: -600px;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure this doesn't block card interaction */
|
||||
@media screen and (max-width: 800px) {
|
||||
#card-details-overlay {
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.subtasks {
|
||||
background: #f7f7f7;
|
||||
|
|
@ -136,25 +127,6 @@ textarea.js-edit-subtask-item {
|
|||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.subtasks-item .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .subtasks-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .subtasks-item .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.subtasks-item .item-title {
|
||||
flex: 1;
|
||||
padding-left: 10px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
template(name="subtasks")
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-globe
|
||||
| 🌐
|
||||
| {{_ 'subtasks'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if toggleDeleteDialog.get
|
||||
|
|
@ -16,7 +16,7 @@ template(name="subtasks")
|
|||
+addSubtaskItemForm
|
||||
else
|
||||
a.js-open-inlined-form(title="{{_ 'add-subtask'}}")
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
|
||||
template(name="subtaskDetail")
|
||||
.js-subtasks.subtask
|
||||
|
|
@ -68,20 +68,18 @@ template(name="subtasksItems")
|
|||
+addSubtaskItemForm
|
||||
else
|
||||
a.add-subtask-item.js-open-inlined-form
|
||||
i.fa.fa-plus
|
||||
| ➕
|
||||
| {{_ 'add-subtask-item'}}...
|
||||
|
||||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
span.check-box-unicode
|
||||
i.fa(class="{{#if item.isFinished}}fa-check-square{{else}}fa-square-o{{/if}}")
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
|
@ -94,10 +92,10 @@ template(name="subtaskActionsPopup")
|
|||
ul.pop-over-list
|
||||
li
|
||||
a.js-view-subtask(title="{{ subtask.title }}")
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
| {{_ "view-it"}}
|
||||
if currentUser.isBoardAdmin
|
||||
a.js-delete-subtask.delete-subtask
|
||||
i.fa.fa-trash
|
||||
| 🗑️
|
||||
| {{_ "delete"}} ...
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
addSubtask(event) {
|
||||
|
|
@ -62,10 +61,10 @@ BlazeComponent.extendComponent({
|
|||
textarea.focus();
|
||||
},
|
||||
|
||||
async deleteSubtask() {
|
||||
deleteSubtask() {
|
||||
const subtask = this.currentData().subtask;
|
||||
if (subtask && subtask._id) {
|
||||
await subtask.archive();
|
||||
subtask.archive();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -73,12 +72,12 @@ BlazeComponent.extendComponent({
|
|||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
|
||||
async editSubtask(event) {
|
||||
editSubtask(event) {
|
||||
event.preventDefault();
|
||||
const textarea = this.find('textarea.js-edit-subtask-item');
|
||||
const title = textarea.value.trim();
|
||||
const subtask = this.currentData().subtask;
|
||||
await subtask.setTitle(title);
|
||||
subtask.setTitle(title);
|
||||
},
|
||||
|
||||
pressKey(event) {
|
||||
|
|
@ -105,19 +104,7 @@ BlazeComponent.extendComponent({
|
|||
}).register('subtasks');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
async toggleItem() {
|
||||
const item = this.currentData().item;
|
||||
if (item && item._id) {
|
||||
await item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
|
||||
},
|
||||
];
|
||||
},
|
||||
// ...
|
||||
}).register('subtaskItemDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
|
@ -138,11 +125,11 @@ BlazeComponent.extendComponent({
|
|||
});
|
||||
}
|
||||
},
|
||||
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () {
|
||||
'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', function () {
|
||||
Popup.back(2);
|
||||
const subtask = this.subtask;
|
||||
if (subtask && subtask._id) {
|
||||
await subtask.archive();
|
||||
subtask.archive();
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,11 +81,11 @@
|
|||
font-size: 11px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
|
||||
.original-position-details {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
|
||||
.original-position-moved,
|
||||
.original-position-unchanged {
|
||||
padding: 3px 5px;
|
||||
|
|
@ -99,24 +99,24 @@
|
|||
border-color: #4a5568;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
|
||||
.original-position-moved {
|
||||
background-color: #744210;
|
||||
border-color: #b7791f;
|
||||
color: #fbd38d;
|
||||
}
|
||||
|
||||
|
||||
.original-position-unchanged {
|
||||
background-color: #22543d;
|
||||
border-color: #38a169;
|
||||
color: #9ae6b4;
|
||||
}
|
||||
|
||||
|
||||
.original-title {
|
||||
color: #a0aec0;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
|
||||
.original-title strong {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,22 @@
|
|||
<div class="original-position-info">
|
||||
{{#if isLoading}}
|
||||
<div class="original-position-loading">
|
||||
⏳ Loading original position...
|
||||
<i class="fa fa-spinner fa-spin"></i> Loading original position...
|
||||
</div>
|
||||
{{else if showOriginalPosition}}
|
||||
<div class="original-position-details">
|
||||
{{#if hasMovedFromOriginal}}
|
||||
<div class="original-position-moved">
|
||||
<span class="original-position-text">ℹ️ {{getOriginalPositionDescription}}</span>
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<span class="original-position-text">{{getOriginalPositionDescription}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="original-position-unchanged">
|
||||
<span class="original-position-text">✅ In original position</span>
|
||||
<i class="fa fa-check-circle"></i>
|
||||
<span class="original-position-text">In original position</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if getOriginalTitle}}
|
||||
<div class="original-title">
|
||||
<strong>Original title:</strong> {{getOriginalTitle}}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
this.originalPosition = new ReactiveVar(null);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.hasMoved = new ReactiveVar(false);
|
||||
|
||||
|
||||
this.autorun(() => {
|
||||
const data = this.data();
|
||||
if (data && data.entityId && data.entityType) {
|
||||
|
|
@ -24,9 +24,9 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
|
||||
loadOriginalPosition(entityId, entityType) {
|
||||
this.isLoading.set(true);
|
||||
|
||||
|
||||
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
|
||||
|
||||
|
||||
Meteor.call(methodName, entityId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
if (error) {
|
||||
|
|
@ -34,7 +34,7 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
this.originalPosition.set(null);
|
||||
} else {
|
||||
this.originalPosition.set(result);
|
||||
|
||||
|
||||
// Check if the entity has moved
|
||||
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
|
||||
|
|
@ -61,11 +61,11 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
getOriginalPositionDescription() {
|
||||
const position = this.getOriginalPosition();
|
||||
if (!position) return 'No original position data';
|
||||
|
||||
|
||||
if (position.originalPosition) {
|
||||
const entityType = this.data().entityType;
|
||||
let description = `Original position: ${position.originalPosition.sort || 0}`;
|
||||
|
||||
|
||||
if (entityType === 'list' && position.originalSwimlaneId) {
|
||||
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||
} else if (entityType === 'card') {
|
||||
|
|
@ -76,10 +76,10 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
description += ` in list ${position.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
return 'No original position data';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ template(name="datepicker")
|
|||
.fields
|
||||
.left
|
||||
label(for="date") {{_ 'date'}}
|
||||
input.js-date-field#date(type="date" name="date" value=showDate autofocus)
|
||||
input.js-date-field#date(type="text" name="date" value=showDate autofocus placeholder=dateFormat)
|
||||
.right
|
||||
label(for="time") {{_ 'time'}}
|
||||
input.js-time-field#time(type="time" name="time" value=showTime)
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ textarea.editor {
|
|||
}
|
||||
input[type="submit"],
|
||||
button {
|
||||
background: #000;
|
||||
background: linear-gradient(#000, #000);
|
||||
background: #cfcfcf;
|
||||
background: linear-gradient(#cfcfcf, #c2c2c2);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
|
|
@ -139,7 +139,6 @@ button {
|
|||
line-height: 1.3;
|
||||
padding: 1vh 2.5vw;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"] .wide,
|
||||
button .wide {
|
||||
|
|
@ -150,16 +149,14 @@ input[type="submit"]:hover,
|
|||
button:hover,
|
||||
input[type="submit"]:focus,
|
||||
button:focus {
|
||||
background: #222;
|
||||
background: linear-gradient(#222, #222);
|
||||
color: #fff;
|
||||
background: #c2c2c2;
|
||||
background: linear-gradient(#c2c2c2, #b5b5b5);
|
||||
}
|
||||
input[type="submit"]:active,
|
||||
button:active {
|
||||
background: #111;
|
||||
background: linear-gradient(#111, #111);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.3);
|
||||
color: #fff;
|
||||
background: #b5b5b5;
|
||||
background: linear-gradient(#b5b5b5, #a8a8a8);
|
||||
box-shadow: inset 0 3px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
input[type="submit"]:active:hover,
|
||||
button:active:hover,
|
||||
|
|
@ -186,12 +183,6 @@ input[type="submit"].primary:active,
|
|||
button.primary:active {
|
||||
background: #01628c;
|
||||
}
|
||||
input[type="submit"].negate,
|
||||
button.negate {
|
||||
background: #eb5a46;
|
||||
box-shadow: 0 1px 0 #4d4d4d;
|
||||
color: #fff;
|
||||
}
|
||||
input[type="submit"].negate:hover,
|
||||
button.negate:hover,
|
||||
input[type="submit"].negate:focus,
|
||||
|
|
@ -226,10 +217,10 @@ input[type="submit"]:disabled:active,
|
|||
input[type="button"].disabled:active,
|
||||
button.disabled:active,
|
||||
.button.disabled:active {
|
||||
background: #555;
|
||||
background: #cfcfcf;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #999;
|
||||
color: #a8a8a8;
|
||||
}
|
||||
fieldset {
|
||||
border: 1px solid #bfbfbf;
|
||||
|
|
@ -324,18 +315,11 @@ textarea::-moz-placeholder {
|
|||
margin-right: 6px;
|
||||
border-top: 2px solid transparent;
|
||||
border-left: 2px solid transparent;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
transform: rotate(40deg);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
.button-link {
|
||||
background: #fff;
|
||||
background: linear-gradient(#fff, #f5f5f5);
|
||||
|
|
@ -409,12 +393,12 @@ body.grey-icons-enabled .materialCheckBox.is-checked {
|
|||
.button-link.setting.disabled.primary,
|
||||
.button-link.setting.disabled.primary:hover,
|
||||
.button-link.setting.disabled.primary:active {
|
||||
background: #555;
|
||||
border-color: #444;
|
||||
border-bottom-color: #333;
|
||||
background: #cfcfcf;
|
||||
border-color: #c2c2c2;
|
||||
border-bottom-color: #b5b5b5;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
color: #999;
|
||||
color: #a8a8a8;
|
||||
}
|
||||
.button-link.setting .label {
|
||||
color: #222;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 .. .}}
|
||||
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
template(name="ganttCard")
|
||||
+cardDetails(selectedCard)
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
template(name="importHeaderBar")
|
||||
h1
|
||||
a.back-btn(href="{{pathFor 'home'}}")
|
||||
i.fa-arrow-left
|
||||
i.fa.fa-chevron-left
|
||||
| {{_ title}}
|
||||
|
||||
template(name="import")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { trelloGetMembersToMap } from './trelloMembersMapper';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { wekanGetMembersToMap } from './wekanMembersMapper';
|
||||
import { csvGetMembersToMap } from './csvMembersMapper';
|
||||
import getSlug from 'limax';
|
||||
|
||||
const Papa = require('papaparse');
|
||||
|
||||
|
|
@ -347,7 +345,7 @@ BlazeComponent.extendComponent({
|
|||
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
|
||||
this.searchResults.set(results);
|
||||
this.searching.set(false);
|
||||
|
||||
|
||||
if (results.length === 0) {
|
||||
this.noResults.set(true);
|
||||
}
|
||||
|
|
@ -358,11 +356,11 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
'keyup .js-search-member-input'(event) {
|
||||
const query = event.target.value.trim();
|
||||
|
||||
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.performSearch(query);
|
||||
}, 300);
|
||||
|
|
|
|||
|
|
@ -161,66 +161,74 @@ body.list-resizing-active * {
|
|||
/* Use original display for consistent button positioning */
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
/* Allow overflow for text wrapping and forms */
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Clearfix for floated buttons */
|
||||
.list-header::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
/* Prevent vertical expansion but allow normal height */
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Ensure title text doesn't cause height changes for all lists */
|
||||
.list-header .list-header-name {
|
||||
/* Allow text wrapping to flow below buttons */
|
||||
white-space: normal !important;
|
||||
/* Prevent text wrapping to maintain consistent height */
|
||||
white-space: nowrap !important;
|
||||
/* Truncate text with ellipsis if too long */
|
||||
text-overflow: ellipsis !important;
|
||||
/* Ensure proper line height */
|
||||
line-height: 1.2 !important;
|
||||
/* Ensure it doesn't overflow horizontally */
|
||||
overflow-wrap: break-word !important;
|
||||
word-wrap: break-word !important;
|
||||
/* Full width since buttons are now absolutely positioned above */
|
||||
width: 100% !important;
|
||||
/* Ensure it doesn't overflow */
|
||||
overflow: hidden !important;
|
||||
/* Add margin to prevent overlap with buttons */
|
||||
margin-right: 120px !important;
|
||||
}
|
||||
|
||||
/* Position elements at top aligned with collapse button */
|
||||
.list-header .js-open-list-menu {
|
||||
/* Position drag handle at top-right corner for ALL lists */
|
||||
.list-header .list-header-handle {
|
||||
/* Position at top-right corner, aligned with title text top */
|
||||
position: absolute !important;
|
||||
top: 5px !important;
|
||||
right: 10px !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.list-header .list-header-plus-top {
|
||||
position: absolute !important;
|
||||
top: 5px !important;
|
||||
right: 30px !important;
|
||||
z-index: 15 !important;
|
||||
display: inline-block !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.list-header .list-header-handle-desktop {
|
||||
position: absolute !important;
|
||||
top: 5px !important;
|
||||
right: 80px !important;
|
||||
top: 2.5vh !important;
|
||||
right: 1.5vw !important;
|
||||
/* Ensure it's above other elements */
|
||||
z-index: 15 !important;
|
||||
/* Remove margin since it's absolutely positioned */
|
||||
margin-right: 0 !important;
|
||||
/* Ensure proper display */
|
||||
display: inline-block !important;
|
||||
/* Ensure it's clickable and shows proper cursor */
|
||||
cursor: move !important;
|
||||
pointer-events: auto !important;
|
||||
/* Add some padding for better clickability */
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
/* Anchor header action buttons within header during resize */
|
||||
.list .list-header { position: relative; z-index: 5; }
|
||||
.list .list-header .js-open-list-menu,
|
||||
.list .list-header .list-header-plus-top,
|
||||
.list .list-header .list-header-handle-desktop {
|
||||
position: absolute !important;
|
||||
/* Ensure buttons maintain original positioning */
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .list-header-plus-top,
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .js-collapse,
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .js-open-list-menu,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .list-header-plus-top,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .js-collapse,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .js-open-list-menu,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-plus-top,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-collapse,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-open-list-menu {
|
||||
/* Use original positioning to maintain layout */
|
||||
position: relative !important;
|
||||
/* Maintain original spacing */
|
||||
margin-right: 15px !important;
|
||||
/* Ensure proper display */
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Ensure watch icon and card count maintain original positioning */
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .list-header-watch-icon,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .list-header-watch-icon,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-watch-icon,
|
||||
.js-swimlane .list[style*="--list-width"] .list-header .cardCount,
|
||||
.dragscroll .list[style*="--list-width"] .list-header .cardCount,
|
||||
[id^="swimlane-"] .list[style*="--list-width"] .list-header .cardCount {
|
||||
/* Use original positioning to maintain layout */
|
||||
position: relative !important;
|
||||
/* Maintain original spacing */
|
||||
margin-right: 15px !important;
|
||||
/* Ensure proper display */
|
||||
display: inline-block !important;
|
||||
}
|
||||
[id^="swimlane-"] .list:first-child {
|
||||
min-width: 2.5vw;
|
||||
|
|
@ -251,61 +259,36 @@ body.list-resizing-active * {
|
|||
}
|
||||
.list.list-collapsed {
|
||||
flex: none;
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
width: 60px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
padding: 5px 0;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
padding: 1vh 1.5vw 0.5vh;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
overflow: visible !important;
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 60px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
position: relative !important;
|
||||
left: -10px !important;
|
||||
margin: 5px auto;
|
||||
margin: 0 auto 20px auto;
|
||||
z-index: 10;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
width: auto;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-header-handle {
|
||||
position: static !important;
|
||||
margin: 5px auto;
|
||||
z-index: 10;
|
||||
padding: 5px;
|
||||
display: block;
|
||||
width: auto;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header .list-header-handle-desktop {
|
||||
position: static !important;
|
||||
margin: 5px auto;
|
||||
z-index: 10;
|
||||
padding: 5px;
|
||||
display: block;
|
||||
width: auto;
|
||||
top: auto !important;
|
||||
right: auto !important;
|
||||
width: fit-content;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
|
|
@ -313,43 +296,31 @@ body.list-resizing-active * {
|
|||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
transform: rotate(90deg);
|
||||
transform-origin: center center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
display: block !important;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: #333;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 4px;
|
||||
border-radius: 4px;
|
||||
margin: 0 auto;
|
||||
width: 25vh;
|
||||
height: 60vh;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
z-index: 10;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.list.list-composer,
|
||||
.list-composer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show list-composer when inside an active inlined form */
|
||||
form.inlined-form .list-composer {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.list.list-composer .open-list-composer,
|
||||
|
|
@ -386,29 +357,16 @@ form.inlined-form .list-composer {
|
|||
display: none;
|
||||
}
|
||||
.list-header .list-header-name {
|
||||
display: block;
|
||||
display: inline;
|
||||
font-size: clamp(14px, 3vw, 18px);
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
min-height: 1.2vh;
|
||||
min-width: 4vw;
|
||||
overflow-wrap: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
}
|
||||
/* Sum badge shown before list title */
|
||||
.list-header .list-sum-badge {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #8c8c8c;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.list-rotated {
|
||||
width: 1.3vw;
|
||||
|
|
@ -420,6 +378,9 @@ form.inlined-form .list-composer {
|
|||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.list-header .list-rotated {
|
||||
|
||||
}
|
||||
.list-header .list-header-watch-icon {
|
||||
padding-left: 10px;
|
||||
|
|
@ -437,8 +398,6 @@ form.inlined-form .list-composer {
|
|||
.list-header .list-header-plus-top {
|
||||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list-header .list-header-collapse-right {
|
||||
color: #a6a6a6;
|
||||
|
|
@ -447,194 +406,151 @@ form.inlined-form .list-composer {
|
|||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
}
|
||||
/* List header collapse button styling - positioned at top left */
|
||||
.list-header .js-collapse {
|
||||
position: absolute !important;
|
||||
top: 5px !important;
|
||||
left: 10px !important;
|
||||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
vertical-align: middle;
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
z-index: 15;
|
||||
font-size: 14px;
|
||||
}
|
||||
.list-header .js-collapse:hover {
|
||||
background-color: transparent;
|
||||
background-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Title text container - full width below buttons */
|
||||
.list-header > div {
|
||||
padding-top: 25px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Hide menu button in collapsed state */
|
||||
.list.list-collapsed .list-header .js-open-list-menu,
|
||||
.list.list-collapsed .list-header .list-header-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for collapsed lists */
|
||||
@media (min-width: 768px) {
|
||||
.list.list-collapsed {
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
width: 60px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
margin: 0;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: 60px;
|
||||
margin: 0 auto;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
transform: rotate(90deg);
|
||||
flex: 1;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: auto;
|
||||
width: 15vh;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 5px auto;
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.list.list-collapsed {
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
transform: rotate(90deg);
|
||||
flex: 1;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: auto;
|
||||
width: 15vh;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 5px auto;
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.list.list-collapsed {
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
min-height: 60vh;
|
||||
height: 60vh;
|
||||
}
|
||||
.list.list-collapsed .list-header {
|
||||
width: 30px;
|
||||
max-width: 30px;
|
||||
min-height: 100% !important;
|
||||
height: 100% !important;
|
||||
min-height: 2.5vh !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 20px 0 0 0 !important;
|
||||
position: relative !important;
|
||||
transform: rotate(90deg);
|
||||
flex: 1;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: auto;
|
||||
width: 15vh;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 5px auto;
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
}
|
||||
.list-header .list-header-collapse {
|
||||
|
|
@ -657,8 +573,6 @@ form.inlined-form .list-composer {
|
|||
}
|
||||
.js-open-list-menu {
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.list-body {
|
||||
flex: 1 1 auto;
|
||||
|
|
@ -730,22 +644,17 @@ form.inlined-form .list-composer {
|
|||
.mini-list.mobile-view {
|
||||
flex: 0 0 60px;
|
||||
height: auto;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.mobile-view {
|
||||
display: block !important;
|
||||
display: contents;
|
||||
flex-basis: auto;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.list.mobile-view:first-child {
|
||||
margin-left: 0px;
|
||||
|
|
@ -753,11 +662,9 @@ form.inlined-form .list-composer {
|
|||
.list.mobile-view.ui-sortable-helper {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.mobile-view.ui-sortable-helper .list-header.ui-sortable-handle {
|
||||
cursor: grabbing;
|
||||
|
|
@ -765,17 +672,14 @@ form.inlined-form .list-composer {
|
|||
.list.mobile-view.placeholder {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.mobile-view .list-body {
|
||||
padding: 15px 19px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.list.mobile-view .list-header {
|
||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||
|
|
@ -784,9 +688,8 @@ form.inlined-form .list-composer {
|
|||
min-height: 30px;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
/* Force grid layout for iPhone */
|
||||
display: grid !important;
|
||||
grid-template-columns: 30px 1fr auto auto !important;
|
||||
|
|
@ -839,9 +742,6 @@ form.inlined-form .list-composer {
|
|||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
text-align: left;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
|
@ -870,22 +770,17 @@ form.inlined-form .list-composer {
|
|||
.mini-list {
|
||||
flex: 0 0 60px;
|
||||
height: auto;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list {
|
||||
display: block !important;
|
||||
display: contents;
|
||||
flex-basis: auto;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-left: 0px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.list:first-child {
|
||||
margin-left: 0px;
|
||||
|
|
@ -893,11 +788,9 @@ form.inlined-form .list-composer {
|
|||
.list.ui-sortable-helper {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list.ui-sortable-helper .list-header.ui-sortable-handle {
|
||||
cursor: grabbing;
|
||||
|
|
@ -905,17 +798,14 @@ form.inlined-form .list-composer {
|
|||
.list.placeholder {
|
||||
flex: 0 0 60px;
|
||||
height: 60px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
border-left: 0px !important;
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: block !important;
|
||||
}
|
||||
.list-body {
|
||||
padding: 15px 19px;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.list-header {
|
||||
/*Updated padding values for mobile devices, this should fix text grouping issue*/
|
||||
|
|
@ -924,9 +814,8 @@ form.inlined-form .list-composer {
|
|||
min-height: 30px;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
min-width: 100vw;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.list-header .list-header-left-icon {
|
||||
padding: 7px;
|
||||
|
|
@ -1056,9 +945,6 @@ form.inlined-form .list-composer {
|
|||
grid-row: 2 !important;
|
||||
grid-column: 2 !important;
|
||||
align-self: start !important;
|
||||
text-align: left !important;
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
|
@ -1130,23 +1016,6 @@ form.inlined-form .list-composer {
|
|||
grid-row: 1/3 !important;
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
|
||||
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header .list-header-name {
|
||||
/* Permit wrapping and full visibility */
|
||||
white-space: normal !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
display: block !important;
|
||||
/* Full width since buttons are absolutely positioned */
|
||||
width: 100% !important;
|
||||
/* Break long words to avoid overflow */
|
||||
word-break: break-word !important;
|
||||
}
|
||||
.link-board-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
@ -1236,48 +1105,3 @@ form.inlined-form .list-composer {
|
|||
.list-header-indigo {
|
||||
border-bottom: 6px solid #4b0082;
|
||||
}
|
||||
|
||||
.list.list-collapsed .collapsed-list-drag-area {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
.list.list-collapsed .collapsed-list-drag-area:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.list.list-collapsed .list-header-name-collapsed {
|
||||
writing-mode: vertical-rl;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
position: relative !important;
|
||||
left: -10px !important;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: block !important;
|
||||
align-items: initial;
|
||||
justify-content: initial;
|
||||
font-size: 16px !important;
|
||||
box-shadow: none;
|
||||
margin: 5px auto;
|
||||
z-index: 10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ template(name='list')
|
|||
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
|
||||
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
unless collapsed
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
|
||||
template(name='miniList')
|
||||
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ BlazeComponent.extendComponent({
|
|||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
if (!list) return 270; // Return default width if list is not available
|
||||
|
||||
|
||||
if (user) {
|
||||
// For logged-in users, get from user profile
|
||||
return user.getListWidthFromStorage(list.boardId, list._id);
|
||||
|
|
@ -223,7 +223,7 @@ BlazeComponent.extendComponent({
|
|||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
if (!list) return 550; // Return default constraint if list is not available
|
||||
|
||||
|
||||
if (user) {
|
||||
// For logged-in users, get from user profile
|
||||
return user.getListConstraintFromStorage(list.boardId, list._id);
|
||||
|
|
@ -260,11 +260,11 @@ BlazeComponent.extendComponent({
|
|||
console.warn('No current template data available for list resize initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const list = Template.currentData();
|
||||
const $list = this.$('.js-list');
|
||||
const $resizeHandle = this.$('.js-list-resize-handle');
|
||||
|
||||
|
||||
// Check if elements exist
|
||||
if (!$list.length || !$resizeHandle.length) {
|
||||
console.warn('List or resize handle not found, retrying in 100ms');
|
||||
|
|
@ -275,22 +275,20 @@ BlazeComponent.extendComponent({
|
|||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reactively show/hide resize handle based on collapse and auto-width state
|
||||
this.autorun(() => {
|
||||
const isAutoWidth = this.autoWidth();
|
||||
const isCollapsed = Utils.getListCollapseState(list);
|
||||
if (isCollapsed || isAutoWidth) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Only enable resize for non-collapsed, non-auto-width lists
|
||||
const isAutoWidth = this.autoWidth();
|
||||
if (list.collapsed || isAutoWidth) {
|
||||
$resizeHandle.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
let isResizing = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
let minWidth = 270; // Minimum width matching system default
|
||||
let minWidth = 100; // Minimum width as defined in the existing code
|
||||
let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
|
||||
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
|
||||
const component = this; // Store reference to component for use in event handlers
|
||||
|
||||
|
|
@ -298,16 +296,16 @@ BlazeComponent.extendComponent({
|
|||
isResizing = true;
|
||||
startX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
startWidth = $list.outerWidth();
|
||||
|
||||
|
||||
|
||||
|
||||
// Add visual feedback
|
||||
$list.addClass('list-resizing');
|
||||
$('body').addClass('list-resizing-active');
|
||||
|
||||
|
||||
|
||||
|
||||
// Prevent text selection during resize
|
||||
$('body').css('user-select', 'none');
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
|
@ -316,11 +314,11 @@ BlazeComponent.extendComponent({
|
|||
if (!isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const newWidth = Math.max(minWidth, startWidth + deltaX);
|
||||
|
||||
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
|
||||
|
||||
// Apply the new width immediately for real-time feedback
|
||||
$list[0].style.setProperty('--list-width', `${newWidth}px`);
|
||||
$list[0].style.setProperty('width', `${newWidth}px`);
|
||||
|
|
@ -330,22 +328,22 @@ BlazeComponent.extendComponent({
|
|||
$list[0].style.setProperty('flex-basis', 'auto');
|
||||
$list[0].style.setProperty('flex-grow', '0');
|
||||
$list[0].style.setProperty('flex-shrink', '0');
|
||||
|
||||
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const stopResize = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
|
||||
isResizing = false;
|
||||
|
||||
|
||||
// Calculate final width
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const finalWidth = Math.max(minWidth, startWidth + deltaX);
|
||||
|
||||
const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
|
||||
|
||||
// Ensure the final width is applied
|
||||
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
|
||||
$list[0].style.setProperty('width', `${finalWidth}px`);
|
||||
|
|
@ -355,23 +353,23 @@ BlazeComponent.extendComponent({
|
|||
$list[0].style.setProperty('flex-basis', 'auto');
|
||||
$list[0].style.setProperty('flex-grow', '0');
|
||||
$list[0].style.setProperty('flex-shrink', '0');
|
||||
|
||||
|
||||
// Remove visual feedback but keep the width
|
||||
$list.removeClass('list-resizing');
|
||||
$('body').removeClass('list-resizing-active');
|
||||
$('body').css('user-select', '');
|
||||
|
||||
|
||||
// Keep the CSS custom property for persistent width
|
||||
// The CSS custom property will remain on the element to maintain the width
|
||||
|
||||
|
||||
// Save the new width using the existing system
|
||||
const boardId = list.boardId;
|
||||
const listId = list._id;
|
||||
|
||||
|
||||
// Use the new storage method that handles both logged-in and non-logged-in users
|
||||
if (process.env.DEBUG === 'true') {
|
||||
}
|
||||
|
||||
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
// For logged-in users, use server method
|
||||
|
|
@ -389,32 +387,32 @@ BlazeComponent.extendComponent({
|
|||
// Save list width
|
||||
const storedWidths = localStorage.getItem('wekan-list-widths');
|
||||
let widths = storedWidths ? JSON.parse(storedWidths) : {};
|
||||
|
||||
|
||||
if (!widths[boardId]) {
|
||||
widths[boardId] = {};
|
||||
}
|
||||
widths[boardId][listId] = finalWidth;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
|
||||
|
||||
|
||||
// Save list constraint
|
||||
const storedConstraints = localStorage.getItem('wekan-list-constraints');
|
||||
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
|
||||
|
||||
|
||||
if (!constraints[boardId]) {
|
||||
constraints[boardId] = {};
|
||||
}
|
||||
constraints[boardId][listId] = listConstraint;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error saving list width/constraint to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
|
|
@ -422,23 +420,22 @@ BlazeComponent.extendComponent({
|
|||
$resizeHandle.on('mousedown', startResize);
|
||||
$(document).on('mousemove', doResize);
|
||||
$(document).on('mouseup', stopResize);
|
||||
|
||||
|
||||
// Touch events for mobile
|
||||
$resizeHandle.on('touchstart', startResize, { passive: false });
|
||||
$(document).on('touchmove', doResize, { passive: false });
|
||||
$(document).on('touchend', stopResize, { passive: false });
|
||||
|
||||
|
||||
|
||||
|
||||
// Prevent dragscroll interference
|
||||
$resizeHandle.on('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
|
||||
// Reactively update resize handle visibility when auto-width or collapse changes
|
||||
|
||||
|
||||
// Reactively update resize handle visibility when auto-width changes
|
||||
component.autorun(() => {
|
||||
const collapsed = Utils.getListCollapseState(list);
|
||||
if (component.autoWidth() || collapsed) {
|
||||
if (component.autoWidth()) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
|
|
@ -455,28 +452,9 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('list');
|
||||
|
||||
Template.list.helpers({
|
||||
collapsed() {
|
||||
return Utils.getListCollapseState(this);
|
||||
},
|
||||
});
|
||||
|
||||
Template.miniList.events({
|
||||
'click .js-select-list'() {
|
||||
const listId = this._id;
|
||||
Session.set('currentList', listId);
|
||||
},
|
||||
});
|
||||
|
||||
// Enable drag-reorder for collapsed lists from .js-collapsed-list-drag area
|
||||
this.$('.js-collapsed-list-drag').draggable({
|
||||
axis: 'x',
|
||||
helper: 'clone',
|
||||
revert: 'invalid',
|
||||
start(evt, ui) {
|
||||
boardComponent.setIsDragging(true);
|
||||
},
|
||||
stop(evt, ui) {
|
||||
boardComponent.setIsDragging(false);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ template(name="listBody")
|
|||
unless collapsed
|
||||
.list-body(class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
|
||||
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
if cards.length
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
|
|
@ -25,15 +26,13 @@ template(name="listBody")
|
|||
+minicard(this)
|
||||
if (showSpinner (idOrNull ../../_id))
|
||||
+spinnerList
|
||||
|
||||
if canSeeAddCard
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
else
|
||||
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-card'}}
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
| ➕
|
||||
|
||||
template(name="spinnerList")
|
||||
.sk-spinner.sk-spinner-list(
|
||||
|
|
@ -55,8 +54,7 @@ template(name="addCardForm")
|
|||
|
||||
.add-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
a.js-close-inlined-form
|
||||
i.fa.fa-times-thin
|
||||
a.js-close-inlined-form | ❌
|
||||
.add-controls.clearfix
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
|
|
@ -87,19 +85,16 @@ template(name="linkCardPopup")
|
|||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each swimlanes
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each lists
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each cards
|
||||
option(value="{{getRealId}}") {{getTitle}}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { Spinner } from '/client/lib/spinner';
|
||||
import getSlug from 'limax';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const InfiniteScrollIter = 10;
|
||||
|
|
@ -18,50 +16,11 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
customFieldsSum() {
|
||||
const list = Template.currentData();
|
||||
if (!list) return [];
|
||||
const boardId = Session.get('currentBoard');
|
||||
const fields = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [boardId] },
|
||||
const ret = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [Session.get('currentBoard')] },
|
||||
showSumAtTopOfList: true,
|
||||
});
|
||||
|
||||
if (!fields || !fields.length) return [];
|
||||
|
||||
const cards = ReactiveCache.getCards({
|
||||
listId: list._id,
|
||||
archived: false,
|
||||
});
|
||||
|
||||
const result = fields.map(field => {
|
||||
let sum = 0;
|
||||
if (cards && cards.length) {
|
||||
cards.forEach(card => {
|
||||
const cfs = (card.customFields || []);
|
||||
const cf = cfs.find(f => f && f._id === field._id);
|
||||
if (!cf || cf.value === null || cf.value === undefined) return;
|
||||
let v = cf.value;
|
||||
if (typeof v === 'string') {
|
||||
// try to parse string numbers, accept comma decimal
|
||||
const parsed = parseFloat(v.replace(',', '.'));
|
||||
if (isNaN(parsed)) return;
|
||||
v = parsed;
|
||||
}
|
||||
if (typeof v === 'number' && isFinite(v)) {
|
||||
sum += v;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
_id: field._id,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
settings: field.settings || {},
|
||||
value: sum,
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
return ret;
|
||||
},
|
||||
|
||||
openForm(options) {
|
||||
|
|
@ -210,12 +169,6 @@ BlazeComponent.extendComponent({
|
|||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
Utils.goBoardId(Session.get('currentBoard'));
|
||||
} else {
|
||||
// Allow normal href navigation, but if it's the same card URL,
|
||||
// we'll handle it by directly setting the session
|
||||
evt.preventDefault();
|
||||
const card = this.currentData();
|
||||
Session.set('currentCard', card._id);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -301,22 +254,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('listBody');
|
||||
|
||||
// Helpers for listBody template context
|
||||
Template.listBody.helpers({
|
||||
formattedCurrencyCustomFieldValue(val) {
|
||||
// `this` is the custom field sum object from customFieldsSum each-iteration
|
||||
const field = this || {};
|
||||
const code = (field.settings && field.settings.currencyCode) || 'USD';
|
||||
try {
|
||||
const n = typeof val === 'number' ? val : parseFloat(val);
|
||||
if (!isFinite(n)) return val;
|
||||
return new Intl.NumberFormat(undefined, { style: 'currency', currency: code }).format(n);
|
||||
} catch (e) {
|
||||
return `${code} ${val}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function toggleValueInReactiveArray(reactiveValue, value) {
|
||||
const array = reactiveValue.get();
|
||||
const valueIndex = array.indexOf(value);
|
||||
|
|
@ -539,10 +476,10 @@ BlazeComponent.extendComponent({
|
|||
if (!board) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// Ensure default swimlane exists
|
||||
board.getDefaultSwimline();
|
||||
|
||||
|
||||
const swimlanes = ReactiveCache.getSwimlanes(
|
||||
{
|
||||
boardId: this.selectedBoardId.get()
|
||||
|
|
@ -550,6 +487,8 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
if (swimlanes.length)
|
||||
this.selectedSwimlaneId.set(swimlanes[0]._id);
|
||||
return swimlanes;
|
||||
},
|
||||
|
||||
|
|
@ -564,6 +503,7 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
if (lists.length) this.selectedListId.set(lists[0]._id);
|
||||
return lists;
|
||||
},
|
||||
|
||||
|
|
@ -572,17 +512,19 @@ BlazeComponent.extendComponent({
|
|||
return [];
|
||||
}
|
||||
const ownCardsIds = this.board.cards().map(card => card.getRealId());
|
||||
const selector = {
|
||||
const ret = ReactiveCache.getCards(
|
||||
{
|
||||
boardId: this.selectedBoardId.get(),
|
||||
swimlaneId: this.selectedSwimlaneId.get(),
|
||||
listId: this.selectedListId.get(),
|
||||
archived: false,
|
||||
linkedId: { $nin: ownCardsIds },
|
||||
_id: { $nin: ownCardsIds },
|
||||
type: { $nin: ['template-card'] },
|
||||
};
|
||||
if (this.selectedBoardId.get()) selector.boardId = this.selectedBoardId.get();
|
||||
if (this.selectedSwimlaneId.get()) selector.swimlaneId = this.selectedSwimlaneId.get();
|
||||
if (this.selectedListId.get()) selector.listId = this.selectedListId.get();
|
||||
|
||||
const ret = ReactiveCache.getCards(selector, { sort: { sort: 1 } });
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 },
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
|
||||
|
|
@ -603,12 +545,8 @@ BlazeComponent.extendComponent({
|
|||
return [
|
||||
{
|
||||
'change .js-select-boards'(evt) {
|
||||
const val = $(evt.currentTarget).val();
|
||||
subManager.subscribe('board', val, false);
|
||||
// Clear selections to allow linking only board or re-choose swimlane/list
|
||||
this.selectedSwimlaneId.set('');
|
||||
this.selectedListId.set('');
|
||||
this.selectedBoardId.set(val);
|
||||
subManager.subscribe('board', $(evt.currentTarget).val(), false);
|
||||
this.selectedBoardId.set($(evt.currentTarget).val());
|
||||
},
|
||||
'change .js-select-swimlanes'(evt) {
|
||||
this.selectedSwimlaneId.set($(evt.currentTarget).val());
|
||||
|
|
@ -817,7 +755,7 @@ BlazeComponent.extendComponent({
|
|||
evt.preventDefault();
|
||||
this.term.set(evt.target.searchTerm.value);
|
||||
},
|
||||
async 'click .js-minicard'(evt) {
|
||||
'click .js-minicard'(evt) {
|
||||
// 0. Common
|
||||
const title = $('.js-element-title')
|
||||
.val()
|
||||
|
|
@ -835,7 +773,7 @@ BlazeComponent.extendComponent({
|
|||
if (this.isTemplateSearch) {
|
||||
element.type = 'cardType-card';
|
||||
element.linkedId = '';
|
||||
_id = await element.copy(this.boardId, this.swimlaneId, this.listId);
|
||||
_id = element.copy(this.boardId, this.swimlaneId, this.listId);
|
||||
// 1.B Linked card
|
||||
} else {
|
||||
_id = element.link(this.boardId, this.swimlaneId, this.listId);
|
||||
|
|
@ -847,13 +785,13 @@ BlazeComponent.extendComponent({
|
|||
.lists()
|
||||
.length;
|
||||
element.type = 'list';
|
||||
_id = await element.copy(this.boardId, this.swimlaneId);
|
||||
_id = element.copy(this.boardId, this.swimlaneId);
|
||||
} else if (this.isSwimlaneTemplateSearch) {
|
||||
element.sort = ReactiveCache.getBoard(this.boardId)
|
||||
.swimlanes()
|
||||
.length;
|
||||
element.type = 'swimlane';
|
||||
_id = await element.copy(this.boardId);
|
||||
_id = element.copy(this.boardId);
|
||||
} else if (this.isBoardTemplateSearch) {
|
||||
Meteor.call(
|
||||
'copyBoard',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ template(name="listHeader")
|
|||
if isMiniScreen
|
||||
if currentList
|
||||
a.list-header-left-icon.js-unselect-list
|
||||
i.fa.fa-caret-left
|
||||
| ◀️
|
||||
else
|
||||
if collapsed
|
||||
if showCardsCountForList cards.length
|
||||
|
|
@ -26,19 +26,15 @@ template(name="listHeader")
|
|||
|/#{wipLimit.value})
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
else
|
||||
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
|
||||
if collapsed
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
if collapsed
|
||||
a.js-collapse(title="{{_ 'uncollapse'}}")
|
||||
| ⬅️
|
||||
| ➡️
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}")
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
|
|
@ -48,54 +44,35 @@ template(name="listHeader")
|
|||
unless collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
if isMiniScreen
|
||||
if currentList
|
||||
if isWatching
|
||||
i.list-header-watch-icon i.fa.fa-eye
|
||||
i.list-header-watch-icon | 👁️
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||
else
|
||||
a.list-header-menu-icon.js-select-list
|
||||
i.fa.fa-caret-right
|
||||
unless currentUser.isWorker
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle.handle.js-list-handle
|
||||
i.fa.fa-arrows
|
||||
a.list-header-menu-icon.js-select-list ▶️
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
else if currentUser.isBoardMember
|
||||
if isWatching
|
||||
i.list-header-watch-icon i.fa.fa-eye
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
i.fa.fa-arrows
|
||||
i.list-header-watch-icon | 👁️
|
||||
unless collapsed
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
//if isBoardAdmin
|
||||
//
|
||||
a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
i.fa.fa-arrows
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
//if isBoardAdmin
|
||||
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||
a.js-collapse(title="{{_ 'collapse'}}")
|
||||
| ⬅️
|
||||
| ➡️
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
|
||||
template(name="editListTitleForm")
|
||||
.list-composer
|
||||
|
|
@ -103,78 +80,63 @@ template(name="editListTitleForm")
|
|||
.edit-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'save'}}
|
||||
a.js-close-inlined-form
|
||||
i.fa.fa-times-thin
|
||||
| ❌
|
||||
|
||||
template(name="listActionPopup")
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-add-card.list-header-plus-top
|
||||
i.fa.fa-plus
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'add-card-to-top-of-list'}}
|
||||
li
|
||||
a.js-add-card.list-header-plus-bottom
|
||||
i.fa.fa-plus
|
||||
i.fa.fa-arrow-down
|
||||
| {{_ 'add-card-to-bottom-of-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-add-list
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-list-width
|
||||
i.fa.fa-arrows-h
|
||||
| {{_ 'set-list-width'}}
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-add-card.list-header-plus-bottom
|
||||
| ➕
|
||||
| ⬇️
|
||||
| {{_ 'add-card-to-bottom-of-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-list-width
|
||||
| ↔️
|
||||
| {{_ 'set-list-width'}}
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-toggle-watch-list
|
||||
if isWatching
|
||||
i.fa.fa-eye
|
||||
| 👁️
|
||||
| {{_ 'unwatch'}}
|
||||
else
|
||||
i.fa.fa-eye-slash
|
||||
| 🙈
|
||||
| {{_ 'watch'}}
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-color-list
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'set-color-list'}}
|
||||
ul.pop-over-list
|
||||
if cards.length
|
||||
li
|
||||
a.js-select-cards
|
||||
i.fa.fa-select-square
|
||||
| {{_ 'list-select-cards'}}
|
||||
if currentUser.isBoardAdmin
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-wip-limit
|
||||
i.fa.fa-ban
|
||||
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
|
||||
unless currentUser.isWorker
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-close-list
|
||||
i.fa.fa-arrow-right
|
||||
i.fa.fa-archive
|
||||
| {{_ 'archive-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
i.fa.fa-link
|
||||
| {{_ 'listMorePopup-title'}}
|
||||
unless currentUser.isWorker
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-color-list
|
||||
| 🎨
|
||||
| {{_ 'set-color-list'}}
|
||||
ul.pop-over-list
|
||||
if cards.length
|
||||
li
|
||||
a.js-select-cards
|
||||
| ☑️
|
||||
| {{_ 'list-select-cards'}}
|
||||
if currentUser.isBoardAdmin
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-set-wip-limit
|
||||
| 🚫
|
||||
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
|
||||
unless currentUser.isWorker
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-close-list
|
||||
| ➡️
|
||||
| 📦
|
||||
| {{_ 'archive-list'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a.js-more
|
||||
| 🔗
|
||||
| {{_ 'listMorePopup-title'}}
|
||||
|
||||
template(name="boardLists")
|
||||
ul.pop-over-list
|
||||
|
|
@ -190,15 +152,13 @@ template(name="listMorePopup")
|
|||
span.clearfix
|
||||
span {{_ 'link-list'}}
|
||||
= ' '
|
||||
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
| {{#if board.isPublic}}🌐{{else}}🔒{{/if}}
|
||||
input.inline-input(type="text" readonly value="{{ rootUrl }}")
|
||||
| {{_ 'added'}}
|
||||
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
|
||||
//unless currentUser.isWorker
|
||||
//
|
||||
if currentUser.isBoardAdmin
|
||||
//
|
||||
a.js-delete {{_ 'delete'}}
|
||||
// if currentUser.isBoardAdmin
|
||||
// a.js-delete {{_ 'delete'}}
|
||||
|
||||
template(name="listDeletePopup")
|
||||
p {{_ "list-delete-pop"}}
|
||||
|
|
@ -212,7 +172,7 @@ template(name="setWipLimitPopup")
|
|||
ul.pop-over-list
|
||||
li: a.js-enable-wip-limit {{_ 'enable-wip-limit'}}
|
||||
if isWipLimitEnabled
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
if isWipLimitEnabled
|
||||
p
|
||||
input.wip-limit-value(type="number" value="{{ wipLimitValue }}" min="1" max="99")
|
||||
|
|
@ -233,8 +193,8 @@ template(name="setListWidthPopup")
|
|||
#js-list-width-edit
|
||||
label {{_ 'set-list-width-value'}}
|
||||
p
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="270")
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="100")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="100")
|
||||
input.list-width-apply(type="submit" value="{{_ 'apply'}}")
|
||||
input.list-width-error
|
||||
br
|
||||
|
|
@ -245,7 +205,7 @@ template(name="setListWidthPopup")
|
|||
|
||||
template(name="listWidthErrorPopup")
|
||||
.list-width-invalid
|
||||
p {{_ 'list-width-error-message'}} '>=270'
|
||||
p {{_ 'list-width-error-message'}} '>=100'
|
||||
button.full.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
|
||||
template(name="setListColorPopup")
|
||||
|
|
@ -254,29 +214,6 @@ template(name="setListColorPopup")
|
|||
// note: we use the swimlane palette to have more than just the border
|
||||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.js-remove-color.negate.wide.right {{_ 'unset-color'}}
|
||||
|
||||
template(name="addListPopup")
|
||||
form.js-add-list-form
|
||||
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}" autocomplete="off" autofocus)
|
||||
if currentSwimlaneData
|
||||
if swimlaneLists.length
|
||||
label {{_ 'add-after-list'}}
|
||||
select.list-position-input.full-line
|
||||
each swimlaneLists
|
||||
option(value="{{_id}}" selected="{{$eq _id currentListIdValue}}") {{increment @index}} {{title}}
|
||||
else
|
||||
if currentBoard.lists.length
|
||||
label {{_ 'add-after-list'}}
|
||||
select.list-position-input.full-line
|
||||
each currentBoard.lists
|
||||
option(value="{{_id}}" selected="{{$eq _id currentListIdValue}}") {{increment @index}} {{title}}
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-list(type="submit") {{_ 'save'}}
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-list-template {{_ 'template'}}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import Lists from '../../../models/lists';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
|
||||
|
|
@ -22,37 +21,36 @@ BlazeComponent.extendComponent({
|
|||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
async starred(check = undefined) {
|
||||
starred(check = undefined) {
|
||||
const list = Template.currentData();
|
||||
const status = list.isStarred();
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
await list.star(!status);
|
||||
list.star(!status);
|
||||
return !status;
|
||||
}
|
||||
},
|
||||
collapsed(check = undefined) {
|
||||
const list = Template.currentData();
|
||||
const status = Utils.getListCollapseState(list);
|
||||
const status = list.isCollapsed();
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
const next = typeof check === 'boolean' ? check : !status;
|
||||
Utils.setListCollapseState(list, next);
|
||||
return next;
|
||||
list.collapse(!status);
|
||||
return !status;
|
||||
}
|
||||
},
|
||||
async editTitle(event) {
|
||||
editTitle(event) {
|
||||
event.preventDefault();
|
||||
const newTitle = this.childComponents('inlinedForm')[0]
|
||||
.getValue()
|
||||
.trim();
|
||||
const list = this.currentData();
|
||||
if (newTitle) {
|
||||
await list.rename(newTitle.trim());
|
||||
list.rename(newTitle.trim());
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -144,48 +142,7 @@ BlazeComponent.extendComponent({
|
|||
Template.listHeader.helpers({
|
||||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
numberFieldsSum() {
|
||||
const list = Template.currentData();
|
||||
if (!list) return 0;
|
||||
const boardId = Session.get('currentBoard');
|
||||
const fields = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [boardId] },
|
||||
showSumAtTopOfList: true,
|
||||
type: 'number',
|
||||
});
|
||||
if (!fields || !fields.length) return 0;
|
||||
const cards = ReactiveCache.getCards({ listId: list._id, archived: false });
|
||||
let total = 0;
|
||||
if (cards && cards.length) {
|
||||
cards.forEach(card => {
|
||||
const cfs = (card.customFields || []);
|
||||
fields.forEach(field => {
|
||||
const cf = cfs.find(f => f && f._id === field._id);
|
||||
if (!cf || cf.value === null || cf.value === undefined) return;
|
||||
let v = cf.value;
|
||||
if (typeof v === 'string') {
|
||||
const parsed = parseFloat(v.replace(',', '.'));
|
||||
if (isNaN(parsed)) return;
|
||||
v = parsed;
|
||||
}
|
||||
if (typeof v === 'number' && isFinite(v)) {
|
||||
total += v;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return total;
|
||||
},
|
||||
hasNumberFieldsSum() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const fields = ReactiveCache.getCustomFields({
|
||||
boardIds: { $in: [boardId] },
|
||||
showSumAtTopOfList: true,
|
||||
type: 'number',
|
||||
});
|
||||
return !!(fields && fields.length);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
Template.listActionPopup.helpers({
|
||||
|
|
@ -204,27 +161,14 @@ Template.listActionPopup.helpers({
|
|||
|
||||
Template.listActionPopup.events({
|
||||
'click .js-list-subscribe'() {},
|
||||
'click .js-add-card.list-header-plus-top'(event) {
|
||||
const listDom = $(`#js-list-${this._id}`)[0];
|
||||
const listComponent = BlazeComponent.getComponentForElement(listDom);
|
||||
if (listComponent) {
|
||||
listComponent.openForm({
|
||||
position: 'top',
|
||||
});
|
||||
}
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-card.list-header-plus-bottom'(event) {
|
||||
const listDom = $(`#js-list-${this._id}`)[0];
|
||||
const listComponent = BlazeComponent.getComponentForElement(listDom);
|
||||
if (listComponent) {
|
||||
listComponent.openForm({
|
||||
position: 'bottom',
|
||||
});
|
||||
}
|
||||
listComponent.openForm({
|
||||
position: 'bottom',
|
||||
});
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-add-list': Popup.open('addList'),
|
||||
'click .js-set-list-width': Popup.open('setListWidth'),
|
||||
'click .js-set-color-list': Popup.open('setListColor'),
|
||||
'click .js-select-cards'() {
|
||||
|
|
@ -239,9 +183,9 @@ Template.listActionPopup.events({
|
|||
if (!err && ret) Popup.back();
|
||||
});
|
||||
},
|
||||
async 'click .js-close-list'(event) {
|
||||
'click .js-close-list'(event) {
|
||||
event.preventDefault();
|
||||
await this.archive();
|
||||
this.archive();
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-set-wip-limit': Popup.open('setWipLimit'),
|
||||
|
|
@ -268,26 +212,26 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
async enableSoftLimit() {
|
||||
enableSoftLimit() {
|
||||
const list = Template.currentData();
|
||||
|
||||
if (
|
||||
list.getWipLimit('soft') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
) {
|
||||
await list.setWipLimit(list.cards().length);
|
||||
list.setWipLimit(list.cards().length);
|
||||
}
|
||||
Meteor.call('enableSoftLimit', Template.currentData()._id);
|
||||
},
|
||||
|
||||
async enableWipLimit() {
|
||||
enableWipLimit() {
|
||||
const list = Template.currentData();
|
||||
// Prevent user from using previously stored wipLimit.value if it is less than the current number of cards in the list
|
||||
if (
|
||||
!list.getWipLimit('enabled') &&
|
||||
list.getWipLimit('value') < list.cards().length
|
||||
) {
|
||||
await list.setWipLimit(list.cards().length);
|
||||
list.setWipLimit(list.cards().length);
|
||||
}
|
||||
Meteor.call('enableWipLimit', list._id);
|
||||
},
|
||||
|
|
@ -381,12 +325,12 @@ BlazeComponent.extendComponent({
|
|||
'click .js-palette-color'() {
|
||||
this.currentColor.set(this.currentData().color);
|
||||
},
|
||||
async 'click .js-submit'() {
|
||||
await this.currentList.setColor(this.currentColor.get());
|
||||
'click .js-submit'() {
|
||||
this.currentList.setColor(this.currentColor.get());
|
||||
Popup.close();
|
||||
},
|
||||
async 'click .js-remove-color'() {
|
||||
await this.currentList.setColor(null);
|
||||
'click .js-remove-color'() {
|
||||
this.currentList.setColor(null);
|
||||
Popup.close();
|
||||
},
|
||||
},
|
||||
|
|
@ -412,7 +356,7 @@ BlazeComponent.extendComponent({
|
|||
);
|
||||
|
||||
// FIXME(mark-i-m): where do we put constants?
|
||||
if (width < 270 || !width || constraint < 270 || !constraint) {
|
||||
if (width < 100 || !width || constraint < 100 || !constraint) {
|
||||
Template.instance()
|
||||
.$('.list-width-error')
|
||||
.click();
|
||||
|
|
@ -453,108 +397,3 @@ BlazeComponent.extendComponent({
|
|||
];
|
||||
},
|
||||
}).register('setListWidthPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentBoard = Utils.getCurrentBoard();
|
||||
this.currentSwimlaneId = new ReactiveVar(null);
|
||||
this.currentListId = new ReactiveVar(null);
|
||||
|
||||
// Get the swimlane context from opener
|
||||
const openerData = Popup.getOpenerComponent()?.data();
|
||||
|
||||
// If opened from swimlane menu, openerData is the swimlane
|
||||
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
|
||||
this.currentSwimlane = openerData;
|
||||
this.currentSwimlaneId.set(openerData._id);
|
||||
} else if (openerData?._id) {
|
||||
// If opened from list menu, get swimlane from the list
|
||||
const list = ReactiveCache.getList({ _id: openerData._id });
|
||||
this.currentSwimlane = list?.swimlaneId ? ReactiveCache.getSwimlane({ _id: list.swimlaneId }) : null;
|
||||
this.currentSwimlaneId.set(this.currentSwimlane?._id || null);
|
||||
this.currentListId.set(openerData._id);
|
||||
}
|
||||
},
|
||||
|
||||
currentSwimlaneData() {
|
||||
const swimlaneId = this.currentSwimlaneId.get();
|
||||
return swimlaneId ? ReactiveCache.getSwimlane({ _id: swimlaneId }) : null;
|
||||
},
|
||||
|
||||
currentListIdValue() {
|
||||
return this.currentListId.get();
|
||||
},
|
||||
|
||||
swimlaneLists() {
|
||||
const swimlaneId = this.currentSwimlaneId.get();
|
||||
if (swimlaneId) {
|
||||
return ReactiveCache.getLists({ swimlaneId, archived: false }).sort((a, b) => a.sort - b.sort);
|
||||
}
|
||||
return this.currentBoard.lists;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .js-add-list-form'(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
const titleInput = this.find('.list-name-input');
|
||||
const title = titleInput?.value.trim();
|
||||
|
||||
if (!title) return;
|
||||
|
||||
let sortIndex = 0;
|
||||
const boardId = Utils.getCurrentBoardId();
|
||||
let swimlaneId = this.currentSwimlane?._id;
|
||||
|
||||
const positionInput = this.find('.list-position-input');
|
||||
|
||||
if (positionInput && positionInput.value) {
|
||||
const positionId = positionInput.value.trim();
|
||||
const selectedList = ReactiveCache.getList({ boardId, _id: positionId, archived: false });
|
||||
|
||||
if (selectedList) {
|
||||
sortIndex = selectedList.sort + 1;
|
||||
// Use the swimlane ID from the selected list to ensure the new list
|
||||
// is added to the same swimlane as the selected list
|
||||
swimlaneId = selectedList.swimlaneId;
|
||||
} else {
|
||||
// No specific position, add at end of swimlane
|
||||
if (swimlaneId) {
|
||||
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
|
||||
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
|
||||
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
|
||||
} else {
|
||||
const lastList = this.currentBoard.getLastList();
|
||||
sortIndex = Utils.calculateIndexData(lastList, null).base;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No position input, add at end of swimlane
|
||||
if (swimlaneId) {
|
||||
const swimlaneLists = ReactiveCache.getLists({ swimlaneId, archived: false });
|
||||
const lastSwimlaneList = swimlaneLists.sort((a, b) => b.sort - a.sort)[0];
|
||||
sortIndex = Utils.calculateIndexData(lastSwimlaneList, null).base;
|
||||
} else {
|
||||
const lastList = this.currentBoard.getLastList();
|
||||
sortIndex = Utils.calculateIndexData(lastList, null).base;
|
||||
}
|
||||
}
|
||||
|
||||
Lists.insert({
|
||||
title,
|
||||
boardId: Session.get('currentBoard'),
|
||||
sort: sortIndex,
|
||||
type: 'list',
|
||||
swimlaneId: swimlaneId,
|
||||
});
|
||||
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-list-template': Popup.open('searchElement'),
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('addListPopup');
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ template(name="minilist")
|
|||
class="minicard-{{colorClass}}")
|
||||
.minicard-title
|
||||
.handle
|
||||
span.drag-handle(title="{{_ 'dragList'}}")
|
||||
i.fa.fa-arrows
|
||||
.fa.fa-arrows
|
||||
+viewer
|
||||
= title
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ Template.bookmarks.helpers({
|
|||
});
|
||||
|
||||
Template.bookmarks.events({
|
||||
async 'click .js-toggle-star'(e) {
|
||||
'click .js-toggle-star'(e) {
|
||||
e.preventDefault();
|
||||
const boardId = this._id;
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && boardId) {
|
||||
await user.toggleBoardStar(boardId);
|
||||
user.toggleBoardStar(boardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -42,12 +42,12 @@ Template.bookmarksPopup.helpers({
|
|||
});
|
||||
|
||||
Template.bookmarksPopup.events({
|
||||
async 'click .js-toggle-star'(e) {
|
||||
'click .js-toggle-star'(e) {
|
||||
e.preventDefault();
|
||||
const boardId = this._id;
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && boardId) {
|
||||
await user.toggleBoardStar(boardId);
|
||||
user.toggleBoardStar(boardId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
template(name="dueCardsHeaderBar")
|
||||
if currentUser
|
||||
h1
|
||||
i.fa.fa-calendar
|
||||
| 📅
|
||||
| {{_ 'dueCards-title'}}
|
||||
|
||||
.board-header-btns.left
|
||||
a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}")
|
||||
i.fa.fa-caret-down
|
||||
| ▼
|
||||
if $eq dueCardsView 'me'
|
||||
i.fa.fa-user
|
||||
| 👤
|
||||
| {{_ 'dueCardsViewChange-choice-me'}}
|
||||
if $eq dueCardsView 'all'
|
||||
i.fa.fa-users
|
||||
| 👥
|
||||
| {{_ 'dueCardsViewChange-choice-all'}}
|
||||
|
||||
template(name="dueCardsModalTitle")
|
||||
if currentUser
|
||||
h2
|
||||
i.fa.fa-keyboard-o
|
||||
| ⌨️
|
||||
| {{_ 'dueCards-title'}}
|
||||
|
||||
template(name="dueCards")
|
||||
|
|
@ -49,18 +49,18 @@ template(name="dueCardsViewChangePopup")
|
|||
li
|
||||
with "dueCardsViewChange-choice-me"
|
||||
a.js-due-cards-view-me
|
||||
i.fa.fa-user
|
||||
| 👤
|
||||
| {{_ 'dueCardsViewChange-choice-me'}}
|
||||
if $eq Utils.dueCardsView "me"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
hr
|
||||
li
|
||||
with "dueCardsViewChange-choice-all"
|
||||
a.js-due-cards-view-all
|
||||
i.fa.fa-users
|
||||
| 👥
|
||||
| {{_ 'dueCardsViewChange-choice-all'}}
|
||||
span.sub-name
|
||||
+viewer
|
||||
| {{_ 'dueCardsViewChange-choice-all-description' }}
|
||||
if $eq Utils.dueCardsView "all"
|
||||
i.fa.fa-check
|
||||
| ✅
|
||||
|
|
|
|||
|
|
@ -92,14 +92,14 @@ BlazeComponent.extendComponent({
|
|||
class DueCardsComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
|
||||
this._cachedCards = null;
|
||||
this._cachedTimestamp = null;
|
||||
this.subscriptionHandle = null;
|
||||
this.isLoading = new ReactiveVar(true);
|
||||
this.hasResults = new ReactiveVar(false);
|
||||
this.searching = new ReactiveVar(false);
|
||||
|
||||
|
||||
// Subscribe to the optimized due cards publication
|
||||
this.autorun(() => {
|
||||
const allUsers = this.dueCardsView() === 'all';
|
||||
|
|
@ -107,7 +107,7 @@ class DueCardsComponent extends BlazeComponent {
|
|||
this.subscriptionHandle.stop();
|
||||
}
|
||||
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
|
||||
|
||||
|
||||
// Update loading state based on subscription
|
||||
this.autorun(() => {
|
||||
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
|
||||
|
|
@ -162,7 +162,7 @@ class DueCardsComponent extends BlazeComponent {
|
|||
// Get the translated text and manually replace %s with the count
|
||||
const baseText = TAPi18n.__('n-cards-found');
|
||||
const result = baseText.replace('%s', count);
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
|
||||
}
|
||||
|
|
@ -196,10 +196,10 @@ class DueCardsComponent extends BlazeComponent {
|
|||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: found', cards.length, 'cards with due dates');
|
||||
console.log('dueCards client: cards details:', cards.map(c => ({
|
||||
id: c._id,
|
||||
title: c.title,
|
||||
dueAt: c.dueAt,
|
||||
console.log('dueCards client: cards details:', cards.map(c => ({
|
||||
id: c._id,
|
||||
title: c.title,
|
||||
dueAt: c.dueAt,
|
||||
boardId: c.boardId,
|
||||
members: c.members,
|
||||
assignees: c.assignees,
|
||||
|
|
@ -223,33 +223,15 @@ class DueCardsComponent extends BlazeComponent {
|
|||
const isAssignee = card.assignees && card.assignees.includes(currentUser._id);
|
||||
const isAuthor = card.userId === currentUser._id;
|
||||
const matches = isMember || isAssignee || isAuthor;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true' && matches) {
|
||||
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
|
||||
}
|
||||
|
||||
|
||||
return matches;
|
||||
});
|
||||
}
|
||||
|
||||
// Normalize dueAt to timestamps for stable client-side ordering
|
||||
const future = new Date('2100-12-31').getTime();
|
||||
const toTime = v => {
|
||||
if (v === null || v === undefined || v === '') return future;
|
||||
if (v instanceof Date) return v.getTime();
|
||||
const t = new Date(v);
|
||||
if (!isNaN(t.getTime())) return t.getTime();
|
||||
return future;
|
||||
};
|
||||
|
||||
filteredCards.sort((a, b) => {
|
||||
const x = toTime(a.dueAt);
|
||||
const y = toTime(b.dueAt);
|
||||
if (x > y) return 1;
|
||||
if (x < y) return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: filtered to', filteredCards.length, 'cards');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
template(name="editor")
|
||||
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
a(title="{{_ 'convert-to-markdown'}}")
|
||||
| 📝
|
||||
a(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
| 📋
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.editor(
|
||||
dir="auto"
|
||||
|
|
|
|||
|
|
@ -4,17 +4,7 @@ var converter = require('@wekanteam/html-to-markdown');
|
|||
|
||||
const specialHandles = [
|
||||
{userId: 'board_members', username: 'board_members'},
|
||||
{userId: 'card_members', username: 'card_members'},
|
||||
{userId: 'board_assignees', username: 'board_assignees'},
|
||||
{userId: 'card_assignees', username: 'card_assignees'}
|
||||
];
|
||||
const cardSpecialHandles = [
|
||||
{userId: 'card_members', username: 'card_members'},
|
||||
{userId: 'card_assignees', username: 'card_assignees'}
|
||||
];
|
||||
const boardSpecialHandles = [
|
||||
{userId: 'board_members', username: 'board_members'},
|
||||
{userId: 'board_assignees', username: 'board_assignees'}
|
||||
{userId: 'card_members', username: 'card_members'}
|
||||
];
|
||||
const specialHandleNames = specialHandles.map(m => m.username);
|
||||
|
||||
|
|
@ -55,27 +45,23 @@ BlazeComponent.extendComponent({
|
|||
match: /\B@([\w.-]*)$/,
|
||||
search(term, callback) {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const searchTerm = term.toLowerCase();
|
||||
const users = currentBoard
|
||||
.activeMembers()
|
||||
.map(member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
const username = user.username.toLowerCase();
|
||||
const fullName = user.profile && user.profile !== undefined && user.profile.fullname ? user.profile.fullname.toLowerCase() : "";
|
||||
return username.includes(searchTerm) || fullName.includes(searchTerm) ? user : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
// Order: 1. Users, 2. Card-specific options, 3. Board-wide options
|
||||
callback(_.union(users, cardSpecialHandles, boardSpecialHandles));
|
||||
callback(
|
||||
_.union(
|
||||
currentBoard
|
||||
.activeMembers()
|
||||
.map(member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
const username = user.username;
|
||||
const fullName = user.profile && user.profile !== undefined && user.profile.fullname ? user.profile.fullname : "";
|
||||
return username.includes(term) || fullName.includes(term) ? user : null;
|
||||
})
|
||||
.filter(Boolean), [...specialHandles])
|
||||
);
|
||||
},
|
||||
template(user) {
|
||||
if (user.profile && user.profile.fullname) {
|
||||
return (user.profile.fullname + " (" + user.username + ")");
|
||||
}
|
||||
// Translate special group mentions
|
||||
if (specialHandleNames.includes(user.username)) {
|
||||
return TAPi18n.__(user.username);
|
||||
}
|
||||
return user.username;
|
||||
},
|
||||
replace(user) {
|
||||
|
|
@ -387,15 +373,13 @@ Blaze.Template.registerHelper(
|
|||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard)
|
||||
return HTML.Raw(sanitizeHTML(content));
|
||||
const knowedUsers = _.union(currentBoard.members
|
||||
.filter(member => member.isActive)
|
||||
.map(member => {
|
||||
const u = ReactiveCache.getUser(member.userId);
|
||||
if (u) {
|
||||
member.username = u.username;
|
||||
}
|
||||
return member;
|
||||
}), [...specialHandles]);
|
||||
const knowedUsers = _.union(currentBoard.members.map(member => {
|
||||
const u = ReactiveCache.getUser(member.userId);
|
||||
if (u) {
|
||||
member.username = u.username;
|
||||
}
|
||||
return member;
|
||||
}), [...specialHandles]);
|
||||
const mentionRegex = /\B@([\w.-]*)/gi;
|
||||
|
||||
let currentMention;
|
||||
|
|
@ -412,14 +396,6 @@ Blaze.Template.registerHelper(
|
|||
if (knowedUser.userId === Meteor.userId()) {
|
||||
linkClass += ' me';
|
||||
}
|
||||
|
||||
// For special group mentions, display translated text
|
||||
let displayText = knowedUser.username;
|
||||
if (specialHandleNames.includes(knowedUser.username)) {
|
||||
displayText = TAPi18n.__(knowedUser.username);
|
||||
linkClass = 'atMention'; // Remove js-open-member for special handles
|
||||
}
|
||||
|
||||
// This @user mention link generation did open same Wekan
|
||||
// window in new tab, so now A is changed to U so it's
|
||||
// underlined and there is no link popup. This way also
|
||||
|
|
@ -434,7 +410,7 @@ Blaze.Template.registerHelper(
|
|||
// using a data attribute.
|
||||
'data-userId': knowedUser.userId,
|
||||
},
|
||||
[' ', at, displayText],
|
||||
linkValue,
|
||||
);
|
||||
|
||||
content = content.replace(fullMention, Blaze.toHTML(link));
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
template(name="globalSearchHeaderBar")
|
||||
if currentUser
|
||||
h1
|
||||
i.fa.fa-search
|
||||
| 🔍
|
||||
| {{_ 'globalSearch-title'}}
|
||||
|
||||
template(name="globalSearchModalTitle")
|
||||
if currentUser
|
||||
h2
|
||||
i.fa.fa-keyboard-o
|
||||
| ⌨️
|
||||
| {{_ 'globalSearch-title'}}
|
||||
|
||||
template(name="resultsPaged")
|
||||
|
|
|
|||
|
|
@ -78,26 +78,12 @@
|
|||
#header #header-main-bar .board-header-btn .board-header-btn-close i.fa {
|
||||
margin: 0 6px;
|
||||
}
|
||||
#header #header-main-bar .board-header-btn .board-header-btn-icon {
|
||||
float: left;
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
color: #27ae60;
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#header #header-main-bar .board-header-btn.is-active,
|
||||
#header #header-main-bar h1.is-clickable.is-active,
|
||||
#header #header-main-bar .board-header-btn:hover:not(.is-disabled),
|
||||
#header #header-main-bar h1.is-clickable:hover:not(.is-disabled) {
|
||||
background: rgba(0,0,0,0.15);
|
||||
}
|
||||
#header #header-main-bar .board-header-btn.js-multiselection-active {
|
||||
background: #1a5080;
|
||||
}
|
||||
#header #header-main-bar .board-header-btn.js-multiselection-active:hover {
|
||||
background: #0f3a5f;
|
||||
}
|
||||
#header #header-main-bar .separator {
|
||||
margin: 2px 4px;
|
||||
border-left: 1px solid rgba(255,255,255,0.3);
|
||||
|
|
@ -177,7 +163,8 @@
|
|||
}
|
||||
#header-quick-access ul.header-quick-access-list {
|
||||
transition: opacity 0.2s;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 10px;
|
||||
margin: -10px;
|
||||
|
|
@ -185,16 +172,26 @@
|
|||
min-width: 0; /* Allow shrinking below content size */
|
||||
display: flex; /* Use flexbox for better control */
|
||||
align-items: center;
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: rgba(255, 255, 255, 0.3) transparent; /* Firefox */
|
||||
}
|
||||
|
||||
/* Hide scrollbar completely */
|
||||
/* Webkit scrollbar styling for better UX */
|
||||
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
#header-quick-access ul.header-quick-access-list {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#header-quick-access ul.header-quick-access-list::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
#header-quick-access ul.header-quick-access-list li {
|
||||
display: inline-block; /* Keep inline-block for proper spacing */
|
||||
|
|
@ -222,13 +219,6 @@
|
|||
}
|
||||
#header-quick-access ul.header-quick-access-list li.current.empty {
|
||||
padding: 12px 10px 12px 10px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
opacity: 0.85;
|
||||
font-style: italic;
|
||||
}
|
||||
#header-quick-access ul.header-quick-access-list li:first-child .fa-home,
|
||||
#header-quick-access ul.header-quick-access-list li:nth-child(3) .fa-globe {
|
||||
|
|
@ -349,20 +339,15 @@
|
|||
width: 100%;
|
||||
min-width: 3vw;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Make zoom input wider on all mobile screens */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
min-width: 80px !important; /* Wider on mobile to show 3 digits */
|
||||
width: 80px !important; /* Fixed width to show 100 fully */
|
||||
font-size: 16px !important; /* Slightly larger text */
|
||||
flex: 0 0 80px !important; /* Prevent shrinking in flex */
|
||||
min-width: 50px !important; /* Wider on mobile */
|
||||
width: 50px !important; /* Fixed width to show all numbers */
|
||||
font-size: 14px !important; /* Slightly larger text */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -865,9 +850,8 @@
|
|||
#header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 16px !important; /* Larger input text */
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
min-width: 80px !important; /* Wider to fit 100 */
|
||||
width: 80px !important; /* Fixed width to show 100 fully */
|
||||
flex: 0 0 80px !important; /* Prevent shrinking in flex */
|
||||
min-width: 6vw !important; /* Much wider for mobile */
|
||||
width: 60px !important; /* Fixed width to show all numbers */
|
||||
}
|
||||
|
||||
/* Make mobile mode toggle larger */
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ template(name="header")
|
|||
// Home icon - always at left side of logo
|
||||
span.home-icon.allBoards
|
||||
a(href="{{pathFor 'home'}}")
|
||||
i.fa.fa-home
|
||||
| 🏠
|
||||
| {{_ 'all-boards'}}
|
||||
|
||||
// Logo - visible; on mobile constrained by CSS
|
||||
|
|
@ -30,14 +30,6 @@ template(name="header")
|
|||
span.zoom-display {{zoomLevel}}%
|
||||
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
|
||||
|
||||
// Drag handles toggle - between zoom and mobile mode toggle
|
||||
a.board-header-btn.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}")
|
||||
i.fa.fa-arrows
|
||||
if isShowDesktopDragHandles
|
||||
i.fa.fa-check
|
||||
unless isShowDesktopDragHandles
|
||||
i.fa.fa-ban
|
||||
|
||||
if isMiniScreen
|
||||
ul.header-quick-access-list
|
||||
if currentList
|
||||
|
|
@ -52,42 +44,48 @@ template(name="header")
|
|||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
+viewer
|
||||
= title
|
||||
//a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
|
||||
// i.fa.fa-arrows
|
||||
// if isShowDesktopDragHandles
|
||||
// i.fa.fa-check-square-o
|
||||
// unless isShowDesktopDragHandles
|
||||
// i.fa.fa-ban
|
||||
#header-new-board-icon
|
||||
else
|
||||
ul.header-quick-access-list
|
||||
//li
|
||||
//
|
||||
a(href="{{pathFor 'public'}}")
|
||||
//
|
||||
span.fa.fa-globe
|
||||
//
|
||||
| {{_ 'public'}}
|
||||
// a(href="{{pathFor 'public'}}")
|
||||
// span.fa.fa-globe
|
||||
// | {{_ 'public'}}
|
||||
each currentUser.starredBoards
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
a(href="{{pathFor 'board' id=_id slug=slug}}")
|
||||
+viewer
|
||||
= title
|
||||
else
|
||||
li.current.empty(title="{{_ 'quick-access-description'}}")
|
||||
| {{_ 'quick-access-description'}}
|
||||
#header-new-board-icon
|
||||
li.current.empty {{_ 'quick-access-description'}}
|
||||
//a.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}" alt="{{_ 'show-desktop-drag-handles'}}")
|
||||
// i.fa.fa-arrows
|
||||
// if isShowDesktopDragHandles
|
||||
// i.fa.fa-check-square-o
|
||||
// unless isShowDesktopDragHandles
|
||||
// i.fa.fa-ban
|
||||
// Next line is used only for spacing at header,
|
||||
// there is no visible clickable icon.
|
||||
#header-new-board-icon
|
||||
//
|
||||
Hide duplicate create board button,
|
||||
//
|
||||
because it did not show board templates correctly.
|
||||
// Hide duplicate create board button,
|
||||
// because it did not show board templates correctly.
|
||||
//a#header-new-board-icon.js-create-board
|
||||
//
|
||||
i.fa.fa-plus(title="Create a new board")
|
||||
// i.fa.fa-plus(title="Create a new board")
|
||||
|
||||
.mobile-mode-toggle
|
||||
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
|
||||
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
|
||||
i.fa.fa-mobile
|
||||
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
|
||||
i.fa.fa-desktop
|
||||
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}") 📱
|
||||
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}") 🖥️
|
||||
|
||||
// Bookmarks button - desktop opens popup, mobile routes to page
|
||||
a.board-header-btn.js-open-bookmarks(title="{{_ 'bookmarks'}}")
|
||||
| 🔖
|
||||
|
||||
// Notifications
|
||||
+notifications
|
||||
|
|
@ -95,7 +93,7 @@ template(name="header")
|
|||
if currentSetting.customHelpLinkUrl
|
||||
#header-help
|
||||
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
|
||||
i.fa.fa-question-circle
|
||||
| ❓
|
||||
|
||||
+headerUserBar
|
||||
|
||||
|
|
@ -114,17 +112,15 @@ template(name="header")
|
|||
if hasAnnouncement
|
||||
.announcement
|
||||
p
|
||||
i.fa.fa-bullhorn
|
||||
| 📢
|
||||
+viewer
|
||||
| #{announcement}
|
||||
a
|
||||
.js-close-announcement
|
||||
i.fa.fa-times-thin
|
||||
| ❌
|
||||
|
||||
template(name="offlineWarning")
|
||||
.offline-warning
|
||||
p
|
||||
i.fa.fa-warning
|
||||
| ⚠️
|
||||
| {{_ 'app-is-offline'}}
|
||||
|
||||
a.app-try-reconnect {{_ 'app-try-reconnect'}}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
Meteor.subscribe('user-admin');
|
||||
Meteor.subscribe('boards');
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('announcements');
|
||||
Template.header.onCreated(function () {
|
||||
Template.header.onCreated(function(){
|
||||
const templateInstance = this;
|
||||
templateInstance.currentSetting = new ReactiveVar();
|
||||
templateInstance.isLoading = new ReactiveVar(false);
|
||||
|
|
@ -14,21 +13,10 @@ Template.header.onCreated(function () {
|
|||
onReady() {
|
||||
templateInstance.currentSetting.set(ReactiveCache.getCurrentSetting());
|
||||
let currSetting = templateInstance.currentSetting.curValue;
|
||||
if (
|
||||
currSetting &&
|
||||
currSetting !== undefined &&
|
||||
currSetting.customLoginLogoImageUrl !== undefined &&
|
||||
document.getElementById('headerIsSettingDatabaseCallDone') != null
|
||||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.display = 'none';
|
||||
else if (
|
||||
document.getElementById('headerIsSettingDatabaseCallDone') != null
|
||||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.display = 'block';
|
||||
if(currSetting && currSetting !== undefined && currSetting.customLoginLogoImageUrl !== undefined && document.getElementById("headerIsSettingDatabaseCallDone") != null)
|
||||
document.getElementById("headerIsSettingDatabaseCallDone").style.display = 'none';
|
||||
else if(document.getElementById("headerIsSettingDatabaseCallDone") != null)
|
||||
document.getElementById("headerIsSettingDatabaseCallDone").style.display = 'block';
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
|
|
@ -86,15 +74,10 @@ Template.header.events({
|
|||
},
|
||||
|
||||
'keypress .js-zoom-input'(evt) {
|
||||
if (evt.which === 13) {
|
||||
// Enter key
|
||||
if (evt.which === 13) { // Enter key
|
||||
const newZoomPercent = parseInt(evt.target.value);
|
||||
|
||||
if (
|
||||
!isNaN(newZoomPercent) &&
|
||||
newZoomPercent >= 50 &&
|
||||
newZoomPercent <= 300
|
||||
) {
|
||||
if (!isNaN(newZoomPercent) && newZoomPercent >= 50 && newZoomPercent <= 300) {
|
||||
const newZoom = newZoomPercent / 100;
|
||||
Utils.setZoomLevel(newZoom);
|
||||
|
||||
|
|
@ -132,10 +115,11 @@ Template.header.events({
|
|||
Session.set('currentCard', null);
|
||||
},
|
||||
'click .js-toggle-desktop-drag-handles'() {
|
||||
currentUser = Meteor.user();
|
||||
if (currentUser) {
|
||||
Meteor.call('toggleDesktopDragHandles');
|
||||
} else if (window.localStorage.getItem('showDesktopDragHandles')) {
|
||||
//currentUser = Meteor.user();
|
||||
//if (currentUser) {
|
||||
// Meteor.call('toggleDesktopDragHandles');
|
||||
//} else if (window.localStorage.getItem('showDesktopDragHandles')) {
|
||||
if (window.localStorage.getItem('showDesktopDragHandles')) {
|
||||
window.localStorage.removeItem('showDesktopDragHandles');
|
||||
location.reload();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ template(name="shortcutsHeaderBar")
|
|||
|
||||
template(name="shortcutsModalTitle")
|
||||
h2
|
||||
i.fa.fa-keyboard-o
|
||||
| ⌨️
|
||||
| {{_ 'keyboard-shortcuts'}}
|
||||
|
||||
template(name="keyboardShortcuts")
|
||||
|
|
|
|||
|
|
@ -81,27 +81,6 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
/* iOS Safari fixes */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Mobile mode specific fixes for iOS Safari */
|
||||
body.mobile-mode {
|
||||
overflow-x: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
/* Prevent iOS Safari bounce scroll */
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Ensure content area is scrollable in mobile mode */
|
||||
body.mobile-mode #content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: calc(100vh - 48px);
|
||||
}
|
||||
#content {
|
||||
position: relative;
|
||||
|
|
@ -515,7 +494,7 @@ a:not(.disabled).is-active i.fa {
|
|||
max-width: 95vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
/* Improve touch targets */
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 44px;
|
||||
|
|
@ -524,7 +503,7 @@ a:not(.disabled).is-active i.fa {
|
|||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
|
||||
/* Form elements */
|
||||
input, select, textarea {
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
|
|
@ -532,7 +511,7 @@ a:not(.disabled).is-active i.fa {
|
|||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
|
||||
/* Cards and lists */
|
||||
.minicard {
|
||||
min-height: 48px;
|
||||
|
|
@ -540,19 +519,19 @@ a:not(.disabled).is-active i.fa {
|
|||
margin-bottom: 8px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
|
||||
.list {
|
||||
margin: 0 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
|
||||
/* Board canvas */
|
||||
.board-canvas {
|
||||
padding: 0 8px 8px 0;
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
|
||||
/* Header mobile layout */
|
||||
#header {
|
||||
padding: 8px;
|
||||
|
|
@ -561,7 +540,7 @@ a:not(.disabled).is-active i.fa {
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
#header-quick-access {
|
||||
/* Keep quick-access items in one row */
|
||||
display: flex;
|
||||
|
|
@ -585,43 +564,43 @@ a:not(.disabled).is-active i.fa {
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
/* Hide text in home icon on mobile, show only icon */
|
||||
#header-quick-access .home-icon a span:not(.fa) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure proper spacing for mobile header elements */
|
||||
#header-quick-access .zoom-controls {
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
.mobile-mode-toggle {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
#header-user-bar {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure header elements don't wrap on very small screens */
|
||||
#header-quick-access {
|
||||
min-width: 0; /* Allow flexbox to shrink */
|
||||
}
|
||||
|
||||
|
||||
/* Make sure logo doesn't take too much space on mobile */
|
||||
#header-quick-access img {
|
||||
max-height: 24px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure zoom controls are compact on mobile */
|
||||
.zoom-controls .zoom-level {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
/* Modal mobile optimization */
|
||||
#modal .modal-content,
|
||||
#modal .modal-content-wide {
|
||||
|
|
@ -632,7 +611,7 @@ a:not(.disabled).is-active i.fa {
|
|||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
/* Table mobile optimization */
|
||||
table {
|
||||
font-size: 14px;
|
||||
|
|
@ -642,19 +621,19 @@ a:not(.disabled).is-active i.fa {
|
|||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
|
||||
/* Admin panel mobile optimization */
|
||||
.setting-content .content-body {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 100%;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .main-body {
|
||||
order: 1;
|
||||
min-height: 60vh;
|
||||
|
|
@ -668,63 +647,58 @@ a:not(.disabled).is-active i.fa {
|
|||
#content > .wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
.wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
.panel-default {
|
||||
width: 90vw;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
|
||||
/* Touch-friendly but more compact */
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
|
||||
.minicard {
|
||||
min-height: 40px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
.list {
|
||||
margin: 0 12px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
.board-canvas {
|
||||
padding: 0 12px 12px 0;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
#header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content {
|
||||
width: 80vw;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content-wide {
|
||||
width: 90vw;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
/* Responsive handling for quick-access description on tablets */
|
||||
#header-quick-access ul.header-quick-access-list li.current.empty {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large displays and digital signage (1920px+) */
|
||||
|
|
@ -732,49 +706,49 @@ a:not(.disabled).is-active i.fa {
|
|||
body {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
|
||||
min-height: 56px;
|
||||
min-width: 56px;
|
||||
padding: 16px 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
.minicard {
|
||||
min-height: 56px;
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
.list {
|
||||
margin: 0 8px;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
|
||||
.board-canvas {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
#header {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
|
||||
#content > .wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
|
||||
#modal .modal-content-wide {
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body {
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 320px;
|
||||
}
|
||||
|
|
@ -782,7 +756,7 @@ a:not(.disabled).is-active i.fa {
|
|||
.inline-input {
|
||||
height: 37px;
|
||||
margin: 8px 10px 0 0;
|
||||
width: 100px;
|
||||
width: 50px;
|
||||
}
|
||||
.select-authentication {
|
||||
width: 100%;
|
||||
|
|
@ -925,40 +899,6 @@ a:not(.disabled).is-active i.fa {
|
|||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS Safari Mobile Mode Fixes */
|
||||
@media screen and (max-width: 800px) {
|
||||
/* Prevent scrolling issues on iOS Safari when card popup is open */
|
||||
body.mobile-mode {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Fix z-index stacking for mobile Safari */
|
||||
body.mobile-mode .board-wrapper {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .pop-over {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Ensure smooth scrolling on iOS */
|
||||
body.mobile-mode .card-details,
|
||||
body.mobile-mode .pop-over .content-wrapper {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes lds-roller {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue