Compare commits

...
87 Commits
Author SHA1 Message Date
Lauri Ojansivu 6b2759f97d v8.78 2026-04-09 04:52:05 +03:00
Lauri Ojansivu adf4312e5a v8.77 2026-04-09 04:40:21 +03:00
Lauri Ojansivu b1707b8691 Upgraded to Meteor 3.5-beta.7
Thanks to Meteor developers !
2026-04-09 04:37:36 +03:00
Lauri Ojansivu 25dff3f2da v8.76 2026-04-09 04:19:51 +03:00
Lauri Ojansivu bc88c0dfc2 Fix GitHub Actions builds.
Thanks to xet7 !
2026-04-09 04:14:46 +03:00
Lauri Ojansivu 1ca2dfd1b1 v8.75 2026-04-09 03:58:01 +03:00
Lauri Ojansivu bdb9274485 Updated to Node.js 24.x at release scripts.
Thanks to xet7 !
2026-04-09 03:56:06 +03:00
Lauri Ojansivu 14f9ac39ad Upgraded to Meteor 3.5-beta.7 and Node.js 24.14.1. Now prefers MongoDB Change Streams at Snap Candidate and docker-compose.yml.
Thanks to Meteor developers and xet7 !
2026-04-09 03:44:58 +03:00
Lauri Ojansivu d4b52f7de9 Updated ChangeLog. 2026-04-09 02:19:44 +03:00
Lauri Ojansivu bdc65ec6f4 Merge pull request #6261 from KhaoulaMaleh/fix/translation-refinement
Refine translations
2026-04-08 23:30:39 +03:00
KhaoulaMaleh 7fb31cecd5 fix refinement 2026-04-08 00:45:27 -11:00
KhaoulaMaleh 317c82a9f0 fixed after review 2026-04-08 00:16:18 -11:00
KhaoulaMaleh 9b5c89d235 Enhance translations with new key:value pairs 2026-04-07 23:36:43 -11:00
Lauri Ojansivu c23e190e8e Updated ChangeLog. 2026-04-08 02:16:21 +03:00
Lauri Ojansivu 4a73371499 Merge pull request #6260 from KhaoulaMaleh/fix/Card_setting_checkbox
fixed Card settings checkboxes
2026-04-08 02:14:28 +03:00
KhaoulaMaleh 6f8af55489 fixed Card settings checkboxes 2026-04-08 00:04:48 -11:00
Lauri Ojansivu 2a05f62bf9 Updated ChangeLog. 2026-04-08 01:49:33 +03:00
Lauri Ojansivu d6eb1eb76f Merge pull request #6259 from KhaoulaMaleh/fix/enhance_translation
Enhance translations with new key:value pairs
2026-04-08 01:46:33 +03:00
Lauri Ojansivu 27542f3a51 v8.74 2026-04-08 00:34:49 +03:00
Lauri Ojansivu 0328e70d5f Fix can not save Card Data. Part 2.
Thanks to xet7 !

Fixes #6254
2026-04-08 00:31:40 +03:00
Lauri Ojansivu c46932ee30 v8.73 2026-04-07 23:18:14 +03:00
Lauri Ojansivu c7408ad8d1 Fix board and card activities. Part 2.
Thanks to xet7 !

Related #6252
2026-04-07 23:12:26 +03:00
Lauri Ojansivu 05dc8f8f93 Updated ChangeLog. 2026-04-07 22:01:59 +03:00
Lauri Ojansivu aea8c285c2 Fix Change list inside or card.
Thanks to mimZD and xet7 !

Fixes #6161
2026-04-07 22:00:11 +03:00
Lauri Ojansivu a859137971 Updated ChangeLog. 2026-04-07 21:53:11 +03:00
Lauri Ojansivu 08ebad0fe4 Fix Checklists add and Subtasks reorder. Fix Board Activity part 1.
Thanks to xet7 !

Related #6252
2026-04-07 21:49:26 +03:00
Lauri Ojansivu d54ebfa0e1 Updated ChangeLog. 2026-04-07 20:39:55 +03:00
Lauri Ojansivu 304a007d07 Fix Search All Boards result opened card missing Copy Card Link etc that can be used to open correct card.
Thanks bcook-konza and xet7 !

Fixes #6251
2026-04-07 20:35:20 +03:00
Lauri Ojansivu b6b58c9d95 Updated ChangeLog. 2026-04-07 20:10:38 +03:00
Lauri Ojansivu 7f0c42cca5 Fix Error in choosing member or assignee in a card.
Thanks to hmeunier95 and xet7 !

Fixes #6257
2026-04-07 20:08:04 +03:00
Lauri Ojansivu 9508d46ebd Updated ChangeLog. 2026-04-07 19:25:15 +03:00
Lauri Ojansivu bcbf4771ea Fix 8.72 Bug: LDAP login does not work. ReactiveCache is not defined.
Thanks to poc-sm and xet7 !

Fixes #6255
2026-04-07 19:23:06 +03:00
Lauri Ojansivu 9da5ec25c6 Updated ChangeLog. 2026-04-07 19:21:20 +03:00
Lauri Ojansivu 028b404377 Removed exclude-arch from rebuild-wekan.sh, it is not necessary anymore.
Thanks to xet7 !
2026-04-07 19:19:53 +03:00
Lauri Ojansivu 8da0f34ee6 Fix can not save Card Data.
Thanks to xet7 !

Fixes #6254
2026-04-07 19:17:35 +03:00
Lauri Ojansivu cac2628b19 Updated ChangeLog. 2026-04-07 11:40:46 +03:00
Lauri Ojansivu 9c69530256 Fix Link to Windows release doesn't work.
Thanks to hmeunier95 and xet7 !

Fixes #6253
2026-04-07 11:37:50 +03:00
Lauri Ojansivu 6e108358d7 v8.72 2026-04-07 10:44:37 +03:00
Lauri Ojansivu 87f592c67f Updated release scripts.
Thanks to xet7 !
2026-04-07 10:42:45 +03:00
Lauri Ojansivu fc95abd587 v8.72 2026-04-07 09:52:41 +03:00
Lauri Ojansivu ce3e51b147 Fix Impersonate User. Show/Edit Avatar at Admin Panel/People/People.
Thanks to ahlgrimma, DnT1mmer, robbagithub, norrig and xet7 !

Fixes #6226,
fixes #6101,
fixes #3327,
fixes #4748
2026-04-07 09:52:41 +03:00
Lauri Ojansivu bb48b23266 Merge pull request #6250 from wekan/dependabot/github_actions/docker/login-action-4.1.0
Bump docker/login-action from 4.0.0 to 4.1.0
2026-04-07 09:26:54 +03:00
Lauri Ojansivu fdf00e06f5 Merge pull request #6249 from wekan/dependabot/github_actions/actions/cache-5
Bump actions/cache from 4 to 5
2026-04-07 09:26:38 +03:00
Lauri Ojansivu 950b246da8 Merge pull request #6248 from wekan/dependabot/github_actions/actions/upload-artifact-7
Bump actions/upload-artifact from 4 to 7
2026-04-07 09:26:25 +03:00
Lauri Ojansivu 6ac0df8448 Merge pull request #6247 from wekan/dependabot/github_actions/actions/download-artifact-8
Bump actions/download-artifact from 4 to 8
2026-04-07 09:25:51 +03:00
dependabot[bot] 80ae3a18a8 Bump docker/login-action from 4.0.0 to 4.1.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/b45d80f862d83dbcd57f89517bcf500b2ab88fb2...4907a6ddec9925e35a0a9e82d7399ccc52663121)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:33:05 +00:00
dependabot[bot] 972bfb9677 Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:33:01 +00:00
dependabot[bot] b1149e4d6e Bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:32:55 +00:00
dependabot[bot] 3cd2ba847b Bump actions/download-artifact from 4 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:32:49 +00:00
Lauri Ojansivu 8bf869654c Updated ChangeLog. 2026-04-06 05:09:38 +03:00
Lauri Ojansivu 3b98f862bc Fix Admin Panel / Version not showing name of MongoDB storage engine and status of MongoDB Oplog enabled.
Thanks to xet7 !
2026-04-06 05:05:19 +03:00
Lauri Ojansivu 9bc889e274 v8.71 2026-04-06 03:40:47 +03:00
Lauri Ojansivu 5df924f416 Fix: Added missing null check to notification.
Thanks to xet7 !
2026-04-06 03:35:28 +03:00
Lauri Ojansivu 5ef94b7079 v8.70 2026-04-06 02:43:39 +03:00
Lauri Ojansivu dfcaaf0ae7 Convert upsert calls to async in authentication.js .
Thanks to snowsky and xet7 !

Fixes https://github.com/wekan/wekan/pull/6243,
fixes https://github.com/wekan/wekan/pull/6245
2026-04-06 02:36:45 +03:00
Lauri Ojansivu 34e7ff532f v8.69 2026-04-06 01:32:30 +03:00
Lauri Ojansivu fbdfcead50 Try to fix Snap to start MongoDB OpLog correctly.
Thanks to xet7.
2026-04-06 01:28:46 +03:00
Lauri Ojansivu b8bbe4431b v8.68 2026-04-06 00:26:18 +03:00
Lauri Ojansivu 9e31666280 Try to fix Snap to start MongoDB OpLog correctly.
Thanks to xet7 !
2026-04-06 00:21:39 +03:00
Lauri Ojansivu 5846d9c9b0 v8.67 2026-04-05 23:22:53 +03:00
Lauri Ojansivu 7707a5b7fc Try to fix GitHub Actions release bundle builds.
Thanks to xet7.
2026-04-05 23:20:35 +03:00
Lauri Ojansivu 18c495940e v8.65 2026-04-05 23:11:32 +03:00
Lauri Ojansivu d3155d24c6 Try to fix GitHub Actions release bundle builds.
Thanks to xet7.
2026-04-05 23:09:36 +03:00
Lauri Ojansivu c0661506c4 Fix Docker Actions Test suite.
Thanks to xet7 !
2026-04-05 22:56:29 +03:00
Lauri Ojansivu fa2b34764f v8.64 2026-04-05 22:51:36 +03:00
Lauri Ojansivu 7a7909b49f Try to fix GitHub Actions release bundle builds.
Thanks to xet7 !
2026-04-05 22:49:33 +03:00
Lauri Ojansivu 17d3b3088f v8.63 2026-04-05 22:38:05 +03:00
Lauri Ojansivu 6a78a178e2 Try to fix GitHub Actions release bundle builds.
Thanks to xet7 !
2026-04-05 22:35:50 +03:00
Lauri Ojansivu 749de39e2a v8.62 2026-04-05 22:20:33 +03:00
Lauri Ojansivu cd55c0e12e Try to fix GitHub Actions bundle builds.
Thanks to xet7 !
2026-04-05 22:17:12 +03:00
Lauri Ojansivu d04037739e v8.61 2026-04-05 21:58:49 +03:00
Lauri Ojansivu f3a97b9938 Delete all GitHub Actions Caches, so that old stuff does not break builds.
Thanks to xet7 !
2026-04-05 21:55:09 +03:00
Lauri Ojansivu 104fbef37a Reverted disabling GitHub workflows. Still trying to fix them.
Thanks to xet7 !
2026-04-05 21:28:19 +03:00
Lauri Ojansivu 98b3dc71af Updated ChangeLog. 2026-04-05 21:15:52 +03:00
Lauri Ojansivu 7b28e4d589 Merge pull request #6246 from KhaoulaMaleh/fix/comment
Fix: Server crash during comment deletion
2026-04-05 21:14:09 +03:00
KhaoulaMaleh 57291dbe50 fixing deletion comment 2026-04-05 17:51:29 -11:00
Lauri Ojansivu 9f9eefce27 Updated ChangeLog. 2026-04-05 21:13:50 +03:00
Lauri Ojansivu af3361b35b Disabled not working GitHub workflows.
Thanks to xet7 !
2026-04-05 21:12:08 +03:00
Lauri Ojansivu 24d65e1e72 Disabled GitHub workflows that produce broken builds.
Thanks to xet7 !
2026-04-05 21:06:49 +03:00
Lauri Ojansivu 29922fca6a Updated ChangeLog. 2026-04-05 18:46:12 +03:00
Lauri Ojansivu 81319fc48b Updated Docker build script to check directories and repos exist.
Thanks to xet7 !
2026-04-05 18:44:07 +03:00
Lauri Ojansivu 28b7209407 v8.60 2026-04-05 18:14:31 +03:00
Lauri Ojansivu 0b59f87887 Updated Docker build script.
Thanks to xet7 !
2026-04-05 18:10:57 +03:00
Lauri Ojansivu b22df9e93d v8.59 2026-04-05 18:09:10 +03:00
Lauri Ojansivu 1fa72fd34a Fix Snap to use OpLog.
Thanks to xet7 !
2026-04-05 18:05:29 +03:00
Lauri Ojansivu a2f38e7494 Updated ChangeLog. 2026-04-05 17:41:52 +03:00
Lauri Ojansivu 30a1792c92 Fix docker-compose.yml to use OpLog.
Thanks to xet7 !
2026-04-05 17:40:01 +03:00
63 changed files with 2450 additions and 1025 deletions

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
+58 -47
View File
@@ -60,12 +60,12 @@ jobs:
echo "GHEOF" >> $GITHUB_OUTPUT
# ─── 2. Build amd64 bundle (full Meteor 3 build) ────────────────────────────
# Meteor 3.4 installer supports only: Linux x86_64, Linux aarch64, macOS, Windows.
# Meteor 3.5-beta.7 installer supports only: Linux x86_64, Linux aarch64, macOS, Windows.
# It explicitly rejects armhf, s390x, and ppc64le at install time.
# Therefore the full Meteor build always runs here on x86_64.
# All other Linux architectures then rebuild only the native npm modules
# from this bundle (much faster than a full Meteor build).
# Node.js version must match what Meteor 3.4 bundles: 22.22.2 (ABI 127).
# Node.js version must match what Meteor 3.5-beta.7 bundles: 24.14.1 (ABI 143).
build-amd64:
needs: prepare
runs-on: ubuntu-24.04
@@ -76,14 +76,14 @@ jobs:
- uses: actions/checkout@v6
- name: Cache Meteor installation
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.meteor
key: meteor-${{ hashFiles('.meteor/release') }}-${{ hashFiles('.meteor/packages') }}
restore-keys: meteor-
- name: Cache npm packages
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
@@ -93,11 +93,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential g++ make python3 curl wget zip unzip
# Pin to Node 22.22.2 — the exact version bundled by Meteor 3.4.
# Pin to Node 24.14.1 — the exact version bundled by Meteor 3.5-beta.7.
# Using the same version for build and npm rebuild ensures consistent native module ABI.
- uses: actions/setup-node@v6
with:
node-version: '22.22.2'
node-version: '24.14.1'
- name: Install Meteor
run: |
@@ -108,23 +108,28 @@ jobs:
- name: Build WeKan bundle
run: |
rm -rf .build node_modules .meteor/local
meteor npm install --no-audit
meteor build .build --directory --platforms=web.browser
rm -rf .build node_modules .meteor/local/bundler-cache
meteor npm install
meteor build .build --directory
- name: Install dependencies in bundle
run: |
cd .build/bundle/programs/server
meteor npm install
- name: Create amd64 zip
run: |
cd .build
zip -r wekan-${VERSION}-amd64.zip bundle
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: bundle-amd64
path: .build/wekan-${{ needs.prepare.outputs.version }}-amd64.zip
retention-days: 1
# ─── 3. Build arm64 bundle (rebuild native modules natively) ─────────────────
# Meteor 3.4 does support Linux aarch64, but running a full Meteor build a
# Meteor 3.5-beta.7 does support Linux aarch64, but running a full Meteor build a
# second time would double the build time for the same result. npm rebuild on
# the native arm64 GitHub Actions runner is much faster and produces the same
# platform-specific native modules.
@@ -142,10 +147,10 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version: '22.22.2'
node-version: '24.14.1'
- name: Download amd64 bundle
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: bundle-amd64
@@ -153,24 +158,25 @@ jobs:
run: |
unzip wekan-${VERSION}-amd64.zip
cd bundle/programs/server
npm rebuild
cd ../../..
rm -rf node_modules
npm install
cd ../..
- name: Clean up and create arm64 zip
run: |
zip -r wekan-${VERSION}-arm64.zip bundle
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: bundle-arm64
path: wekan-${{ needs.prepare.outputs.version }}-arm64.zip
retention-days: 1
# ─── 4. Build s390x bundle (rebuild native modules via QEMU container) ───────
# Meteor 3.4 does NOT support s390x (installer exits with error on that arch).
# Meteor 3.5-beta.7 does NOT support s390x (installer exits with error on that arch).
# GitHub Actions has no native s390x runners.
# Solution: QEMU emulation + node:22.22.2-slim (s390x variant) for npm rebuild.
# node:22.22.2-slim is pinned to match Meteor 3.4's bundled Node.js version exactly.
# Solution: QEMU emulation + node:24.14.1-slim (s390x variant) for npm rebuild.
# node:24.14.1-slim is pinned to match Meteor 3.5-beta.7's bundled Node.js version exactly.
build-s390x:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
@@ -187,34 +193,35 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for s390x in container
run: |
unzip wekan-${VERSION}-amd64.zip
# node:22.22.2-slim s390x — same Node.js version as Meteor 3.4 bundled
rm -rf bundle/programs/server/node_modules
# node:24.14.1-slim s390x — same Node.js version as Meteor 3.5-beta.7 bundled
docker run --rm --platform linux/s390x \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:22.22.2-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm rebuild"
node:24.14.1-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create s390x zip
run: |
zip -r wekan-${VERSION}-s390x.zip bundle
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: bundle-s390x
path: wekan-${{ needs.prepare.outputs.version }}-s390x.zip
retention-days: 1
# ─── 4b. Build ppc64le bundle (rebuild native modules via QEMU container) ────
# Meteor 3.4 does NOT support ppc64le (installer exits with error on that arch).
# Meteor 3.5-beta.7 does NOT support ppc64le (installer exits with error on that arch).
# GitHub Actions has no native ppc64le runners.
# Solution: QEMU emulation + node:22.22.2-slim (ppc64le variant) for npm rebuild.
# Solution: QEMU emulation + node:24.14.1-slim (ppc64le variant) for npm rebuild.
build-ppc64le:
needs: [prepare, build-amd64]
runs-on: ubuntu-latest
@@ -231,34 +238,35 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for ppc64le in container
run: |
unzip wekan-${VERSION}-amd64.zip
# node:22.22.2-slim ppc64le — same Node.js version as Meteor 3.4 bundled
rm -rf bundle/programs/server/node_modules
# node:24.14.1-slim ppc64le — same Node.js version as Meteor 3.5-beta.7 bundled
docker run --rm --platform linux/ppc64le \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:22.22.2-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm rebuild"
node:24.14.1-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create ppc64le zip
run: |
zip -r wekan-${VERSION}-ppc64le.zip bundle
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: bundle-ppc64le
path: wekan-${{ needs.prepare.outputs.version }}-ppc64le.zip
retention-days: 1
# ─── 4c. Build armhf bundle (rebuild native modules via QEMU container) ──────
# Meteor 3.4 does NOT support arm/v7 (armhf). Installer explicitly rejects it.
# Meteor 3.5-beta.7 does NOT support arm/v7 (armhf). Installer explicitly rejects it.
# GitHub Actions has no native arm/v7 runners.
# Solution: QEMU emulation + node:22.22.2-slim (arm/v7 variant) for npm rebuild.
# Solution: QEMU emulation + node:24.14.1-slim (arm/v7 variant) for npm rebuild.
# arm/v7 under QEMU is slowest — timeout set to 120 min.
build-armhf:
needs: [prepare, build-amd64]
@@ -276,25 +284,26 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y unzip zip
- name: Download amd64 bundle
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: bundle-amd64
- name: Rebuild native modules for armhf in container
run: |
unzip wekan-${VERSION}-amd64.zip
# node:22.22.2-slim arm/v7 — same Node.js version as Meteor 3.4 bundled
rm -rf bundle/programs/server/node_modules
# node:24.14.1-slim arm/v7 — same Node.js version as Meteor 3.5-beta.7 bundled
docker run --rm --platform linux/arm/v7 \
-v "$(pwd)/bundle:/bundle" \
-w /bundle/programs/server \
node:22.22.2-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm rebuild"
node:24.14.1-slim \
sh -c "apt-get update -qq && apt-get install -y -q build-essential python3 && npm install"
- name: Clean up and create armhf zip
run: |
zip -r wekan-${VERSION}-armhf.zip bundle
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: bundle-armhf
path: wekan-${{ needs.prepare.outputs.version }}-armhf.zip
@@ -310,17 +319,18 @@ jobs:
steps:
- uses: actions/setup-node@v6
with:
node-version: '22.22.2'
node-version: '24.14.1'
- name: Install node-gyp and build tools
run: |
npm install -g windows-build-tools --vs2019 2>$null; exit 0
## With Node 24 these are already preinstalled at GitHub Windows runners
#- name: Install node-gyp and build tools
# run: |
# npm install -g windows-build-tools --vs2019 2>$null; exit 0
- name: Install 7-zip
run: choco install -y 7zip
- name: Download amd64 bundle
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: bundle-amd64
@@ -329,14 +339,15 @@ jobs:
run: |
7z x wekan-%VERSION%-amd64.zip
cd bundle\programs\server
npm rebuild
rmdir /s /q node_modules
npm install
cd ..\..\..
- name: Create win64 zip
shell: cmd
run: 7z a wekan-%VERSION%-win64.zip bundle
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: bundle-win64
path: wekan-${{ needs.prepare.outputs.version }}-win64.zip
@@ -350,7 +361,7 @@ jobs:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
- name: Download all bundles
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
pattern: bundle-*
merge-multiple: true
@@ -446,7 +457,7 @@ jobs:
# --launchpad-accept-public-upload
#
# - name: Upload all snap files as artifacts
# uses: actions/upload-artifact@v4
# uses: actions/upload-artifact@v7
# with:
# name: snaps-all-arches
# path: "*.snap"
+7 -2
View File
@@ -124,13 +124,18 @@ jobs:
restore-keys: |
v1-meteor_build_cache-
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.14.1'
- name: Setup meteor
uses: meteorengineer/setup-meteor@v3
with:
meteor-release: '2.2'
meteor-release: '3.5-beta.7'
- name: Install NPM Dependencies
run: meteor npm ci --no-audit
run: meteor npm install
- name: Run Tests
run: sh ./test-wekan.sh -cv
+20 -20
View File
@@ -3,12 +3,12 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-base
meteor-base@1.5.2
# Build system
ecmascript
standard-minifier-js
rspack
ecmascript@0.17.0
standard-minifier-js@3.2.0
rspack@1.0.0
blaze@3.0.2
@@ -16,7 +16,7 @@ blaze@3.0.2
aldeed:collection2
aldeed:schema-index@4.0.0-beta.5
reywood:publish-composite
mongo
mongo@2.3.0-beta350.7
# The holy trifecta - this is a replacement for subs-manager
# for IndexedDB persistence and auto-sync
@@ -27,7 +27,7 @@ jam:method
jam:pub-sub
# Account system
accounts-password
accounts-password@3.2.3-beta350.7
communitypackages:core
communitypackages:unstyled
communitypackages:flow-routing
@@ -41,19 +41,19 @@ wekan-oidc
wekan-accounts-oidc
# Utilities
check
random
reactive-dict
session
tracker
audit-argument-checks
check@1.5.0
random@1.2.2
reactive-dict@1.3.2
session@1.2.2
tracker@1.3.4
audit-argument-checks@1.0.8
# UI components
ostrio:i18n
reactive-var
shell-server
email
dynamic-import
reactive-var@1.0.13
shell-server@0.7.0
email@3.2.0-beta350.7
dynamic-import@0.7.4
wekan-markdown
quave:synced-cron
ostrio:cookies
@@ -61,9 +61,9 @@ ostrio:files
# lmieulet:meteor-coverage
meteortesting:mocha
matb33:collection-hooks
service-configuration
minifier-css
ejson
logging
service-configuration@1.3.5
minifier-css@2.0.1
ejson@1.2.0-beta350.7
logging@1.3.6
wekan-fullcalendar
wekan-fontawesome
+1 -1
View File
@@ -1 +1 @@
METEOR@3.4
METEOR@3.5-beta.7
+12 -12
View File
@@ -1,6 +1,6 @@
accounts-base@3.2.0
accounts-base@3.3.0-beta350.7
accounts-oauth@1.4.6
accounts-password@3.2.2
accounts-password@3.2.3-beta350.7
aldeed:collection2@4.1.5
aldeed:schema-index@4.0.0
aldeed:simple-schema@1.13.1
@@ -23,18 +23,18 @@ communitypackages:flow-routing@2.1.0
communitypackages:unstyled@2.0.1
core-runtime@1.0.0
ddp@1.4.2
ddp-client@3.1.1
ddp-client@3.2.0-beta350.7
ddp-common@1.4.4
ddp-rate-limiter@1.2.2
ddp-server@3.1.2
ddp-rate-limiter@1.3.0-beta350.7
ddp-server@3.2.0-beta350.7
diff-sequence@1.1.3
dynamic-import@0.7.4
ecmascript@0.17.0
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.12.3
ecmascript-runtime-server@0.11.1
ejson@1.1.5
email@3.1.2
ejson@1.2.0-beta350.7
email@3.2.0-beta350.7
es5-shim@4.8.1
facts-base@1.0.2
fetch@0.1.6
@@ -60,11 +60,11 @@ meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.0.1
minifier-css@2.0.1
minifier-js@3.1.0
minimongo@2.0.5
minimongo@2.1.0-beta350.7
modern-browsers@0.2.3
modules@0.20.3
modules-runtime@0.13.2
mongo@2.2.0
mongo@2.3.0-beta350.7
mongo-decimal@0.2.0
mongo-dev-server@1.1.1
mongo-id@1.0.9
@@ -82,7 +82,7 @@ promise@1.0.0
quave:synced-cron@2.4.0
raix:eventemitter@1.0.0
random@1.2.2
rate-limit@1.1.2
rate-limit@1.2.0-beta350.7
react-fast-refresh@0.3.0
reactive-dict@1.3.2
reactive-var@1.0.13
@@ -95,7 +95,7 @@ service-configuration@1.3.5
session@1.2.2
sha@1.0.10
shell-server@0.7.0
socket-stream-client@0.6.1
socket-stream-client@0.7.0-beta350.7
spacebars@2.0.0
spacebars-compiler@2.0.0
standard-minifier-js@3.2.0
@@ -107,7 +107,7 @@ tools-core@1.0.0
tracker@1.3.4
typescript@5.9.3
url@1.3.5
webapp@2.1.0
webapp@2.2.0-beta350.7
webapp-hashing@1.1.2
wekan-accounts-cas@0.2.0
wekan-accounts-lockout@1.1.0
+211 -11
View File
@@ -2,6 +2,7 @@
Newest WeKan at these platforms:
- [Upgrade WeKan](https://wekan.fi/upgrade/)
- [bundle zip at releases](https://github.com/wekan/wekan/releases), docs at
- [Linux amd64, arm64, s390x](https://github.com/wekan/wekan/blob/main/docs/Platforms/FOSS/RaspberryPi/Raspberry-Pi.md)
- [Windows amd64, works also at Windows arm64](https://github.com/wekan/wekan/blob/main/docs/Platforms/Propietary/Windows/Offline.md)
@@ -11,24 +12,223 @@ Newest WeKan at these platforms:
- Docker amd64/arm64/s390x
- 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.
Fixing other platforms In Progress.
Versions:
- [Node.js 22.x](https://nodejs.org/dist/latest-v22.x/)
- MongoDB 6.x and 7.x, or [FerretDB2/PostgreSQL](https://github.com/wekan/wekan/blob/main/docs/Databases/FerretDB2-PostgreSQL.md)
- WeKan 8.75 and newer uses Meteor 3.5-beta.7, [Node.js 24.x](https://nodejs.org/dist/latest-v24.x/),
MongoDB 7.x (or 6.x), or [FerretDB2/PostgreSQL](https://github.com/wekan/wekan/blob/main/docs/Databases/FerretDB2-PostgreSQL.md)
- WeKan 8.43 upgraded to Meteor 3.x, huge thanks to harryadel:
- https://harryadel.com/dev-diary-24/
- https://harryadel.com/dev-diary-25/
- https://harryadel.com/dev-diary-26/
- WeKan 8.00-8.24 used Colorful Unicode Emoji Icons, versions before and after use mostly Font Awesome 4.7 icons.
- WeKan 8.00-8.06 had wrong raw database directory setting /var/snap/wekan/common/wekan and some cards were not visible,
it was fixed at WeKan 8.07 where database directory is back to /var/snap/wekan/common and all cards are visible.
[Upgrade WeKan](https://wekan.fi/upgrade/)
# v8.78 2026-04-09 WeKan ® release
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.
This release adds the the following updates:
WeKan 8.00-8.24 used Colorful Unicode Emoji Icons, versions before and after use mostly Font Awesome 4.7 icons.
- Upgraded to Meteor 3.5-beta.7 and Node.js 24.14.1. Now prefers MongoDB Change Streams at Snap Candidate and docker-compose.yml.
[Part 1](https://github.com/wekan/wekan/commit/14f9ac39ad93db4f7c06721f1efd4c18e53cc5f8),
[Part 2](https://github.com/wekan/wekan/commit/b1707b8691d0ba5f355e78e250765c8a7657c3d7).
Thanks to Meteor developers.
- [Enhance translations with new key:value pairs](https://github.com/wekan/wekan/pull/6259).
Thanks to KhaoulaMaleh.
- [Refine translations](https://github.com/wekan/wekan/pull/6261).
Thanks to KhaoulaMaleh.
- [Updated to Node.js 24.x at release scripts](https://github.com/wekan/wekan/commit/bdb92744854cc211866631746c63d748d4a93cfa).
Thanks to xet7.
- [Fix GitHub Actions builds](https://github.com/wekan/wekan/commit/bc88c0dfc2742154e94861201d239bc4b3c51cef).
Thanks to xet7.
WeKan 8.43 and newer uses Meteor 3.x, Node.js 22.x and MongoDB 7.x, huge thanks to harryadel:
and fixes the following bugs:
- https://harryadel.com/dev-diary-24/
- https://harryadel.com/dev-diary-25/
- https://harryadel.com/dev-diary-26/
- [Fixed Card settings checkboxes](https://github.com/wekan/wekan/pull/6260).
Thanks to KhaoulaMaleh.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.74 2026-04-08 WeKan ® release
This release fixes the following bugs:
- [Fix can not save Card Data. Part 2](https://github.com/wekan/wekan/commit/0328e70d5f4a277722635e3fd9ea78312d1359cf).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.73 2026-04-07 WeKan ® release
This release fixes the following bugs:
- [Fix Link to Windows release doesn't work](https://github.com/wekan/wekan/commit/9c695302565073b971a3521c0a94a380641cc436).
Thanks to hmeunier95 and xet7.
- [Fix 8.72 Bug: Can not save Card Date](https://github.com/wekan/wekan/commit/8da0f34ee6ca66884ab8a2dcdf1a724a0c9fcc81).
Thanks to xet7.
- [Removed exclude-archs from rebuild-wekan.sh, it is not necessary anymore](https://github.com/wekan/wekan/commit/028b404377651d34e334b048ecc960d3ff5b2eab).
Thanks to xet7.
- [Fix 8.72 Bug: LDAP login does not work. ReactiveCache is not defined](https://github.com/wekan/wekan/commit/bcbf4771ea88fb617ddc44650c8ac139b0cfc998).
Thanks to poc-sm and xet7.
- [Fix Error in choosing member or assignee in a card](https://github.com/wekan/wekan/commit/7f0c42cca555dd2dca04d3b6413c0080ea75e96e).
Thanks to hmeunier95 and xet7.
- [Fix Search All Boards result opened card missing Copy Card Link etc that can be used to open correct card](https://github.com/wekan/wekan/commit/304a007d078c04dac7de5074a856c4f0cc2c0aa7).
Thanks bcook-konza and xet7.
- [Fix Checklists add and Subtasks reorder. Fix Board Activity part 1](https://github.com/wekan/wekan/commit/08ebad0fe4a2b8a7b93d75a9a9c2317ce9374fea).
Thanks bcook-konza and xet7.
- [Fix Change list inside or card](https://github.com/wekan/wekan/commit/aea8c285c246b490ac259ceafe7d9f733681c20f).
Thanks to mimZD and xet7.
- [Fix board and card activities. Part 2](https://github.com/wekan/wekan/commit/c7408ad8d147f36cfdbb71089af688bef91c3ecf).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.72 2026-04-07 WeKan ® release
This release adds the following new features and fixes:
- [Fix Impersonate User. Show/Edit Avatar at Admin Panel/People/People](https://github.com/wekan/wekan/commit/3ac7c9057e1e53da9f285149ce832ad430b3bc30).
Thanks to ahlgrimma, DnT1mmer, robbagithub, norrig and xet7.
and adds the following updates:
- [Bump actions/download-artifact from 4 to 8](https://github.com/wekan/wekan/pull/6247).
Thanks to dependabot.
- [Bump actions/upload-artifact from 4 to 7](https://github.com/wekan/wekan/pull/6248).
Thanks to dependabot.
- [Bump actions/cache from 4 to 5](https://github.com/wekan/wekan/pull/6249).
Thanks to dependabot.
- [Bump docker/login-action from 4.0.0 to 4.1.0](https://github.com/wekan/wekan/pull/6250).
Thanks to dependabot.
- [Updated release scripts](https://github.com/wekan/wekan/commit/87f592c67ffb1962ec8acb0d1676b8df6ac42a5d).
Thanks to xet7.
and fixes the following bugs:
- [Fix Admin Panel / Version not showing name of MongoDB storage engine and status of MongoDB Oplog enabled](https://github.com/wekan/wekan/commit/3b98f862bcda3e8a9b0d0131b91cdfa59986c2d8).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.71 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Fix: Added missing null check to notification](https://github.com/wekan/wekan/commit/5df924f416083a50795443687e1c28f402dc8132).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.70 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Convert upsert calls to async in authentication.js](https://github.com/wekan/wekan/commit/dfcaaf0ae719e8d8b918ac632100293a6e401371).
Thanks to snowsky and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.69 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Try to fix Snap to start MongoDB OpLog correctly](https://github.com/wekan/wekan/commit/fbdfcead50fb599d4fafecc2f5847c46c73adf72).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.68 2026-04-06 WeKan ® release
This release fixes the following bugs:
- [Try to fix Snap to start MongoDB OpLog correctly](https://github.com/wekan/wekan/commit/9e316662800fc3b5e38b1176e70a50b18d73d2a6).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.67 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/7707a5b7fc21f981f26c512cd3fea6171a57bd46).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.65 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Fix Docker Actions Test suite](https://github.com/wekan/wekan/commit/c0661506c42ac412ae9e8fc5392d160efbc446ea).
Thanks to xet7.
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/d3155d24c64fe1104afe454b756d2a6324fab5c2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.64 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/7a7909b49feaaa938b5dbfc2083b0c719187859d).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.63 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/6a78a178e2fcff9ff280ba44c123403e2792bb7b).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.62 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Try to fix GitHub Actions release bundle builds](https://github.com/wekan/wekan/commit/cd55c0e12ed80ddcb07895f2d787be95c440f991).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.61 2026-04-05 WeKan ® release
This release adds the following updates:
- [Updated Docker build script to check directories and repos exist](https://github.com/wekan/wekan/commit/81319fc48bbd2a91553090428455bc11aff24fbc).
Thanks to xet7.
- [Updated release scripts: Delete all GitHub Actions Caches, so that old stuff does not break builds](https://github.com/wekan/wekan/commit/f3a97b993842425e4f13bb78e5939a069d64887e).
Thanks to xet7.
and fixes the following bugs:
- [Fix: Server crash during comment deletion](https://github.com/wekan/wekan/pull/6246).
Thanks to KhaoulaMaleh.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.60 2026-04-05 WeKan ® release
This release adds the following updates:
- [Updated Docker build script](https://github.com/wekan/wekan/commit/0b59f87887c215dc34093409ccd12da9acd3ef02).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.59 2026-04-05 WeKan ® release
This release fixes the following bugs:
- [Fix docker-compose.yml to use OpLog](https://github.com/wekan/wekan/commit/30a1792c92de9d485c0b9ed3970a1187ca974576).
Thanks to xet7.
- [Fix Snap to use OpLog](https://github.com/wekan/wekan/commit/1fa72fd34ab4547e97e7e0c201bc3d50d478ef5e).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v8.58 2026-04-05 WeKan ® release
+4 -5
View File
@@ -7,18 +7,17 @@ LABEL org.opencontainers.image.source="https://github.com/wekan/wekan"
# TARGETARCH and TARGETVARIANT are automatically provided by Docker Buildx
ARG TARGETARCH
ARG TARGETVARIANT
ARG VERSION=8.58
ARG VERSION=8.78
ARG DEBIAN_FRONTEND=noninteractive
ENV BUILD_DEPS="apt-utils gnupg wget bzip2 g++ curl libarchive-tools build-essential git ca-certificates python3 unzip"
ENV \
DEBUG=false \
NODE_VERSION=v22.22.2 \
METEOR_RELEASE=METEOR@3.4 \
NODE_VERSION=v24.14.1 \
METEOR_RELEASE=METEOR@3.5-beta.7 \
USE_EDGE=false \
METEOR_EDGE=1.5-beta.17 \
NPM_VERSION=10.9.2 \
NPM_VERSION=11.11.0 \
SRC_PATH=./ \
WITH_API=true \
MONGO_OPLOG_URL="" \
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v8.58.0"
appVersion: "v8.78.0"
files:
userUploads:
- README.md
+2 -2
View File
@@ -9,11 +9,11 @@ template(name="activities")
+cardActivities
template(name="boardActivities")
each activityData in currentBoard.activities
each activityData in boardActivitiesList
+activity(activity=activityData card=card mode=mode)
template(name="cardActivities")
each activityData in activities
each activityData in cardActivitiesList
+activity(activity=activityData card=card mode=mode)
template(name="activity")
+27 -2
View File
@@ -76,7 +76,9 @@ function _showActivities(data) {
const currentCard = Utils.getCurrentCard();
ret = currentCard.showActivities ?? false;
} else if (mode === 'card') {
ret = data?.card?.showActivities ?? false;
// For card mode, get showActivities from data context or fallback to current card
const card = data?.card || Utils.getCurrentCard();
ret = card?.showActivities ?? false;
} else {
ret = Utils.getCurrentBoard().showActivities ?? false;
}
@@ -85,8 +87,31 @@ function _showActivities(data) {
}
Template.activities.helpers({
showActivities() {
const data = Template.currentData();
return _showActivities(data);
},
activities() {
return this.card.activities();
const activities = this.card?.activities?.();
return activities || [];
},
});
Template.boardActivities.helpers({
boardActivitiesList() {
const board = Utils.getCurrentBoard();
const activities = board?.activities?.();
return activities || [];
},
});
Template.cardActivities.helpers({
cardActivitiesList() {
const data = Template.currentData();
const card = data?.card;
const activities = card?.activities?.();
return activities || [];
},
});
+4 -4
View File
@@ -48,7 +48,7 @@ Template.editCardReceivedDatePopup.helpers(datePickerHelpers());
Template.editCardReceivedDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setReceived(formatDateTime(date));
this.datePicker.card.setReceived(date);
},
deleteDate() {
this.datePicker.card.unsetReceived();
@@ -72,7 +72,7 @@ Template.editCardStartDatePopup.helpers(datePickerHelpers());
Template.editCardStartDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setStart(formatDateTime(date));
this.datePicker.card.setStart(date);
},
deleteDate() {
this.datePicker.card.unsetStart();
@@ -96,7 +96,7 @@ Template.editCardDueDatePopup.helpers(datePickerHelpers());
Template.editCardDueDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setDue(formatDateTime(date));
this.datePicker.card.setDue(date);
},
deleteDate() {
this.datePicker.card.unsetDue();
@@ -120,7 +120,7 @@ Template.editCardEndDatePopup.helpers(datePickerHelpers());
Template.editCardEndDatePopup.events(datePickerEvents({
storeDate(date) {
this.datePicker.card.setEnd(formatDateTime(date));
this.datePicker.card.setEnd(date);
},
deleteDate() {
this.datePicker.card.unsetEnd();
+23 -24
View File
@@ -11,31 +11,30 @@ template(name="cardDetails")
+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'}}")
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
if cardCollapsed
i.fa.fa-caret-right
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
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"
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'}}
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'}}")
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"
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
+32 -17
View File
@@ -207,22 +207,22 @@ Template.cardDetails.onRendered(function () {
EscapeActions.executeUpTo('popup-close');
},
stop(evt, ui) {
let prevChecklist = ui.item.prev('.js-subtasks').get(0);
if (prevChecklist) {
prevChecklist = Blaze.getData(prevChecklist).subtask;
let prevSubtask = ui.item.prev('.js-subtasks').get(0);
if (prevSubtask) {
prevSubtask = Blaze.getData(prevSubtask).subtask;
}
let nextChecklist = ui.item.next('.js-subtasks').get(0);
if (nextChecklist) {
nextChecklist = Blaze.getData(nextChecklist).subtask;
let nextSubtask = ui.item.next('.js-subtasks').get(0);
if (nextSubtask) {
nextSubtask = Blaze.getData(nextSubtask).subtask;
}
const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
const sortIndex = calculateIndexData(prevSubtask, nextSubtask, 1);
$subtasksDom.sortable('cancel');
const subtask = Blaze.getData(ui.item.get(0)).subtask;
Subtasks.update(subtask._id, {
Cards.updateAsync(subtask._id, {
$set: {
subtaskSort: sortIndex.base,
sort: sortIndex.base,
},
});
},
@@ -272,8 +272,8 @@ Template.cardDetails.helpers({
},
showActivities() {
const user = ReactiveCache.getCurrentUser();
return user && user.hasShowActivities();
const card = Template.currentData();
return card && card.showActivities;
},
cardCollapsed() {
@@ -337,7 +337,12 @@ Template.cardDetails.helpers({
},
isCurrentListId(listId) {
const data = Template.currentData();
// When called from within 'each currentBoard.lists' block, we need the card from parent context
let data = Template.currentData();
if (!data || typeof data.listId === 'undefined') {
// Try parent context (for use within each loops)
data = Template.parentData(1);
}
if (!data || typeof data.listId === 'undefined') return false;
return data.listId == listId;
},
@@ -556,9 +561,8 @@ Template.cardDetails.events({
}
},
async 'change .js-select-card-details-lists'(event, tpl) {
const listId = event.target.value;
let card = Template.currentData();
const listSelect = tpl.$('.js-select-card-details-lists')[0];
const listId = listSelect.options[listSelect.selectedIndex].value;
const minOrder = await card.getMinSort(listId, card.swimlaneId);
await card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
@@ -602,6 +606,10 @@ Template.cardDetails.events({
const card = Template.currentData();
await card.toggleHideCheckedChecklistItems();
},
async 'change #toggleShowActivitiesCard'() {
const card = Template.currentData();
await card.toggleShowActivities();
},
'click #toggleCustomFieldsGridButton'() {
Meteor.call('toggleCustomFieldsGrid');
},
@@ -1096,14 +1104,21 @@ const filterMembers = (filterTerm) => {
let members = currBoard.activeMembers();
if (filterTerm) {
const searchTerm = filterTerm.toLowerCase();
members = members
.map(member => ({
member,
user: ReactiveCache.getUser(member.userId)
}))
.filter(({ user }) =>
(user.profile.fullname !== undefined && user.profile.fullname.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
|| user.profile.fullname === undefined && user.profile.username !== undefined && user.profile.username.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
.filter(({ user }) => {
// Check if user data is available
if (!user || !user.profile) {
return false;
}
const fullname = (user.profile.fullname || '').toLowerCase();
const username = (user.username || '').toLowerCase();
return fullname.indexOf(searchTerm) !== -1 || username.indexOf(searchTerm) !== -1;
})
.map(({ member }) => member);
}
return members;
+1 -1
View File
@@ -223,7 +223,7 @@ Template.checklists.events({
event.preventDefault();
Template.currentData().card.toggleHideFinishedChecklist();
},
keydown(event) {
'keydown textarea.js-add-checklist-item'(event) {
//If user press enter key inside a form, submit it
//Unless the user is also holding down the 'shift' key
if (event.keyCode === 13 && !event.shiftKey) {
+33 -27
View File
@@ -11,17 +11,21 @@ template(name="minicard")
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
.dates
if getReceived
.date
+minicardReceivedDate
if getStart
.date
+minicardStartDate
if getDue
.date
+minicardDueDate
if getEnd
+minicardEndDate
if showReceived
if getReceived
.date
+minicardReceivedDate
if showStart
if getStart
.date
+minicardStartDate
if showDue
if getDue
.date
+minicardDueDate
if showEnd
if getEnd
+minicardEndDate
if getSpentTime
.date
+cardSpentTime
@@ -68,19 +72,20 @@ template(name="minicard")
span.linked-icon.linked-archived
i.fa.fa-archive
+viewer
if currentBoard.allowsCardNumber
if showCardNumber
span.card-number
| ##{getCardNumber}
= getTitle
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
unless hiddenMinicardLabelText
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if hiddenMinicardLabelText
.minicard-label(class="card-label-{{color}}" title="{{name}}")
if showLabels
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
unless hiddenMinicardLabelText
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if hiddenMinicardLabelText
.minicard-label(class="card-label-{{color}}" title="{{name}}")
.minicard-custom-fields
each customFieldsWD
@@ -179,12 +184,13 @@ template(name="minicard")
span.badge-icon
i.fa.fa-paperclip
span.badge-text= attachments.length
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if showSubtasks
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
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
+30 -12
View File
@@ -50,32 +50,50 @@ Template.minicard.helpers({
},
showMembers() {
// cache "board" to reduce the mini-mongodb access
const board = this.board();
let ret = false;
if (board) {
ret =
board.allowsMembers === null ||
board.allowsMembers === undefined ||
board.allowsMembers
;
ret = board.allowsMembersOnMinicard ?? false;
}
return ret;
},
showAssignee() {
// cache "board" to reduce the mini-mongodb access
const board = this.board();
let ret = false;
if (board) {
ret =
board.allowsAssignee === null ||
board.allowsAssignee === undefined ||
board.allowsAssignee
;
ret = board.allowsAssigneeOnMinicard ?? false;
}
return ret;
},
showReceived() {
const board = this.board();
return board ? board.allowsReceivedDateOnMinicard : false;
},
showStart() {
const board = this.board();
return board ? board.allowsStartDateOnMinicard : false;
},
showDue() {
const board = this.board();
return board ? board.allowsDueDateOnMinicard : false;
},
showEnd() {
const board = this.board();
return board ? board.allowsEndDateOnMinicard : false;
},
showLabels() {
const board = this.board();
return board ? board.allowsLabelsOnMinicard : false;
},
showCardNumber() {
const board = this.board();
return board ? board.allowsCardNumberOnMinicard : false;
},
showSubtasks() {
const board = this.board();
return board ? board.allowsSubtasksOnMinicard : false;
},
hiddenMinicardLabelText() {
const currentUser = ReactiveCache.getCurrentUser();
+15 -4
View File
@@ -15,7 +15,7 @@ textarea.js-edit-subtask-item {
color: #8c8c8c;
text-decoration: underline;
word-wrap: break-word;
float: right;
flex-shrink: 0;
padding-top: 6px;
}
.delete-text:hover,
@@ -24,8 +24,13 @@ textarea.js-edit-subtask-item {
.js-delete-subtask-item:hover {
color: inherit;
}
.subtask-title {
display: flex;
align-items: center;
width: 100%;
}
.subtask-title .checkbox {
float: left;
flex-shrink: 0;
width: 30px;
height: 30px;
font-size: 18px;
@@ -34,10 +39,12 @@ textarea.js-edit-subtask-item {
.subtask-title .title {
font-size: 18px;
line-height: 25px;
flex: 1;
order: 1;
}
.subtask-title .subtasks-stat {
margin: 0 0.5em;
float: right;
flex-shrink: 0;
padding-top: 6px;
}
.subtask-title .subtasks-stat.is-finished {
@@ -46,6 +53,9 @@ textarea.js-edit-subtask-item {
.subtask-title .js-delete-subtask {
margin: 0 0.5em;
}
.subtask-title .js-view-subtask {
margin: 0 0.5em;
}
.js-confirm-subtask-delete {
background-color: #f7f7f7;
position: absolute;
@@ -175,6 +185,7 @@ body.grey-icons-enabled .subtasks-item .check-box-unicode {
display: inline-block;
}
.subtask-details-menu {
float: right;
flex-shrink: 0;
padding: 6px 10px 6px 10px;
order: 2;
}
+14 -5
View File
@@ -4,7 +4,14 @@ import Cards from '/models/cards';
import { Filter } from '/client/lib/filter';
Template.subtasks.events({
'click .js-open-subtask-details-menu': Popup.open('subtaskActions'),
'click .js-open-subtask-details-menu'(event) {
// Close any existing popup first to avoid accumulating content
if (Popup.isOpen()) {
Popup.close();
}
// Now open the popup for this specific subtask
Popup.open('subtaskActions').call(this, event);
},
async 'submit .js-add-subtask'(event, tpl) {
event.preventDefault();
const textarea = tpl.find('textarea.js-add-subtask-item');
@@ -30,10 +37,12 @@ Template.subtasks.events({
_id: card.swimlaneId,
});
//find the swimlane of the same name in the target board.
const targetSwimlane = ReactiveCache.getSwimlane({
boardId: targetBoard._id,
title: parentSwimlane.title,
});
const targetSwimlane = parentSwimlane
? ReactiveCache.getSwimlane({
boardId: targetBoard._id,
title: parentSwimlane.title,
})
: undefined;
//If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
const swimlaneId =
targetSwimlane === undefined
@@ -37,7 +37,7 @@ template(name='statistics')
td {{statistics.mongo.mongoStorageEngine}}
tr
th {{_ 'MongoDB_Oplog_enabled'}}
td {{statistics.mongo.mongoOplogEnabled}}
td {{formatBoolean statistics.mongo.mongoOplogEnabled}}
tr
th {{_ 'OS_Type'}}
td {{statistics.os.type}}
@@ -47,4 +47,8 @@ Template.statistics.helpers({
}
return ret;
},
formatBoolean(value) {
return value ? TAPi18n.__('yes') : TAPi18n.__('no');
},
});
+43
View File
@@ -311,3 +311,46 @@ table td:first-child {
text-align: center;
color: #333;
}
/* Avatar styling - removed people-avatar column */
/* Username column styling - displays avatar inline with username */
.username a.js-edit-people-avatar {
display: inline-flex;
align-items: center;
cursor: pointer;
text-decoration: none;
margin-right: 8px;
vertical-align: middle;
}
.username a.js-edit-people-avatar:hover {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 50%;
}
.username .avatar {
height: 30px;
width: 30px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
overflow: hidden;
}
.username .avatar.avatar-initials {
background-color: #dbdbdb;
color: #444;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
}
.username .avatar img.avatar-image {
object-fit: cover;
object-position: center;
height: 100%;
width: 100%;
}
+35 -14
View File
@@ -114,7 +114,7 @@ template(name="orgGeneral")
th {{_ 'shortName'}}
th {{_ 'website'}}
th {{_ 'createdAt'}}
th {{_ 'active'}}
th {{_ 'active-org'}}
tbody
tr
each org in orgList
@@ -137,7 +137,7 @@ template(name="teamGeneral")
th {{_ 'shortName'}}
th {{_ 'website'}}
th {{_ 'createdAt'}}
th {{_ 'active'}}
th {{_ 'active-team'}}
tbody
tr
each team in teamList
@@ -160,7 +160,7 @@ template(name="peopleGeneral")
th {{_ 'username'}}
th {{_ 'email'}}
th {{_ 'admin'}}
th {{_ 'admin-people-active-status'}}
th {{_ 'active-person'}}
th {{_ 'accounts-lockout-status'}}
th {{_ 'createdAt'}}
th
@@ -260,17 +260,38 @@ template(name="teamRow")
template(name="peopleRow")
tr
td
a.edit-user
a.edit-user(data-user-id=userData._id)
i.fa.fa-pencil-square-o
| {{_ 'edit'}}
a.more-settings-user
a.more-settings-user(data-user-id=userData._id)
| ⋯
if userData.loginDisabled
td.username <s>{{ userData.username }}</s>
td.username
a.js-edit-people-avatar(data-user-id=userData._id)
if hasAvatarUrl
.avatar
img.avatar-image(src="{{userData.profile.avatarUrl}}", alt="{{userData.username}}", title="{{_ 'change-avatar'}}")
else
+userAvatarInitials(userId=userData._id)
<s>{{ userData.username }}</s>
else if isUserLocked
td.username {{ userData.username }}
td.username
a.js-edit-people-avatar(data-user-id=userData._id)
if hasAvatarUrl
.avatar
img.avatar-image(src="{{userData.profile.avatarUrl}}", alt="{{userData.username}}", title="{{_ 'change-avatar'}}")
else
+userAvatarInitials(userId=userData._id)
| {{ userData.username }}
else
td.username {{ userData.username }}
td.username
a.js-edit-people-avatar(data-user-id=userData._id)
if hasAvatarUrl
.avatar
img.avatar-image(src="{{userData.profile.avatarUrl}}", alt="{{userData.username}}", title="{{_ 'change-avatar'}}")
else
+userAvatarInitials(userId=userData._id)
| {{ userData.username }}
if userData.loginDisabled
td <s>{{ userData.emails.[0].address }}</s>
else
@@ -333,7 +354,7 @@ template(name="editOrgPopup")
| {{_ 'website'}}
input.js-orgWebsite(type="text" value=org.orgWebsite)
label
| {{_ 'active'}}
| {{_ 'active-org'}}
select.select-active.js-org-isactive
option(value="false") {{_ 'no'}}
option(value="true" selected="{{org.orgIsActive}}") {{_ 'yes'}}
@@ -359,7 +380,7 @@ template(name="editTeamPopup")
| {{_ 'website'}}
input.js-teamWebsite(type="text" value=team.teamWebsite)
label
| {{_ 'active'}}
| {{_ 'active-team'}}
select.select-active.js-team-isactive
option(value="false") {{_ 'no'}}
option(value="true" selected="{{team.teamIsActive}}") {{_ 'yes'}}
@@ -406,7 +427,7 @@ template(name="editUserPopup")
option(value="false") {{_ 'no'}}
option(value="true" selected="{{userData.emails.[0].verified}}") {{_ 'yes'}}
label
| {{_ 'active'}}
| {{_ 'active-person'}}
select.select-active.js-profile-isactive
option(value="false") {{_ 'yes'}}
option(value="true" selected="{{user.loginDisabled}}") {{_ 'no'}}
@@ -469,7 +490,7 @@ template(name="newOrgPopup")
| {{_ 'website'}}
input.js-orgWebsite(type="text" value="" required)
label
| {{_ 'active'}}
| {{_ 'active-org'}}
select.select-active.js-org-isactive
option(value="false" selected="selected") {{_ 'no'}}
option(value="true") {{_ 'yes'}}
@@ -493,7 +514,7 @@ template(name="newTeamPopup")
| {{_ 'website'}}
input.js-teamWebsite(type="text" value="")
label
| {{_ 'active'}}
| {{_ 'active-team'}}
select.select-active.js-team-isactive
option(value="false" selected="selected") {{_ 'no'}}
option(value="true") {{_ 'yes'}}
@@ -555,7 +576,7 @@ template(name="newUserPopup")
option(value="false" selected="selected") {{_ 'no'}}
option(value="true") {{_ 'yes'}}
label
| {{_ 'active'}}
| {{_ 'active-person'}}
select.select-active.js-profile-isactive
option(value="false" selected="selected") {{_ 'yes'}}
option(value="true") {{_ 'no'}}
+77 -11
View File
@@ -1,4 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { avatarUpdateCounter } from '/client/components/users/avatarUpdateCounter';
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
import LockoutSettings from '/models/lockoutSettings';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
@@ -536,7 +537,39 @@ Template.teamRow.helpers({
Template.peopleRow.helpers({
userData() {
return this.user || ReactiveCache.getUser(this.userId);
// Depend on global avatar update counter to reactively update when avatars change
avatarUpdateCounter.get();
// Get the user ID from either this._id or this.user._id
let userId;
if (this.user && this.user._id) {
userId = this.user._id;
} else if (this._id) {
userId = this._id;
}
// Always fetch from ReactiveCache to ensure latest data
if (userId) {
return ReactiveCache.getUser(userId);
}
return this.user || this;
},
hasAvatarUrl() {
// Depend on global avatar update counter to reactively update when avatars change
avatarUpdateCounter.get();
// Get the user ID from either this._id or this.user._id
let userId;
if (this.user && this.user._id) {
userId = this.user._id;
} else if (this._id) {
userId = this._id;
}
let user;
if (userId) {
user = ReactiveCache.getUser(userId);
} else {
user = this.user || this;
}
if (!user || !user.profile) return false;
return !!user.profile.avatarUrl;
},
isUserLocked() {
const user = this.user || ReactiveCache.getUser(this.userId);
@@ -726,8 +759,20 @@ Template.teamRow.events({
});
Template.peopleRow.events({
'click a.edit-user': Popup.open('editUser'),
'click a.more-settings-user': Popup.open('settingsUser'),
'click a.edit-user'(event) {
// Get the user ID from the data attribute
const userId = event.currentTarget.getAttribute('data-user-id');
if (userId) {
Popup.open('editUser').call({ userId: userId }, event);
}
},
'click a.more-settings-user'(event) {
// Get the user ID from the data attribute
const userId = event.currentTarget.getAttribute('data-user-id');
if (userId) {
Popup.open('settingsUser').call({ userId: userId }, event);
}
},
'click .selectUserChkBox': function(ev){
if(ev.currentTarget){
if(ev.currentTarget.checked){
@@ -791,6 +836,18 @@ Template.peopleRow.events({
// For now, we'll leave this as a no-op
}
},
'click a.js-edit-people-avatar'(event) {
// Extract the user ID from the data attribute
const userId = event.currentTarget.getAttribute('data-user-id');
if (userId) {
// Get the user from cache to pass correct context
const user = ReactiveCache.getUser(userId);
if (user) {
// Call Popup.open with the correct user data context
Popup.open('adminChangeAvatar').call({ _id: userId, user: user }, event);
}
}
},
});
Template.modifyTeamsUsers.helpers({
@@ -1462,19 +1519,23 @@ Template.settingsTeamPopup.events({
Template.settingsUserPopup.events({
'click .impersonate-user'(event) {
event.preventDefault();
const userId = this.userId || this.user?._id;
Meteor.call('impersonate', this.userId, (err) => {
Meteor.call('impersonate', userId, (err) => {
if (!err) {
// Meteor.connection.setUserId() triggers automatic cache invalidation
// No need to manually invalidate - let Meteor handle the user data refresh
Meteor.connection.setUserId(userId);
FlowRouter.go('/');
Meteor.connection.setUserId(this.userId);
}
});
},
'click #deleteButton'(event) {
event.preventDefault();
const userId = this.userId || this.user?._id;
// Use secure server method instead of direct client-side removal
Meteor.call('removeUser', this.userId, (error, result) => {
Meteor.call('removeUser', userId, (error, result) => {
if (error) {
if (process.env.DEBUG === 'true') {
console.error('Error removing user:', error);
@@ -1501,19 +1562,24 @@ Template.settingsUserPopup.events({
Template.settingsUserPopup.helpers({
user() {
return ReactiveCache.getUser(this.userId);
const userId = this.userId || this.user?._id;
return ReactiveCache.getUser(userId);
},
authentications() {
return Template.instance().authenticationMethods.get();
},
isSelected(match) {
const userId = Template.instance().data.userId;
const selected = ReactiveCache.getUser(userId).authenticationMethod;
const userId = Template.instance().data.userId || Template.instance().data.user?._id;
const user = ReactiveCache.getUser(userId);
if (!user) return false;
const selected = user.authenticationMethod;
return selected === match;
},
isLdap() {
const userId = Template.instance().data.userId;
const selected = ReactiveCache.getUser(userId).authenticationMethod;
const userId = Template.instance().data.userId || Template.instance().data.user?._id;
const user = ReactiveCache.getUser(userId);
if (!user) return false;
const selected = user.authenticationMethod;
return selected === 'ldap';
},
errorMessage() {
+30 -30
View File
@@ -177,8 +177,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsReceivedDate}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-receiveddate(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsReceivedDate}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-receiveddate-on-minicard(title="{{_ 'card-received'}}" class="{{#if allowsReceivedDateOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsReceivedDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-sign-in
@@ -188,8 +188,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsStartDate}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-startdate(title="{{_ 'card-start'}}" class="{{#if allowsStartDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsStartDate}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-startdate-on-minicard(title="{{_ 'card-start'}}" class="{{#if allowsStartDateOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsStartDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-hourglass
@@ -199,8 +199,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsDueDate}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-duedate(title="{{_ 'card-due'}}" class="{{#if allowsDueDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsDueDate}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-duedate-on-minicard(title="{{_ 'card-due'}}" class="{{#if allowsDueDateOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsDueDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-sign-in
@@ -210,8 +210,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsEndDate}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-enddate(title="{{_ 'card-end'}}" class="{{#if allowsEndDate}}is-checked{{/if}}")
i.fa(class="{{#if allowsEndDate}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-enddate-on-minicard(title="{{_ 'card-end'}}" class="{{#if allowsEndDateOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsEndDateOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-clock-o
@@ -221,8 +221,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
i.fa(class="{{#if allowsMembers}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-members(title="{{_ 'members'}}" class="{{#if allowsMembers}}is-checked{{/if}}")
i.fa(class="{{#if allowsMembers}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-members-on-minicard(title="{{_ 'members'}}" class="{{#if allowsMembersOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsMembersOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-users
@@ -252,8 +252,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
i.fa(class="{{#if allowsAssignee}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-assignee(title="{{_ 'assignee'}}" class="{{#if allowsAssignee}}is-checked{{/if}}")
i.fa(class="{{#if allowsAssignee}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-assignee-on-minicard(title="{{_ 'assignee'}}" class="{{#if allowsAssigneeOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsAssigneeOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-user
@@ -263,8 +263,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
i.fa(class="{{#if allowsAssignedBy}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-assigned-by(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedBy}}is-checked{{/if}}")
i.fa(class="{{#if allowsAssignedBy}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-assigned-by-on-minicard(title="{{_ 'assigned-by'}}" class="{{#if allowsAssignedByOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsAssignedByOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-shopping-cart
@@ -274,8 +274,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
i.fa(class="{{#if allowsRequestedBy}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-requested-by(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedBy}}is-checked{{/if}}")
i.fa(class="{{#if allowsRequestedBy}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-requested-by-on-minicard(title="{{_ 'requested-by'}}" class="{{#if allowsRequestedByOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsRequestedByOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-user
@@ -316,8 +316,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
i.fa(class="{{#if allowsLabels}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-labels(title="{{_ 'labels'}}" class="{{#if allowsLabels}}is-checked{{/if}}")
i.fa(class="{{#if allowsLabels}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-labels-on-minicard(title="{{_ 'labels'}}" class="{{#if allowsLabelsOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsLabelsOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-tag
@@ -337,8 +337,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
i.fa(class="{{#if allowsCardNumber}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
i.fa(class="{{#if allowsCardNumber}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-card-number-on-minicard(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumberOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsCardNumberOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
| #️⃣
@@ -349,8 +349,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
i.fa(class="{{#if allowsDescriptionTitle}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-description-title(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
i.fa(class="{{#if allowsDescriptionTitle}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-description-title-on-minicard(title="{{_ 'description'}} {{_ 'title'}}" class="{{#if allowsDescriptionTitleOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsDescriptionTitleOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-file-text-o
@@ -361,8 +361,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
i.fa(class="{{#if allowsDescriptionText}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-description-text(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionText}}is-checked{{/if}}")
i.fa(class="{{#if allowsDescriptionText}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-description-text-on-minicard(title="{{_ 'description'}} {{_ 'custom-field-text'}}" class="{{#if allowsDescriptionTextOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsDescriptionTextOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-file-text-o
@@ -383,8 +383,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
i.fa(class="{{#if allowsChecklists}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-checklists(title="{{_ 'checklists'}}" class="{{#if allowsChecklists}}is-checked{{/if}}")
i.fa(class="{{#if allowsChecklists}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-checklists-on-minicard(title="{{_ 'checklists'}}" class="{{#if allowsChecklistsOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsChecklistsOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-check
@@ -394,8 +394,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
i.fa(class="{{#if allowsSubtasks}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-subtasks(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasks}}is-checked{{/if}}")
i.fa(class="{{#if allowsSubtasks}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-subtasks-on-minicard(title="{{_ 'subtasks'}}" class="{{#if allowsSubtasksOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsSubtasksOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-globe
@@ -405,8 +405,8 @@ template(name="boardCardSettingsPopup")
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
i.fa(class="{{#if allowsAttachments}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-attachments(title="{{_ 'attachments'}}" class="{{#if allowsAttachments}}is-checked{{/if}}")
i.fa(class="{{#if allowsAttachments}}fa-check{{else}}fa-square-o{{/if}}")
a.flex.js-field-has-attachments-on-minicard(title="{{_ 'attachments'}}" class="{{#if allowsAttachmentsOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsAttachmentsOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-paperclip
+142 -2
View File
@@ -235,8 +235,8 @@ Template.homeSidebar.helpers({
});
Template.homeSidebar.events({
'click .js-toggle-show-activities'() {
Utils.getCurrentBoard().toggleShowActivities();
async 'click .js-toggle-show-activities'() {
await Utils.getCurrentBoard().toggleShowActivities();
},
});
@@ -1029,26 +1029,51 @@ Template.boardCardSettingsPopup.helpers({
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsReceivedDate : false;
},
allowsReceivedDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsReceivedDateOnMinicard : false;
},
allowsStartDate() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsStartDate : false;
},
allowsStartDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsStartDateOnMinicard : false;
},
allowsDueDate() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDueDate : false;
},
allowsDueDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDueDateOnMinicard : false;
},
allowsEndDate() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsEndDate : false;
},
allowsEndDateOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsEndDateOnMinicard : false;
},
allowsSubtasks() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsSubtasks : false;
},
allowsSubtasksOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsSubtasksOnMinicard : false;
},
allowsCreator() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
@@ -1064,21 +1089,41 @@ Template.boardCardSettingsPopup.helpers({
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsMembers : false;
},
allowsMembersOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsMembersOnMinicard : false;
},
allowsAssignee() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignee : false;
},
allowsAssigneeOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssigneeOnMinicard : false;
},
allowsAssignedBy() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignedBy : false;
},
allowsAssignedByOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignedByOnMinicard : false;
},
allowsRequestedBy() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsRequestedBy : false;
},
allowsRequestedByOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsRequestedByOnMinicard : false;
},
allowsCardSortingByNumber() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
@@ -1094,6 +1139,11 @@ Template.boardCardSettingsPopup.helpers({
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsLabels : false;
},
allowsLabelsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsLabelsOnMinicard : false;
},
allowsShowListsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
@@ -1104,11 +1154,21 @@ Template.boardCardSettingsPopup.helpers({
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsChecklists : false;
},
allowsChecklistsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsChecklistsOnMinicard : false;
},
allowsAttachments() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAttachments : false;
},
allowsAttachmentsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAttachmentsOnMinicard : false;
},
allowsComments() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
@@ -1119,11 +1179,21 @@ Template.boardCardSettingsPopup.helpers({
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardNumber : false;
},
allowsCardNumberOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardNumberOnMinicard : false;
},
allowsDescriptionTitle() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTitle : false;
},
allowsDescriptionTitleOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTitleOnMinicard : false;
},
allowsDescriptionText() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
@@ -1213,26 +1283,51 @@ Template.boardCardSettingsPopup.events({
const newValue = !tpl.currentBoard.allowsReceivedDate;
Boards.update(tpl.currentBoard._id, { $set: { allowsReceivedDate: newValue } });
},
'click .js-field-has-receiveddate-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsReceivedDateOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsReceivedDateOnMinicard: newValue } });
},
'click .js-field-has-startdate'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsStartDate;
Boards.update(tpl.currentBoard._id, { $set: { allowsStartDate: newValue } });
},
'click .js-field-has-startdate-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsStartDateOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsStartDateOnMinicard: newValue } });
},
'click .js-field-has-enddate'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsEndDate;
Boards.update(tpl.currentBoard._id, { $set: { allowsEndDate: newValue } });
},
'click .js-field-has-enddate-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsEndDateOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsEndDateOnMinicard: newValue } });
},
'click .js-field-has-duedate'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsDueDate;
Boards.update(tpl.currentBoard._id, { $set: { allowsDueDate: newValue } });
},
'click .js-field-has-duedate-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsDueDateOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsDueDateOnMinicard: newValue } });
},
'click .js-field-has-subtasks'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsSubtasks;
Boards.update(tpl.currentBoard._id, { $set: { allowsSubtasks: newValue } });
},
'click .js-field-has-subtasks-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsSubtasksOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsSubtasksOnMinicard: newValue } });
},
'click .js-field-has-creator'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsCreator;
@@ -1248,21 +1343,41 @@ Template.boardCardSettingsPopup.events({
const newValue = !tpl.currentBoard.allowsMembers;
Boards.update(tpl.currentBoard._id, { $set: { allowsMembers: newValue } });
},
'click .js-field-has-members-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsMembersOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsMembersOnMinicard: newValue } });
},
'click .js-field-has-assignee'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsAssignee;
Boards.update(tpl.currentBoard._id, { $set: { allowsAssignee: newValue } });
},
'click .js-field-has-assignee-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsAssigneeOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsAssigneeOnMinicard: newValue } });
},
'click .js-field-has-assigned-by'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsAssignedBy;
Boards.update(tpl.currentBoard._id, { $set: { allowsAssignedBy: newValue } });
},
'click .js-field-has-assigned-by-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsAssignedByOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsAssignedByOnMinicard: newValue } });
},
'click .js-field-has-requested-by'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsRequestedBy;
Boards.update(tpl.currentBoard._id, { $set: { allowsRequestedBy: newValue } });
},
'click .js-field-has-requested-by-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsRequestedByOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsRequestedByOnMinicard: newValue } });
},
'click .js-field-has-card-sorting-by-number'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsCardSortingByNumber;
@@ -1278,6 +1393,11 @@ Template.boardCardSettingsPopup.events({
const newValue = !tpl.currentBoard.allowsLabels;
Boards.update(tpl.currentBoard._id, { $set: { allowsLabels: newValue } });
},
'click .js-field-has-labels-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsLabelsOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsLabelsOnMinicard: newValue } });
},
'click .js-field-has-card-show-lists-on-minicard'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsShowListsOnMinicard = !tpl.currentBoard
@@ -1310,6 +1430,11 @@ Template.boardCardSettingsPopup.events({
tpl.currentBoard.allowsDescriptionTitle,
);
},
'click .js-field-has-description-title-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsDescriptionTitleOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsDescriptionTitleOnMinicard: newValue } });
},
'click .js-field-has-card-number'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsCardNumber = !tpl.currentBoard
@@ -1326,6 +1451,11 @@ Template.boardCardSettingsPopup.events({
tpl.currentBoard.allowsCardNumber,
);
},
'click .js-field-has-card-number-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsCardNumberOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsCardNumberOnMinicard: newValue } });
},
'click .js-field-has-description-text-on-minicard'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsDescriptionTextOnMinicard = !tpl.currentBoard
@@ -1374,6 +1504,11 @@ Template.boardCardSettingsPopup.events({
tpl.currentBoard.allowsChecklists,
);
},
'click .js-field-has-checklists-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsChecklistsOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsChecklistsOnMinicard: newValue } });
},
'click .js-field-has-attachments'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsAttachments = !tpl.currentBoard
@@ -1390,6 +1525,11 @@ Template.boardCardSettingsPopup.events({
tpl.currentBoard.allowsAttachments,
);
},
'click .js-field-has-attachments-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsAttachmentsOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsAttachmentsOnMinicard: newValue } });
},
'click .js-field-has-comments'(evt, tpl) {
evt.preventDefault();
tpl.currentBoard.allowsComments = !tpl.currentBoard.allowsComments;
@@ -0,0 +1,4 @@
import { ReactiveVar } from 'meteor/reactive-var';
// Reactive source for invalidating people row when avatars change
export const avatarUpdateCounter = new ReactiveVar(0);
+37 -2
View File
@@ -70,11 +70,11 @@ template(name="changeAvatarPopup")
each uploadedAvatars
li: a.js-select-avatar
.member
img.avatar.avatar-image(src="{{link}}")
img.avatar.avatar-image(src="{{avatarLink}}")
if isSelected
i.fa.fa-check
p.sub-name
a.js-delete-avatar {{_ 'delete'}}
a.js-delete-avatar(data-avatar-id="{{_id}}") {{_ 'delete'}}
| -
= name
li: a.js-select-initials
@@ -100,6 +100,41 @@ template(name="deleteAvatarPopup")
p {{_ 'delete-avatar-confirm'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
template(name="adminChangeAvatarPopup")
.admin-change-avatar-header
h3 {{_ 'change-avatar'}} - {{ username }}
if error.get
.warning {{_ error.get}}
ul.pop-over-list
each uploadedAvatars
li: a.js-select-avatar
.member
img.avatar.avatar-image(src="{{avatarLink}}")
if isSelected
i.fa.fa-check
p.sub-name
a.js-delete-avatar(data-avatar-id="{{_id}}") {{_ 'delete'}}
| -
= name
li: a.js-select-initials
.member
+userAvatarInitials(userData=userData)
| {{_ 'initials' }}
if noAvatarUrl
i.fa.fa-check
p.sub-name {{_ 'default-avatar'}}
input.hide.js-upload-avatar-input(accept="image/*;capture=camera" type="file")
if Meteor.settings.public.avatarsUploadMaxSize
| {{_ 'max-avatar-filesize'}} {{Meteor.settings.public.avatarsUploadMaxSize}}
br
if Meteor.settings.public.avatarsUploadMimeTypes
| {{_ 'allowed-avatar-filetypes'}} {{Meteor.settings.public.avatarsUploadMimeTypes}}
br
| {{_ 'invalid-file'}}
button.full.js-upload-avatar
i.fa.fa-upload
| {{_ 'upload-avatar' }}
template(name="cardMemberPopup")
.board-member-menu
.mini-profile-info
+187 -8
View File
@@ -1,4 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ReactiveVar } from 'meteor/reactive-var';
import { avatarUpdateCounter } from '/client/components/users/avatarUpdateCounter';
import Avatars from '/models/avatars';
import Presences from '/models/presences';
import { Utils } from '/client/lib/utils';
@@ -164,6 +166,7 @@ Template.boardTeamName.helpers({
Template.changeAvatarPopup.onCreated(function () {
this.error = new ReactiveVar('');
this.avatarUpdateCounter = new ReactiveVar(0); // Trigger to force helper re-evaluation
Meteor.subscribe('my-avatars');
});
@@ -172,16 +175,28 @@ Template.changeAvatarPopup.helpers({
return Template.instance().error;
},
uploadedAvatars() {
Template.instance().avatarUpdateCounter.get(); // Create dependency on update counter
const ret = ReactiveCache.getAvatars({ userId: Meteor.userId() }, {}, true);
return ret;
},
avatarLink() {
if (this && typeof this.link === 'function') {
return this.link();
}
return '';
},
isSelected() {
Template.instance().avatarUpdateCounter.get(); // Create dependency on update counter
const userProfile = ReactiveCache.getCurrentUser().profile;
const avatarUrl = userProfile && userProfile.avatarUrl;
const currentAvatarUrl = Template.currentData().link();
return avatarUrl === currentAvatarUrl;
const currentAvatarUrl = this.link && typeof this.link === 'function' ? this.link() : '';
// Normalize URLs by removing query parameters for comparison
// (they may be added for boardId but shouldn't affect selection comparison)
const normalizeUrl = (url) => url ? url.split('?')[0] : '';
return normalizeUrl(avatarUrl) === normalizeUrl(currentAvatarUrl);
},
noAvatarUrl() {
Template.instance().avatarUpdateCounter.get(); // Create dependency on update counter
const userProfile = ReactiveCache.getCurrentUser().profile;
const avatarUrl = userProfile && userProfile.avatarUrl;
return !avatarUrl;
@@ -192,6 +207,14 @@ function changeAvatarSetAvatar(tpl, avatarUrl) {
Meteor.call('setAvatarUrl', avatarUrl, (err) => {
if (err) {
tpl.error.set(err.reason || 'Error setting avatar');
} else {
// Trigger a re-evaluation of helpers to show updated avatar selection
const counter = tpl.avatarUpdateCounter.get();
tpl.avatarUpdateCounter.set(counter + 1);
// Also increment global counter for admin people list updates
avatarUpdateCounter.set(avatarUpdateCounter.get() + 1);
// Clear input for next upload
tpl.$('.js-upload-avatar-input').val('');
}
});
}
@@ -212,15 +235,25 @@ Template.changeAvatarPopup.events({
uploader.on('error', (error, fileData) => {
tpl.error.set(error.reason);
});
uploader.on('uploaded', (error, fileRef) => {
if (!error) {
// Trigger a re-evaluation of helpers to show new uploaded avatar
const counter = tpl.avatarUpdateCounter.get();
tpl.avatarUpdateCounter.set(counter + 1);
// Also increment global counter for admin people list updates
avatarUpdateCounter.set(avatarUpdateCounter.get() + 1);
} else {
tpl.error.set(error.reason);
}
});
uploader.start();
}
},
'click .js-select-avatar'(event, tpl) {
event.preventDefault();
event.stopPropagation();
const data = Blaze.getData(event.currentTarget);
if (data && typeof data.link === 'function') {
const avatarUrl = data.link();
if (this && typeof this.link === 'function') {
const avatarUrl = this.link();
changeAvatarSetAvatar(tpl, avatarUrl);
}
},
@@ -229,10 +262,13 @@ Template.changeAvatarPopup.events({
event.stopPropagation();
changeAvatarSetAvatar(tpl, '');
},
'click .js-delete-avatar': Popup.afterConfirm('deleteAvatar', async function (event) {
await Avatars.removeAsync(this._id);
'click .js-delete-avatar': Popup.afterConfirm('deleteAvatar', async function() {
// Inside the each loop, 'this' is the avatar object
const avatarId = this._id;
if (avatarId) {
await Avatars.removeAsync(avatarId);
}
Popup.back();
event.stopPropagation();
}),
});
@@ -249,3 +285,146 @@ Template.cardMemberPopup.events({
},
'click .js-edit-profile': Popup.open('editProfile'),
});
Template.adminChangeAvatarPopup.onCreated(function () {
this.error = new ReactiveVar('');
this.avatarUpdateCounter = new ReactiveVar(0);
const userId = this.data._id || (this.data.user && this.data.user._id);
this.targetUserId = userId;
if (userId) {
Meteor.subscribe('avatars-for-user', userId);
}
});
Template.adminChangeAvatarPopup.helpers({
error() {
return Template.instance().error;
},
userId() {
const instance = Template.instance();
return instance.targetUserId || (this._id || (this.user && this.user._id));
},
userData() {
return this.user || this;
},
uploadedAvatars() {
Template.instance().avatarUpdateCounter.get();
const instance = Template.instance();
const userId = instance.targetUserId || (this._id || (this.user && this.user._id));
if (!userId) return [];
const ret = ReactiveCache.getAvatars({ userId: userId }, {}, true);
return ret;
},
avatarLink() {
if (this && typeof this.link === 'function') {
return this.link();
}
return '';
},
currentEditingUser() {
Template.instance().avatarUpdateCounter.get();
const instance = Template.instance();
const userId = instance.targetUserId;
if (!userId) return null;
return ReactiveCache.getUser(userId);
},
isSelected() {
Template.instance().avatarUpdateCounter.get();
const instance = Template.instance();
const userId = instance.targetUserId;
if (!userId) return false;
const user = ReactiveCache.getUser(userId);
if (!user) return false;
const userProfile = user.profile;
const avatarUrl = userProfile && userProfile.avatarUrl;
const currentAvatarUrl = this.link && typeof this.link === 'function' ? this.link() : '';
const normalizeUrl = (url) => url ? url.split('?')[0] : '';
return normalizeUrl(avatarUrl) === normalizeUrl(currentAvatarUrl);
},
noAvatarUrl() {
Template.instance().avatarUpdateCounter.get();
const instance = Template.instance();
const userId = instance.targetUserId;
if (!userId) return true;
const user = ReactiveCache.getUser(userId);
if (!user) return true;
const userProfile = user.profile;
const avatarUrl = userProfile && userProfile.avatarUrl;
return !avatarUrl;
},
});
function adminChangeAvatarSetAvatar(tpl, avatarUrl) {
const userId = tpl.targetUserId || (Template.currentData()._id || (Template.currentData().user && Template.currentData().user._id));
if (!userId) {
console.error('Cannot set avatar: no userId found');
return;
}
Meteor.call('adminSetAvatarUrl', userId, avatarUrl, (err) => {
if (err) {
tpl.error.set(err.reason || 'Error setting avatar');
} else {
const counter = tpl.avatarUpdateCounter.get();
tpl.avatarUpdateCounter.set(counter + 1);
// Also increment global counter to update admin people list
avatarUpdateCounter.set(avatarUpdateCounter.get() + 1);
tpl.$('.js-upload-avatar-input').val('');
}
});
}
Template.adminChangeAvatarPopup.events({
'click .js-upload-avatar'(event, tpl) {
tpl.$('.js-upload-avatar-input').click();
},
async 'change .js-upload-avatar-input'(event, tpl) {
if (event.currentTarget.files && event.currentTarget.files[0]) {
const userId = tpl.targetUserId || (this._id || (this.user && this.user._id));
const uploader = await Avatars.insertAsync(
{
file: event.currentTarget.files[0],
chunkSize: 'dynamic',
meta: {
adminUploadForUserId: userId,
},
},
false,
);
uploader.on('error', (error, fileData) => {
tpl.error.set(error.reason);
});
uploader.on('uploaded', (error, fileRef) => {
if (!error) {
const counter = tpl.avatarUpdateCounter.get();
tpl.avatarUpdateCounter.set(counter + 1);
// Also increment global counter to update admin people list
avatarUpdateCounter.set(avatarUpdateCounter.get() + 1);
} else {
tpl.error.set(error.reason);
}
});
uploader.start();
}
},
'click .js-select-avatar'(event, tpl) {
event.preventDefault();
event.stopPropagation();
if (this && typeof this.link === 'function') {
const avatarUrl = this.link();
adminChangeAvatarSetAvatar(tpl, avatarUrl);
}
},
'click .js-select-initials'(event, tpl) {
event.preventDefault();
event.stopPropagation();
adminChangeAvatarSetAvatar(tpl, '');
},
'click .js-delete-avatar': Popup.afterConfirm('deleteAvatar', async function() {
// Inside the each loop, 'this' is the avatar object
const avatarId = this._id;
if (avatarId) {
await Avatars.removeAsync(avatarId);
}
Popup.back();
}),
});
+373 -69
View File
@@ -18,8 +18,9 @@ window.ExportHtml = Popup => {
}
};
const getPageHtmlString = () => {
return `<!doctype html>${window.document.querySelector('html').outerHTML}`;
const getPageHtmlString = (clonedElement = null) => {
const element = clonedElement || window.document.querySelector('html');
return `<!doctype html>${element.outerHTML}`;
};
const removeAnchors = htmlString => {
@@ -30,74 +31,161 @@ window.ExportHtml = Popup => {
return replaceOpenAnchor.replace(new RegExp('</a', 'gim'), '</span');
};
const ensureSidebarRemoved = () => {
document.querySelector('.board-sidebar.sidebar').remove();
const ensureSidebarRemoved = (element = null) => {
const target = element || document;
const sidebar = target.querySelector('.board-sidebar.sidebar');
if (sidebar) {
sidebar.remove();
}
};
const addJsonExportToZip = async (zip, boardSlug) => {
const downloadJSONLink = document.querySelector('.download-json-link');
const downloadJSONURL = downloadJSONLink.href;
const response = await fetch(downloadJSONURL);
const responseBody = await response.text();
zip.file(`data/${boardSlug}.json`, responseBody);
};
const closeSidebar = () => {
document.querySelector('.board-header-btn.js-toggle-sidebar').click();
};
const cleanBoardHtml = () => {
Array.from(document.querySelectorAll('script')).forEach(elem =>
const cleanBoardHtmlClone = (clonedElement) => {
// Work on cloned element only, not live DOM
Array.from(clonedElement.querySelectorAll('script')).forEach(elem =>
elem.remove(),
);
Array.from(
document.querySelectorAll('link:not([rel="stylesheet"])'),
clonedElement.querySelectorAll('link:not([rel="stylesheet"])'),
).forEach(elem => elem.remove());
document.querySelector('#header-quick-access').remove();
const headerQuickAccess = clonedElement.querySelector('#header-quick-access');
if (headerQuickAccess) headerQuickAccess.remove();
Array.from(
document.querySelectorAll('#header-main-bar .board-header-btns'),
clonedElement.querySelectorAll('#header-main-bar .board-header-btns'),
).forEach(elem => elem.remove());
Array.from(
document.querySelectorAll('.js-pop-over, .pop-over'),
clonedElement.querySelectorAll('.js-pop-over, .pop-over'),
).forEach(elem => elem.remove());
Array.from(document.querySelectorAll('.list-composer')).forEach(elem =>
Array.from(clonedElement.querySelectorAll('.list-composer')).forEach(elem =>
elem.remove(),
);
Array.from(
document.querySelectorAll(
clonedElement.querySelectorAll(
'.list-composer,.js-card-composer, .js-add-card',
),
).forEach(elem => elem.remove());
Array.from(document.querySelectorAll('[href]:not(link)')).forEach(elem =>
// Remove edit/action buttons from cards but keep card content
Array.from(clonedElement.querySelectorAll('.js-edit, .js-delete, .js-open-card')).forEach(elem =>
elem.remove(),
);
// Keep card details structure for viewing - don't remove or hide
// The interactive script will handle showing/hiding them
Array.from(clonedElement.querySelectorAll('[href]:not(link)')).forEach(elem =>
elem.attributes.removeNamedItem('href'),
);
Array.from(document.querySelectorAll('[href]')).forEach(elem => {
Array.from(clonedElement.querySelectorAll('[href]')).forEach(elem => {
// eslint-disable-next-line no-self-assign
elem.href = elem.href;
// eslint-disable-next-line no-self-assign
elem.src = elem.src;
});
Array.from(document.querySelectorAll('.is-editable')).forEach(elem => {
Array.from(clonedElement.querySelectorAll('.is-editable')).forEach(elem => {
elem.classList.remove('is-editable');
});
};
const addJsonExportToZip = async (zip, boardSlug, zipDirName) => {
const downloadJSONLink = document.querySelector('.download-json-link');
const downloadJSONURL = downloadJSONLink.href;
const response = await fetch(downloadJSONURL);
const responseBody = await response.text();
zip.file(`${zipDirName}/data/${boardSlug}.json`, responseBody);
};
const getBoardSlug = () => {
// Use board ID from session instead of URL slug to keep filenames short
const boardId = Session.get('currentBoard');
if (boardId) {
return boardId;
}
// Fallback to URL slug if session is not available
return window.location.href.split('/').pop();
};
const getBoardTitle = () => {
// Extract from page header - most reliable method
const headerTitle = document.querySelector('h1.header-board-menu');
if (headerTitle && headerTitle.textContent) {
const title = headerTitle.textContent.trim();
// Filter out translation keys like "{{_ 'templates'}}"
if (title && !title.includes('{{')) {
return title;
}
}
// Fallback: use board slug if header title extraction fails
return getBoardSlug();
};
const sanitizeFilename = (filename) => {
// Remove or replace invalid filename characters
// Keep alphanumeric, hyphens, underscores, and spaces
return filename
.replace(/[^a-zA-Z0-9\s\-_]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Replace multiple hyphens with single
.substring(0, 100) // Limit to 100 chars
.trim();
};
const getStylesheetList = () => {
return Array.from(
document.querySelectorAll('link[href][rel="stylesheet"]'),
);
};
const downloadStylesheets = async (stylesheets, zip) => {
const getSrcAttached = () => {
return Array.from(document.querySelectorAll('[src]'));
};
const downloadStylesheets = async (stylesheets, zip, clonedHtmlElement, zipDirName) => {
await asyncForEach(stylesheets, async elem => {
const response = await fetch(elem.href);
const responseBody = await response.text();
let responseBody = await response.text();
const finalResponse = responseBody.replace(
// Check if this is a Font Awesome CSS file
const isFontAwesomeCSS = elem.href && (
elem.href.includes('fontawesome') ||
elem.href.includes('font-awesome') ||
elem.href.includes('fa-') ||
elem.href.includes('wekan-fontawesome')
);
if (isFontAwesomeCSS) {
// For Font Awesome: preserve @font-face but update paths to local files
responseBody = responseBody.replace(
/url\(["']?(?:\.\.\/)?webfonts\/([^"')]+)["']?\)/gi,
'url("../webfonts/$1")'
);
// Also handle upstream package paths for Font Awesome
responseBody = responseBody.replace(
/url\(["']?[^"']*packages\/[^/]+\/upstream\/webfonts\/([^"')]+)["']?\)/gi,
'url("../webfonts/$1")'
);
} else {
// For non-Font-Awesome CSS: remove @font-face rules to eliminate external font dependencies
responseBody = responseBody.replace(
/@font-face\s*\{[^}]*\}/gi,
''
);
// Replace non-Font-Awesome font families with system fonts (sans-serif)
// But preserve Font Awesome families
responseBody = responseBody.replace(
/font-family\s*:\s*[^;]*(?:Roboto|Lato|Open Sans)[^;]*;/gi,
'font-family: sans-serif;'
);
}
// Fix relative paths for assets
let finalResponse = responseBody;
// Replace upstream package paths
finalResponse = finalResponse.replace(
new RegExp('packages/[^/]+/upstream/', 'gim'),
'../',
);
@@ -107,17 +195,20 @@ window.ExportHtml = Popup => {
.pop()
.split('?')
.shift();
const fileFullPath = `style/${filename}`;
const fileFullPath = `${zipDirName}/style/${filename}`;
zip.file(fileFullPath, finalResponse);
elem.href = `../${fileFullPath}`;
// Update the cloned HTML element, not the live one
if (clonedHtmlElement) {
const clonedElements = clonedHtmlElement.querySelectorAll(`link[href="${elem.href}"]`);
clonedElements.forEach(clonedElem => {
clonedElem.href = `./style/${filename}`;
});
}
});
};
const getSrcAttached = () => {
return Array.from(document.querySelectorAll('[src]'));
};
const downloadSrcAttached = async (elements, zip, boardSlug) => {
const downloadSrcAttached = async (elements, zip, boardSlug, clonedHtmlElement, zipDirName) => {
await asyncForEach(elements, async elem => {
const response = await fetch(elem.src);
const responseBody = await response.blob();
@@ -126,9 +217,16 @@ window.ExportHtml = Popup => {
.pop()
.split('?')
.shift();
const fileFullPath = `${boardSlug}/${elem.tagName.toLowerCase()}/${filename}`;
const fileFullPath = `${zipDirName}/${elem.tagName.toLowerCase()}/${filename}`;
zip.file(fileFullPath, responseBody);
elem.src = `./${elem.tagName.toLowerCase()}/${filename}`;
// Update the cloned HTML element, not the live one
if (clonedHtmlElement) {
const clonedElements = clonedHtmlElement.querySelectorAll(`[src="${elem.src}"]`);
clonedElements.forEach(clonedElem => {
clonedElem.src = `./${elem.tagName.toLowerCase()}/${filename}`;
});
}
});
};
@@ -156,7 +254,7 @@ window.ExportHtml = Popup => {
};
const getWebFonts = () => {
let fontUrls = [];
let fonts = [];
for (let sheet of document.styleSheets) {
// Get the base URL of the stylesheet
@@ -169,10 +267,14 @@ window.ExportHtml = Popup => {
let urlMatch = src.match(/url\(["']?(.+?)["']?\)/);
if (urlMatch) {
let fontUrl = urlMatch[1];
let fontFamily = rule.style.getPropertyValue('font-family').replace(/["']/g, '');
// Resolve the URL relative to the stylesheet's base URL
let resolvedUrl = new URL(fontUrl, baseUrl);
fontUrls.push(resolvedUrl.href); // Using .href to get the absolute URL
fonts.push({
url: resolvedUrl.href,
family: fontFamily
}); // Using .href to get the absolute URL
}
}
}
@@ -181,25 +283,114 @@ window.ExportHtml = Popup => {
}
}
return fontUrls;
return fonts;
};
const downloadFonts = async(elements, zip) => {
await asyncForEach(elements, async elem => {
const downloadFonts = async(elements, zip, zipDirName) => {
let fontIndex = 1;
await asyncForEach(elements, async (fontObj, idx) => {
const elem = fontObj.url;
const fontFamily = fontObj.family || `font${fontIndex}`;
const response = await fetch(elem);
const responseBody = await response.blob();
const filename = elem.split('/')
.pop()
.split('?')
.shift()
.split('#')
.shift();
const fileFullPath = `webfonts/${filename}`;
zip.file(fileFullPath, responseBody);
// Try to extract a better filename
let filename = fontFamily.replace(/[^a-z0-9-]/gi, '').substring(0, 50) || `font${fontIndex++}`;
// If filename is empty after sanitization, use index
if (!filename || filename.length === 0) {
filename = `font${fontIndex++}`;
}
// First, try to extract filename from URL path (for normal URLs)
if (!elem.startsWith('data:')) {
const pathname = new URL(elem).pathname;
const urlFilename = pathname.split('/').pop().split('?')[0].split('#')[0];
if (urlFilename && urlFilename.length > 0 && !urlFilename.match(/^[a-f0-9]+$/i)) {
// Only use URL filename if it's not a hash
filename = urlFilename;
}
}
// Detect format from content-type or file extension
let extension = 'woff2';
const contentType = response.headers.get('content-type');
if (contentType) {
if (contentType.includes('font/woff')) extension = 'woff';
else if (contentType.includes('font/ttf')) extension = 'ttf';
else if (contentType.includes('application/x-font-ttf')) extension = 'ttf';
else if (contentType.includes('application/x-font-opentype')) extension = 'otf';
}
// Ensure filename has extension
if (!filename.includes('.')) {
filename = `${filename}.${extension}`;
}
const fileFullPath = `${zipDirName}/webfonts/${filename}`;
zip.file(fileFullPath, responseBody);
});
}
const downloadCardCovers = async (elements, zip, boardSlug) => {
const downloadFontAwesomeFonts = async (zip, zipDirName) => {
// List of Font Awesome webfont files to include
const fontAwesomeFiles = [
'fa-solid-900.woff2',
'fa-solid-900.ttf',
'fa-brands-400.woff2',
'fa-brands-400.ttf',
'fa-regular-400.woff2',
'fa-regular-400.ttf',
'fa-v4compatibility.woff2',
'fa-v4compatibility.ttf',
];
try {
await asyncForEach(fontAwesomeFiles, async (filename) => {
try {
// Try to fetch from the Meteor bundle webfont location
const fontUrls = [
`/packages/wekan-fontawesome/webfonts/${filename}`,
`/webfonts/${filename}`,
`./webfonts/${filename}`,
`../packages/wekan-fontawesome/webfonts/${filename}`,
];
let response = null;
let fetchedUrl = null;
for (const url of fontUrls) {
try {
response = await fetch(url);
if (response.ok) {
fetchedUrl = url;
break;
}
} catch (e) {
// Try next URL
continue;
}
}
if (response && response.ok) {
const responseBody = await response.blob();
const fileFullPath = `${zipDirName}/webfonts/${filename}`;
zip.file(fileFullPath, responseBody);
console.log(`✓ Font Awesome font included: ${filename} from ${fetchedUrl}`);
} else {
console.log(`✗ Font Awesome font not found: ${filename}`);
}
} catch (e) {
console.log(`Error downloading Font Awesome font ${filename}:`, e);
}
});
} catch (e) {
console.log('Error in downloadFontAwesomeFonts:', e);
}
}
const downloadCardCovers = async (elements, zip, boardSlug, clonedHtmlElement, zipDirName) => {
await asyncForEach(elements, async elem => {
const response = await fetch(
removeCssUrlSurround(elem.style['background-image']),
@@ -212,18 +403,117 @@ window.ExportHtml = Popup => {
.shift()
.split('#')
.shift();
const fileFullPath = `${boardSlug}/covers/${filename}`;
const fileFullPath = `${zipDirName}/covers/${filename}`;
zip.file(fileFullPath, responseBody);
elem.style = "background-image: url('" + `covers/${filename}` + "')";
// Update the cloned HTML element, not the live one
if (clonedHtmlElement) {
const bgImage = removeCssUrlSurround(elem.style['background-image']);
const clonedElements = clonedHtmlElement.querySelectorAll('.minicard-cover');
clonedElements.forEach(clonedElem => {
if (removeCssUrlSurround(clonedElem.style['background-image']) === bgImage) {
clonedElem.style.backgroundImage = `url('./covers/${filename}')`;
}
});
}
});
};
const addBoardHTMLToZip = (boardSlug, zip) => {
ensureSidebarRemoved();
const htmlOutputPath = `${boardSlug}/index.html`;
const addBoardHTMLToZip = (boardSlug, zip, clonedElement, zipDirName) => {
ensureSidebarRemoved(clonedElement);
// Get the HTML string
let htmlString = removeAnchors(getPageHtmlString(clonedElement));
// Inject font styling with Font Awesome exception
// Apply sans-serif to most elements, but preserve Font Awesome icon fonts
const fontStyleTag = `<style>
* { font-family: sans-serif !important; }
body { font-family: sans-serif !important; }
.fa, .fas, .far, .fab, .fa-solid, .fa-regular, .fa-brands, [class*="fa-"] {
font-family: 'Font Awesome 6 Free', 'Font Awesome 6 Brands' !important;
}
</style>`;
htmlString = htmlString.replace(
/<\/head>/i,
`${fontStyleTag}</head>`
);
// Inject interactive script into the HTML before closing body tag
const scriptContent = `
// Make cards interactive in exported HTML
document.addEventListener('DOMContentLoaded', function() {
console.log('Initializing card interactions...');
// Create modal/popup for card details
let modal = document.getElementById('card-details-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'card-details-modal';
modal.style.cssText = 'display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:white;border:1px solid #ccc;border-radius:8px;padding:20px;max-width:600px;width:90%;max-height:80vh;overflow-y:auto;z-index:10000;box-shadow:0 4px 20px rgba(0,0,0,0.3);';
document.body.appendChild(modal);
}
// Add click handlers to cards to show/hide details
const cards = document.querySelectorAll('.minicard');
console.log('Found ' + cards.length + ' cards');
cards.forEach((card, idx) => {
card.style.cursor = 'pointer';
card.dataset.cardIndex = idx;
card.addEventListener('click', function(e) {
// Ignore clicks on buttons or action elements
if (e.target.closest('button, a, .js-') || e.target.closest('[role="button"]')) {
return;
}
e.preventDefault();
e.stopPropagation();
// Extract card title
const title = this.querySelector('.card-title, .minicard-title, [class*="title"]');
const titleText = title ? title.textContent.trim() : 'Card ' + this.dataset.cardIndex;
// Get all text content from the card
const allText = this.textContent.trim();
// Show modal with card details
modal.innerHTML = '<button style="position:absolute;top:10px;right:10px;padding:5px 10px;background:#f0f0f0;border:1px solid #ccc;border-radius:4px;cursor:pointer;">Close</button>';
modal.querySelector('button').onclick = () => { modal.style.display = 'none'; document.body.style.overflow = 'auto'; };
const content = document.createElement('div');
content.innerHTML = '<h2 style="margin-bottom:10px;">' + titleText + '</h2><hr style="margin:10px 0;"><div style="white-space:pre-wrap;word-wrap:break-word;font-family:sans-serif;font-size:13px;max-height:400px;overflow-y:auto;line-height:1.4;">' + allText + '</div><p style="color:#666;font-size:12px;margin-top:20px;"><em>Card #' + this.dataset.cardIndex + ' from exported board</em></p>';
modal.appendChild(content);
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
console.log('Showing card details for card ' + this.dataset.cardIndex);
});
});
// Add style for cards
if (!document.getElementById('card-open-styles')) {
const style = document.createElement('style');
style.id = 'card-open-styles';
style.textContent = '* { font-family: sans-serif !important; } body { font-family: sans-serif !important; } .minicard { transition: all 0.3s ease; cursor: pointer; } .minicard:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important; transform: translateY(-2px); }';
document.head.appendChild(style);
console.log('Added card styles and sans-serif font');
}
});
`;
// Inject script before closing body tag
htmlString = htmlString.replace(
/<\/body>/i,
`<script>${scriptContent}</script></body>`
);
const htmlOutputPath = `${zipDirName}/index.html`;
zip.file(
htmlOutputPath,
new Blob([removeAnchors(getPageHtmlString())], {
new Blob([htmlString], {
type: 'application/html',
}),
);
@@ -232,21 +522,35 @@ window.ExportHtml = Popup => {
return async () => {
const zip = new JSZip();
const boardSlug = getBoardSlug();
const boardTitle = sanitizeFilename(getBoardTitle());
const zipFilename = boardTitle || boardSlug;
const zipDirName = zipFilename; // Directory name inside the ZIP
await addJsonExportToZip(zip, boardSlug);
// Clone the HTML element to process for export without modifying live DOM
const htmlElement = window.document.querySelector('html');
const clonedHtmlElement = htmlElement.cloneNode(true);
await addJsonExportToZip(zip, boardSlug, zipDirName);
Popup.back();
closeSidebar();
cleanBoardHtml();
await downloadStylesheets(getStylesheetList(), zip);
await downloadSrcAttached(getSrcAttached(), zip, boardSlug);
await downloadCardCovers(getCardCovers(), zip, boardSlug);
await downloadFonts(getWebFonts(), zip);
// Process the cloned element only
cleanBoardHtmlClone(clonedHtmlElement);
addBoardHTMLToZip(boardSlug, zip);
// Pass cloned element and zipDirName to all download functions so they update the clone, not the live DOM
await downloadStylesheets(getStylesheetList(), zip, clonedHtmlElement, zipDirName);
await downloadSrcAttached(getSrcAttached(), zip, boardSlug, clonedHtmlElement, zipDirName);
await downloadCardCovers(getCardCovers(), zip, boardSlug, clonedHtmlElement, zipDirName);
// Download Font Awesome webfont files for icon support in exported HTML
await downloadFontAwesomeFonts(zip, zipDirName);
// Note: General fonts are not exported to keep export lightweight
// downloadFonts(getWebFonts(), zip, zipDirName); -- skipped
addBoardHTMLToZip(boardSlug, zip, clonedHtmlElement, zipDirName);
const content = await zip.generateAsync({ type: 'blob' });
saveAs(content, `${boardSlug}.zip`);
window.location.reload();
saveAs(content, `${zipFilename}.zip`);
// No page reload - impersonation session is preserved!
};
};
+14 -2
View File
@@ -171,7 +171,7 @@ services:
# Leave it unset or empty to skip migration.
#-------------------------------------------------------------------------------------
#wekan-migrate:
# image: node:22.22.2-slim
# image: node:24.14.1-slim
# container_name: wekan-migrate
# restart: "no"
# networks:
@@ -226,7 +226,15 @@ services:
image: mongo:7
container_name: wekan-db
restart: always
command: mongod --logpath /dev/null --oplogSize 128 --quiet
#command: mongod --logpath /dev/null --oplogSize 128 --replSet rs0 --quiet
#command: mongod --oplogSize 128 --replSet rs0 --quiet
command: >
sh -c "
mongod --oplogSize 128 --replSet rs0 --quiet --bind_ip_all &
until mongosh --eval 'rs.initiate()' > /dev/null 2>&1; do
sleep 2
done;
wait"
networks:
- wekan-tier
expose:
@@ -281,6 +289,10 @@ services:
# remove port mapping if you use nginx reverse proxy, port 8080 is already exposed to wekan-tier network
- 80:8080
environment:
#-------------------- CHANGE STREAMS --------------------
# https://forums.meteor.com/t/meteor-3-5-beta-change-streams-performance-improvements/64461#change-streams-setup-3
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
#-----------------------------------------------------------------
# ==== WRITEABLE PATH FOR FILE UPLOADS ====
- WRITABLE_PATH=/data
+3 -4
View File
@@ -10,9 +10,8 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-8.58-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.58/wekan-8.58-amd64-windows.zip)
2. [node.exe](https://nodejs.org/dist/latest-v22.x/win-x64/node.exe)
1. [wekan-8.78-win64.zip](https://github.com/wekan/wekan/releases/download/v8.78/wekan-8.78-win64.zip)
2. [node.exe](https://nodejs.org/dist/latest-v24.x/win-x64/node.exe)
3. [mongodb-windows-x86_64-7.0.31-signed.msi](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.31-signed.msi)
@@ -22,7 +21,7 @@ Right click and download files 1-4:
6. Double click `mongodb-windows-x86_64-7.0.31-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-8.58-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-8.78-win64.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)
+5 -4
View File
@@ -345,7 +345,7 @@
"unset-color": "Unset",
"comments": "Comments",
"comment": "Comment",
"comment-placeholder": "Write Comment",
"comment-placeholder": "",
"comment-only": "Comment only",
"comment-only-desc": "Can comment on cards only.",
"comment-assigned-only": "Only Assigned Comment",
@@ -808,6 +808,9 @@
"modifiedAt": "Modified at",
"verified": "Verified",
"active": "Active",
"active-team": "Active Team",
"active-org": "Active Organization",
"active-person": "Active Person",
"card-received": "Received",
"card-received-on": "Received on",
"card-end": "End",
@@ -1572,7 +1575,6 @@
"run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?",
"run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?",
"restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore",
"migration-progress-title": "Board Migration in Progress",
"migration-progress-overall": "Overall Progress",
"migration-progress-current-step": "Current Step",
@@ -1582,7 +1584,6 @@
"steps": "steps",
"view": "View",
"has-swimlanes": "Has Swimlanes",
"step-analyze-board-structure": "Analyze Board Structure",
"step-fix-orphaned-cards": "Fix Orphaned Cards",
"step-convert-shared-lists": "Convert Shared Lists",
@@ -1729,4 +1730,4 @@
"logout": "Logout",
"server": "Server",
"protocol": "Protocol"
}
}
+54 -5
View File
@@ -80,7 +80,20 @@ Avatars.onAfterUpload = async function (fileObj) {
if (isValid) {
// Set avatar URL using universal URL generator (URL-agnostic)
const universalUrl = generateUniversalAvatarUrl(fileObj._id);
const user = await ReactiveCache.getUser(fileObj.userId);
// Check if this is an admin uploading for another user
let targetUserId = fileObj.userId;
if (fileObj.meta && fileObj.meta.adminUploadForUserId) {
// Verify the uploader is an admin
const uploader = await Meteor.users.findOneAsync(fileObj.userId);
if (uploader && uploader.isAdmin) {
targetUserId = fileObj.meta.adminUploadForUserId;
// Update the file to belong to the target user
await Avatars.updateAsync({ _id: fileObj._id }, { $set: { userId: targetUserId } });
}
}
const user = await ReactiveCache.getUser(targetUserId);
user.setAvatarUrl(universalUrl);
} else {
await Avatars.removeAsync(fileObj._id);
@@ -93,20 +106,56 @@ Avatars.interceptDownload = function (http, fileObj, versionName) {
};
Avatars.onBeforeRemove = async function (filesInput) {
const files = normalizeRemovedFiles(filesInput);
let files;
try {
files = normalizeRemovedFiles(filesInput);
// If normalizeRemovedFiles returns a Promise (from async fetch), await it
if (files && typeof files.then === 'function') {
console.warn('normalizeRemovedFiles returned a Promise, awaiting it');
files = await files;
}
} catch (e) {
console.error('Error normalizing removed files:', e, 'filesInput:', filesInput);
files = [];
}
// Ensure files is an array
if (!Array.isArray(files)) {
console.error('normalizeRemovedFiles did not return an array, got:', typeof files);
files = [];
}
for (const fileObj of files) {
if (fileObj && fileObj.userId) {
const user = await ReactiveCache.getUser(fileObj.userId);
user.setAvatarUrl('');
if (user) {
await user.setAvatarUrl('');
}
}
}
return true;
};
Avatars.onAfterRemove = function (filesInput) {
const files = normalizeRemovedFiles(filesInput);
Avatars.onAfterRemove = async function (filesInput) {
let files;
try {
files = normalizeRemovedFiles(filesInput);
// If normalizeRemovedFiles returns a Promise (from async fetch), await it
if (files && typeof files.then === 'function') {
console.warn('normalizeRemovedFiles returned a Promise in onAfterRemove, awaiting it');
files = await files;
}
} catch (e) {
console.error('Error normalizing removed files in onAfterRemove:', e, 'filesInput:', filesInput);
files = [];
}
// Ensure files is an array
if (!Array.isArray(files)) {
console.error('normalizeRemovedFiles did not return an array in onAfterRemove, got:', typeof files);
files = [];
}
files.forEach(fileObj => {
if (!fileObj || !fileObj.versions) {
+98
View File
@@ -450,6 +450,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsSubtasksOnMinicard: {
/**
* Does the board allows subtasks on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsAttachments: {
/**
@@ -458,6 +465,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsAttachmentsOnMinicard: {
/**
* Does the board allows attachments on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsChecklists: {
/**
@@ -466,6 +480,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsChecklistsOnMinicard: {
/**
* Does the board allows checklists on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsComments: {
/**
@@ -482,6 +503,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsDescriptionTitleOnMinicard: {
/**
* Does the board allows description title on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsDescriptionText: {
/**
@@ -529,6 +557,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: false,
},
allowsCardNumberOnMinicard: {
/**
* Does the board allows card numbers on minicard?
*/
type: Boolean,
defaultValue: false,
},
allowsActivities: {
/**
@@ -545,6 +580,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsLabelsOnMinicard: {
/**
* Does the board allows labels on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsCreator: {
/**
@@ -569,6 +611,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsAssigneeOnMinicard: {
/**
* Does the board allows assignee on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsMembers: {
/**
@@ -577,6 +626,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsMembersOnMinicard: {
/**
* Does the board allows members on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsRequestedBy: {
/**
@@ -585,6 +641,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsRequestedByOnMinicard: {
/**
* Does the board allows requested by on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsCardSortingByNumber: {
/**
@@ -610,6 +673,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsAssignedByOnMinicard: {
/**
* Does the board allows requested by on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsShowListsOnMinicard: {
/**
* Does the board allow showing list names on all minicards?
@@ -633,6 +703,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsReceivedDateOnMinicard: {
/**
* Does the board allows received date on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsStartDate: {
/**
@@ -641,6 +718,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsStartDateOnMinicard: {
/**
* Does the board allows start date on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsEndDate: {
/**
@@ -649,6 +733,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsEndDateOnMinicard: {
/**
* Does the board allows end date on minicard?
*/
type: Boolean,
defaultValue: true,
},
allowsDueDate: {
/**
@@ -657,6 +748,13 @@ Boards.attachSchema(
type: Boolean,
defaultValue: true,
},
allowsDueDateOnMinicard: {
/**
* Does the board allows due date on minicard?
*/
type: Boolean,
defaultValue: true,
},
presentParentTask: {
/**
+6 -6
View File
@@ -223,7 +223,7 @@ if (Meteor.isServer) {
// Public boards don't require authentication - skip hash operations
const exporter = new Exporter(boardId);
if( params.query.delimiter == "\t" ) {
if( req.query.delimiter == "\t" ) {
// TSV file
res.writeHead(200, {
'Content-Type': 'text/tsv',
@@ -238,13 +238,13 @@ if (Meteor.isServer) {
// use Uint8Array to prevent from converting bytes to string
res.write(new Uint8Array([0xEF, 0xBB, 0xBF]));
}
res.write(await exporter.buildCsv(params.query.delimiter, 'en'));
res.write(await exporter.buildCsv(req.query.delimiter, 'en'));
res.end();
return;
}
// Only perform expensive authentication for private boards
const loginToken = params.query.authToken;
const loginToken = req.query.authToken;
if (loginToken) {
// Validate token length to prevent resource abuse
if (loginToken.length > 10000) {
@@ -274,7 +274,7 @@ if (Meteor.isServer) {
if (await exporter.canExport(user) || impersonateDone) {
if (impersonateDone) {
let exportType = 'exportCSV';
if( params.query.delimiter == "\t" ) {
if( req.query.delimiter == "\t" ) {
exportType = 'exportTSV';
}
await ImpersonatedUsers.insertAsync({
@@ -289,7 +289,7 @@ if (Meteor.isServer) {
userLanguage = user.profile.language
}
if( params.query.delimiter == "\t" ) {
if( req.query.delimiter == "\t" ) {
// TSV file
res.writeHead(200, {
'Content-Type': 'text/tsv',
@@ -304,7 +304,7 @@ if (Meteor.isServer) {
// use Uint8Array to prevent from converting bytes to string
res.write(new Uint8Array([0xEF, 0xBB, 0xBF]));
}
res.write(await exporter.buildCsv(params.query.delimiter, userLanguage));
res.write(await exporter.buildCsv(req.query.delimiter, userLanguage));
res.end();
} else {
res.writeHead(403);
+1 -1
View File
@@ -55,7 +55,7 @@ runOnServer(function() {
}
// Only perform expensive authentication for private boards
const loginToken = params.query.authToken;
const loginToken = req.query.authToken;
if (loginToken) {
// Validate token length to prevent resource abuse
if (loginToken.length > 10000) {
+553 -552
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wekan",
"version": "v8.58.0",
"version": "v8.78.0",
"description": "Open-Source kanban",
"private": true,
"repository": {
+3 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf8" />
<title>WeKan REST API v8.58</title>
<title>WeKan REST API v8.78</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@@ -390,7 +390,7 @@ data-styled.g138[id="sc-iJQrDi"]{content:"gtHWGb,"}/*!sc*/
55.627 l 55.6165,55.627 -231.245496,231.24803 c -127.185,127.1864
-231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688,
-104.0616 -231.873,-231.248 z
" fill="currentColor"></path></g></svg></div></div><div class="sc-juTflS gfWNtA api-content"><div class="sc-eDDNvO eTiIZG"><div class="sc-iAEyYj cmAaWK"><div class="sc-hLseeT hoYmkG api-info"><h1 class="sc-fsQipe sc-crPCXn ePkAIL bSStQp">Wekan REST API<!-- --> <span>(<!-- -->v8.58<!-- -->)</span></h1><p>Download OpenAPI specification<!-- -->:</p><div class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"></div><div data-role="redoc-summary" html="" class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"></div><div data-role="redoc-description" html="&lt;p&gt;The REST API allows you to control and extend Wekan with ease.&lt;/p&gt;
" fill="currentColor"></path></g></svg></div></div><div class="sc-juTflS gfWNtA api-content"><div class="sc-eDDNvO eTiIZG"><div class="sc-iAEyYj cmAaWK"><div class="sc-hLseeT hoYmkG api-info"><h1 class="sc-fsQipe sc-crPCXn ePkAIL bSStQp">Wekan REST API<!-- --> <span>(<!-- -->v8.78<!-- -->)</span></h1><p>Download OpenAPI specification<!-- -->:</p><div class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"></div><div data-role="redoc-summary" html="" class="sc-iKGpAq sc-cCYyou dXXcln dHaogz"></div><div data-role="redoc-description" html="&lt;p&gt;The REST API allows you to control and extend Wekan with ease.&lt;/p&gt;
&lt;p&gt;If you are an end-user and not a dev or a tester, &lt;a href=&quot;https://github.com/wekan/wekan/issues/new&quot;&gt;create an issue&lt;/a&gt; to request new APIs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;All API calls in the documentation are made using &lt;code&gt;curl&lt;/code&gt;. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.&lt;/p&gt;
@@ -446,7 +446,7 @@ data-styled.g138[id="sc-iJQrDi"]{content:"gtHWGb,"}/*!sc*/
" class="sc-iKGpAq sc-cCYyou sc-cjERFZ dXXcln fTBBlJ dkmSdy"><p>Error in registration</p>
</div></button></div></div></div><div class="sc-jTrPJt sc-gLDzao dVngAA fYLqku"><div class="sc-fYzRkH dHdMVa"><button class="sc-jYvNnh iWrBta"><span type="post" class="sc-eGFuAY ejslkd http-verb post">post</span><span class="sc-GJyyy dkiPkt">/users/register</span><svg class="sc-fbJfz ivEQut" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0" aria-hidden="true"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></button><div aria-hidden="true" class="sc-fnxdBX dplsyJ"><div class="sc-llcuoK cNCbuV"><div html="" class="sc-iKGpAq sc-cCYyou dXXcln cFvDiF"></div><div tabindex="0" role="button"><div class="sc-jnsZEx eobUac"><span></span>/users/register</div></div></div></div></div></div></div></div></div><div class="sc-emEvRt jYOHCb"></div></div></div>
<script>
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"Wekan REST API","version":"v8.58","description":"The REST API allows you to control and extend Wekan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production Wekan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}}},"components":{"securitySchemes":{"UserSecurity":{"type":"apiKey","in":"header","name":"Authorization"}}}}},"searchIndex":{"store":["section/Production-Security-Concerns","tag/Login","tag/Login/operation/login","tag/Login/operation/register"],"index":{"version":"2.3.9","fields":["title","description"],"fieldVectors":[["title/0",[0,0.641,1,1.113,2,1.113]],["description/0",[0,0.353,3,1.592,4,0.613,5,0.613,6,0.995,7,0.613,8,0.995,9,1.257,10,0.613,11,0.613,12,0.613,13,0.295,14,0.353,15,0.613,16,0.613,17,0.613,18,0.613,19,0.613,20,0.613,21,0.613,22,0.182,23,0.182,24,0.613,25,0.613,26,0.613,27,0.353,28,0.724,29,0.613,30,0.613,31,0.613,32,0.353,33,0.613,34,0.613,35,0.613,36,0.613,37,0.613]],["title/1",[13,0.473]],["description/1",[]],["title/2",[13,0.33,22,0.33,23,0.33]],["description/2",[38,1.941]],["title/3",[22,0.33,23,0.33,39,1.113]],["description/3",[14,0.841,27,0.841,28,0.841,32,0.841,40,1.46,41,1.46,42,1.46,43,1.46]]],"invertedIndex":[["",{"_index":28,"title":{},"description":{"0":{},"3":{}}}],["api",{"_index":23,"title":{"2":{},"3":{}},"description":{"0":{}}}],["authent",{"_index":42,"title":{},"description":{"3":{}}}],["author",{"_index":31,"title":{},"description":{"0":{}}}],["call",{"_index":3,"title":{},"description":{"0":{}}}],["certif",{"_index":12,"title":{},"description":{"0":{}}}],["concern",{"_index":2,"title":{"0":{}},"description":{}}],["ensur",{"_index":6,"title":{},"description":{"0":{}}}],["expir",{"_index":33,"title":{},"description":{"0":{}}}],["few",{"_index":25,"title":{},"description":{"0":{}}}],["highli",{"_index":20,"title":{},"description":{"0":{}}}],["http",{"_index":9,"title":{},"description":{"0":{}}}],["implement",{"_index":29,"titLine truncated
const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","info":{"title":"Wekan REST API","version":"v8.78","description":"The REST API allows you to control and extend Wekan with ease.\n\nIf you are an end-user and not a dev or a tester, [create an issue](https://github.com/wekan/wekan/issues/new) to request new APIs.\n\n> All API calls in the documentation are made using `curl`. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.\n\n# Production Security Concerns\nWhen calling a production Wekan server, ensure it is running via HTTPS and has a valid SSL Certificate. The login method requires you to post your username and password in plaintext, which is why we highly suggest only calling the REST login api over HTTPS. Also, few things to note:\n\n* Only call via HTTPS\n* Implement a timed authorization token expiration strategy\n* Ensure the calling user only has permissions for what they are calling and no more\n"},"paths":{"/users/login":{"post":{"operationId":"login","summary":"Login with REST API","tags":["Login"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"}}}}},"description":"Login credentials","required":true},"responses":{"200":{"description":"Successful authentication","content":{"*/*":{"schema":{"type":"object","required":["id","token","tokenExpires"],"properties":{"id":{"type":"string","description":"User ID"},"token":{"type":"string","description":"Authentication token\n"},"tokenExpires":{"type":"string","format":"date-time","description":"Token expiration date\n"}}}}}},"400":{"description":"Error in authentication\n","content":{"*/*":{"schema":{"type":"object","properties":{"error":{"type":"string"},"reason":{"type":"string"}}}}}},"default":{"description":"Error in authentication\n"}}}},"/users/register":{"post":{"operationId":"register","summary":"Register with REST API","description":"Notes:\n - You will need to provide the token for any of the authenticated methods.\n","tags":["Login"],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"username":{"description":"Your username\n","type":"string"},"password":{"description":"Your password\n","type":"string","format":"password"},"email":{"description":"Your email\n","type":"string"}},"required":["username","password","email"]}}},"required":true},"responses":{"200":{"description":"Successful registration","content":{"*/*":{"schema":{"items":{"properties":{"id":{"type":"string"},"token":{"type":"string"},"tokenExpires":{"type":"string"}}}}}}},"400":{"description":"Error in registration\n","content":{"*/*":{"schema":{"items":{"properties":{"error":{"type":"number"},"reason":{"type":"string"}}}}}}},"default":{"description":"Error in registration\n"}}}}},"components":{"securitySchemes":{"UserSecurity":{"type":"apiKey","in":"header","name":"Authorization"}}}}},"searchIndex":{"store":["section/Production-Security-Concerns","tag/Login","tag/Login/operation/login","tag/Login/operation/register"],"index":{"version":"2.3.9","fields":["title","description"],"fieldVectors":[["title/0",[0,0.641,1,1.113,2,1.113]],["description/0",[0,0.353,3,1.592,4,0.613,5,0.613,6,0.995,7,0.613,8,0.995,9,1.257,10,0.613,11,0.613,12,0.613,13,0.295,14,0.353,15,0.613,16,0.613,17,0.613,18,0.613,19,0.613,20,0.613,21,0.613,22,0.182,23,0.182,24,0.613,25,0.613,26,0.613,27,0.353,28,0.724,29,0.613,30,0.613,31,0.613,32,0.353,33,0.613,34,0.613,35,0.613,36,0.613,37,0.613]],["title/1",[13,0.473]],["description/1",[]],["title/2",[13,0.33,22,0.33,23,0.33]],["description/2",[38,1.941]],["title/3",[22,0.33,23,0.33,39,1.113]],["description/3",[14,0.841,27,0.841,28,0.841,32,0.841,40,1.46,41,1.46,42,1.46,43,1.46]]],"invertedIndex":[["",{"_index":28,"title":{},"description":{"0":{},"3":{}}}],["api",{"_index":23,"title":{"2":{},"3":{}},"description":{"0":{}}}],["authent",{"_index":42,"title":{},"description":{"3":{}}}],["author",{"_index":31,"title":{},"description":{"0":{}}}],["call",{"_index":3,"title":{},"description":{"0":{}}}],["certif",{"_index":12,"title":{},"description":{"0":{}}}],["concern",{"_index":2,"title":{"0":{}},"description":{}}],["ensur",{"_index":6,"title":{},"description":{"0":{}}}],["expir",{"_index":33,"title":{},"description":{"0":{}}}],["few",{"_index":25,"title":{},"description":{"0":{}}}],["highli",{"_index":20,"title":{},"description":{"0":{}}}],["http",{"_index":9,"title":{},"description":{"0":{}}}],["implement",{"_index":29,"titLine truncated
var container = document.getElementById('redoc');
Redoc.hydrate(__redoc_state, container);
+1 -1
View File
@@ -1,7 +1,7 @@
swagger: '2.0'
info:
title: Wekan REST API
version: v8.58
version: v8.78
description: |
The REST API allows you to control and extend Wekan with ease.
+8 -8
View File
@@ -25,13 +25,13 @@ do
sudo apt install -y build-essential gcc g++ make git curl wget p7zip-full zip unzip unp npm p7zip-full
#sudo chown -R $(id -u):$(id -g) $HOME/.npm
sudo npm -g install n
sudo n 22.22.2
sudo n 24.14.1
sudo npm -g install meteor --unsafe-perm
#sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "macOS"
brew install npm
brew install node@22
brew install node@24
directory_name="~/.npm"
if [ ! -d "$directory_name" ]; then
mkdir "$directory_name"
@@ -81,7 +81,7 @@ do
#---------------------------------------------------------------------
# Logging of terminal output to console and to ../wekan-log.txt at end of this line: 2>&1 | tee ../wekan-log.txt
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000 2>&1 | tee ../wekan-log.txt
WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --port 4000 2>&1 | tee ../wekan-log.txt
#---------------------------------------------------------------------
break
;;
@@ -91,7 +91,7 @@ do
#Not in use, could increase RAM usage: NODE_OPTIONS="--max_old_space_size=4096"
#---------------------------------------------------------------------
# Logging of terminal output to console and to ../wekan-log.txt at end of this line: 2>&1 | tee ../wekan-log.txt
WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings" WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000 2>&1 | tee ../wekan-log.txt
WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings" WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --port 4000 2>&1 | tee ../wekan-log.txt
#---------------------------------------------------------------------
break
;;
@@ -101,7 +101,7 @@ do
#---------------------------------------------------------------------
#Logging of terminal output to console and to ../wekan-log.txt at end of this line: 2>&1 | tee ../wekan-log.txt
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000 --extra-packages bundle-visualizer --production 2>&1 | tee ../wekan-log.txt
WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://localhost:4000 meteor run --port 4000 --extra-packages bundle-visualizer --production 2>&1 | tee ../wekan-log.txt
#---------------------------------------------------------------------
break
;;
@@ -118,7 +118,7 @@ do
#---------------------------------------------------------------------
#Logging of terminal output to console and to ../wekan-log.txt at end of this line: 2>&1 | tee ../wekan-log.txt
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
DEBUG=true WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000 2>&1 | tee ../wekan-log.txt
DEBUG=true WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:4000 meteor run --port 4000 2>&1 | tee ../wekan-log.txt
#---------------------------------------------------------------------
break
;;
@@ -135,7 +135,7 @@ do
#---------------------------------------------------------------------
#Logging of terminal output to console and to ../wekan-log.txt at end of this line: 2>&1 | tee ../wekan-log.txt
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
MONGO_URL=mongodb://127.0.0.1:27019/wekan WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:4000 meteor run --exclude-archs web.browser.legacy,web.cordova --port 4000 2>&1 | tee ../wekan-log.txt
MONGO_URL=mongodb://127.0.0.1:27019/wekan WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:4000 meteor run --port 4000 2>&1 | tee ../wekan-log.txt
#---------------------------------------------------------------------
break
;;
@@ -152,7 +152,7 @@ do
#---------------------------------------------------------------------
#Logging of terminal output to console and to ../wekan-log.txt at end of this line: 2>&1 | tee ../wekan-log.txt
#WARN_WHEN_USING_OLD_API=true NODE_OPTIONS="--trace-warnings"
WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:$PORT meteor run --exclude-archs web.browser.legacy,web.cordova --port $PORT 2>&1 | tee ../wekan-log.txt
WRITABLE_PATH=.. WITH_API=true RICHER_CARD_COMMENT_EDITOR=false ROOT_URL=http://$IPADDRESS:$PORT meteor run --port $PORT 2>&1 | tee ../wekan-log.txt
#---------------------------------------------------------------------
break
;;
+1 -1
View File
@@ -3,7 +3,7 @@
REM Build WeKan Windows (win64) bundle - Meteor 3 style (no fibers needed).
REM
REM Prerequisites:
REM 1. Node.js 22.x from https://nodejs.org/
REM 1. Node.js 24.x from https://nodejs.org/
REM 2. 7-Zip from https://www.7-zip.org/
REM 3. wget (choco install wget) or use curl
REM 4. Visual Studio Build Tools 2019 or later
+15
View File
@@ -9,6 +9,21 @@ fi
VERSION=$1
# Check is there script for deleting Docker containers
W_DIR="../w"
CLEANUP_DIR="$W_DIR/docker-cleanup-volumes"
if [ ! -d "$W_DIR" ]; then
mkdir -p "$W_DIR"
fi
if [ ! -d "$CLEANUP_DIR" ]; then
(cd "$W_DIR" && git clone https://github.com/wekan/docker-cleanup-volumes)
fi
# Delete Docker containers
(cd ../w/docker-cleanup-volumes && git pull && ./start.sh)
# Install Docker build deps
./releases/docker-build-deps.sh
# Ensure you are using the correct builder
docker buildx use mybuilder
+58 -8
View File
@@ -12,7 +12,7 @@
# - Make sure all code changes are ready to ship
#
# What this script does:
# 0. Updates Node.js to the latest 22.x release across all files
# 0. Updates Node.js to the latest 24.x release across all files
# 1. Rebuilds OpenAPI docs (wekan.yml + wekan.html) from current source
# 2. Updates all WeKan version numbers in release files
# 3. Commits and pushes the version bump
@@ -46,6 +46,9 @@ if ! echo "$OLD" | grep -qE '^[0-9]+\.[0-9]+$' || \
exit 1
fi
# Delete all GitHub Actions Caches, so that old stuff does not break builds.
gh cache delete --all
# Resolve repo root from script location so this works regardless of CWD
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
RELEASES_DIR="$REPO_DIR/releases"
@@ -58,11 +61,31 @@ cd "$REPO_DIR"
echo "=== WeKan release: v$OLD -> v$NEW ==="
echo ""
# ── Step 0: Update Node.js to the latest 22.x release ───────────────────────
# Fetches https://nodejs.org/dist/latest-v22.x/ and updates Dockerfile,
# ── Check if this is a resume from a previous interrupted run ────────────────
if git tag -l | grep -q "^v$NEW$"; then
echo "--- Resume detected: Tag v$NEW already exists locally ---"
# Check if tag was already pushed
if git ls-remote --tags origin 2>/dev/null | grep -q "refs/tags/v$NEW"; then
echo "Tag v$NEW already exists on remote. Nothing to do!"
echo "GitHub Actions release is already in progress at: https://github.com/wekan/wekan/actions"
exit 0
else
echo "Tag exists locally but not on remote. Skipping to Step 4 (push only)..."
SKIP_TO_STEP_4=1
fi
else
SKIP_TO_STEP_4=0
fi
echo ""
if [ "$SKIP_TO_STEP_4" = "0" ]; then
# ── Step 0: Update Node.js to the latest 24.x release ───────────────────────
# Fetches https://nodejs.org/dist/latest-v24.x/ and updates Dockerfile,
# snapcraft.yaml, GitHub Actions workflow, rebuild-wekan.sh, and
# releases/test-download-urls.sh so all platforms use the same newest version.
echo "--- Step 0: Updating Node.js to latest 22.x ---"
echo "--- Step 0: Updating Node.js to latest 24.x ---"
"$RELEASES_DIR/update-node-version.sh"
echo "Done."
echo ""
@@ -84,15 +107,42 @@ echo ""
# ── Step 3: Commit and push ──────────────────────────────────────────────────
echo "--- Step 3: Committing and pushing version bump ---"
git add --all
git commit -m "v$NEW"
git push
if git diff --cached --quiet; then
echo "No changes to commit (already at v$NEW). Skipping commit."
else
git commit -m "v$NEW"
fi
timeout 60 git push || {
echo "Warning: git push failed (timeout or connection error), continuing to tag push..."
}
echo "Done."
echo ""
fi # End of: if [ "$SKIP_TO_STEP_4" = "0" ]
# ── Step 4: Tag and push (this triggers GitHub Actions release-all.yml) ──────
echo "--- Step 4: Creating and pushing tag v$NEW ---"
git tag -a "v$NEW" -m "v$NEW"
git push origin "v$NEW"
# Check if tag already exists locally
if git tag -l | grep -q "^v$NEW$"; then
echo "Tag v$NEW already exists locally. Skipping tag creation."
else
git tag -a "v$NEW" -m "v$NEW"
echo "Created tag v$NEW"
fi
# Push tag to remote (always do this, in case it was created but not pushed)
echo "Pushing tag v$NEW to remote..."
timeout 60 git push origin "v$NEW" || {
echo "Error: Failed to push tag v$NEW (timeout or connection error)"
exit 1
}
# Verify tag was pushed
if ! git ls-remote --tags origin 2>/dev/null | grep -q "refs/tags/v$NEW"; then
echo "Error: Tag v$NEW was not found on remote after push"
exit 1
fi
echo "Done."
echo ""
+11 -11
View File
@@ -58,17 +58,17 @@ check "db-tools 100.13.0 arm64" \
"https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2404-arm64-100.13.0.tgz"
echo ""
echo "=== Node.js 22.22.2 (all snap architectures) ==="
check "Node.js 22.22.2 linux-x64 (amd64)" \
"https://nodejs.org/dist/v22.22.2/node-v22.22.2-linux-x64.tar.gz"
check "Node.js 22.22.2 linux-arm64 (arm64)" \
"https://nodejs.org/dist/v22.22.2/node-v22.22.2-linux-arm64.tar.gz"
check "Node.js 22.22.2 linux-armv7l (armhf)" \
"https://nodejs.org/dist/v22.22.2/node-v22.22.2-linux-armv7l.tar.gz"
check "Node.js 22.22.2 linux-s390x (s390x)" \
"https://nodejs.org/dist/v22.22.2/node-v22.22.2-linux-s390x.tar.gz"
check "Node.js 22.22.2 linux-ppc64le (ppc64el)" \
"https://nodejs.org/dist/v22.22.2/node-v22.22.2-linux-ppc64le.tar.gz"
echo "=== Node.js 24.14.1 (all snap architectures) ==="
check "Node.js 24.14.1 linux-x64 (amd64)" \
"https://nodejs.org/dist/v24.14.1/node-v24.14.1-linux-x64.tar.gz"
check "Node.js 24.14.1 linux-arm64 (arm64)" \
"https://nodejs.org/dist/v24.14.1/node-v24.14.1-linux-arm64.tar.gz"
check "Node.js 24.14.1 linux-armv7l (armhf)" \
"https://nodejs.org/dist/v24.14.1/node-v24.14.1-linux-armv7l.tar.gz"
check "Node.js 24.14.1 linux-s390x (s390x)" \
"https://nodejs.org/dist/v24.14.1/node-v24.14.1-linux-s390x.tar.gz"
check "Node.js 24.14.1 linux-ppc64le (ppc64el)" \
"https://nodejs.org/dist/v24.14.1/node-v24.14.1-linux-ppc64le.tar.gz"
echo ""
echo "=== Results ==="
+32 -32
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Fetch the latest Node.js 22.x release and update all files that hardcode
# Fetch the latest Node.js 24.x release and update all files that hardcode
# the version number: Dockerfile, snapcraft.yaml, GitHub Actions workflow,
# rebuild-wekan.sh, and releases/test-download-urls.sh.
#
@@ -34,69 +34,69 @@ else
sedi() { sed -i "$@"; }
fi
# ── Fetch latest Node.js 22.x version ────────────────────────────────────────
echo " Fetching latest Node.js 22.x version from nodejs.org..."
# ── Fetch latest Node.js 24.x version ────────────────────────────────────────
echo " Fetching latest Node.js 24.x version from nodejs.org..."
# The directory listing always contains a line like:
# node-v22.22.2-linux-x64.tar.gz
# node-v24.14.1-linux-x64.tar.gz
# Extract the version from the first x64 tarball listed.
NEW_NODE=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ \
| grep -o 'node-v22\.[0-9][0-9]*\.[0-9][0-9]*-linux-x64\.tar\.gz' \
NEW_NODE=$(curl -fsSL https://nodejs.org/dist/latest-v24.x/ \
| grep -o 'node-v24\.[0-9][0-9]*\.[0-9][0-9]*-linux-x64\.tar\.gz' \
| head -1 \
| sed 's/node-v\(.*\)-linux-x64\.tar\.gz/\1/')
if [ -z "$NEW_NODE" ]; then
echo "Error: could not determine latest Node.js 22.x version." >&2
echo "Error: could not determine latest Node.js 24.x version." >&2
exit 1
fi
echo " Latest Node.js 22.x: $NEW_NODE"
echo " Latest Node.js 24.x: $NEW_NODE"
# ── Dockerfile ────────────────────────────────────────────────────────────────
# Matches: NODE_VERSION=v22.22.2
sedi "s|NODE_VERSION=v22\.[0-9][0-9]*\.[0-9][0-9]*|NODE_VERSION=v${NEW_NODE}|g" \
# Matches: NODE_VERSION=v24.14.1
sedi "s|NODE_VERSION=v24\.[0-9][0-9]*\.[0-9][0-9]*|NODE_VERSION=v${NEW_NODE}|g" \
Dockerfile
# ── snapcraft.yaml ───────────────────────────────────────────────────────────
# Matches: npm-node-version: 22.22.2
sedi "s|npm-node-version: 22\.[0-9][0-9]*\.[0-9][0-9]*|npm-node-version: ${NEW_NODE}|g" \
# Matches: npm-node-version: 24.14.1
sedi "s|npm-node-version: 24\.[0-9][0-9]*\.[0-9][0-9]*|npm-node-version: ${NEW_NODE}|g" \
snapcraft.yaml
# Matches: NODE_VERSION="22.22.2" (in wekan override-build)
sedi "s|NODE_VERSION=\"22\.[0-9][0-9]*\.[0-9][0-9]*\"|NODE_VERSION=\"${NEW_NODE}\"|g" \
# Matches: NODE_VERSION="24.14.1" (in wekan override-build)
sedi "s|NODE_VERSION=\"24\.[0-9][0-9]*\.[0-9][0-9]*\"|NODE_VERSION=\"${NEW_NODE}\"|g" \
snapcraft.yaml
# Matches comment: matching Node.js 22.22.2 binary
sedi "s|Node\.js 22\.[0-9][0-9]*\.[0-9][0-9]* binary|Node.js ${NEW_NODE} binary|g" \
# Matches comment: matching Node.js 24.14.1 binary
sedi "s|Node\.js 24\.[0-9][0-9]*\.[0-9][0-9]* binary|Node.js ${NEW_NODE} binary|g" \
snapcraft.yaml
# ── .github/workflows/release-all.yml ────────────────────────────────────────
WORKFLOW=".github/workflows/release-all.yml"
# Matches: node-version: '22.22.0' (actions/setup-node, windows job)
sedi "s|node-version: '22\.[0-9][0-9]*\.[0-9][0-9]*'|node-version: '${NEW_NODE}'|g" \
# Matches: node-version: '24.14.1' (actions/setup-node, windows job)
sedi "s|node-version: '24\.[0-9][0-9]*\.[0-9][0-9]*'|node-version: '${NEW_NODE}'|g" \
"$WORKFLOW"
# Matches: node:22.22.0-slim (docker run commands and comments)
sedi "s|node:22\.[0-9][0-9]*\.[0-9][0-9]*-slim|node:${NEW_NODE}-slim|g" \
# Matches: node:24.14.1-slim (docker run commands and comments)
sedi "s|node:24\.[0-9][0-9]*\.[0-9][0-9]*-slim|node:${NEW_NODE}-slim|g" \
"$WORKFLOW"
# Matches version number in comments: bundles: 22.22.0 (ABI
sedi "s|bundles: 22\.[0-9][0-9]*\.[0-9][0-9]* (ABI|bundles: ${NEW_NODE} (ABI|g" \
# Matches version number in comments: bundles: 24.14.1 (ABI
sedi "s|bundles: 24\.[0-9][0-9]*\.[0-9][0-9]* (ABI|bundles: ${NEW_NODE} (ABI|g" \
"$WORKFLOW"
# Matches: Node 22.22.0 — the exact version bundled
sedi "s|Node 22\.[0-9][0-9]*\.[0-9][0-9]* —|Node ${NEW_NODE} —|g" \
# Matches: Node 24.14.1 — the exact version bundled
sedi "s|Node 24\.[0-9][0-9]*\.[0-9][0-9]* —|Node ${NEW_NODE} —|g" \
"$WORKFLOW"
# ── rebuild-wekan.sh ──────────────────────────────────────────────────────────
# Matches: sudo n 22.22.0
sedi "s|sudo n 22\.[0-9][0-9]*\.[0-9][0-9]*|sudo n ${NEW_NODE}|g" \
# Matches: sudo n 24.14.1
sedi "s|sudo n 24\.[0-9][0-9]*\.[0-9][0-9]*|sudo n ${NEW_NODE}|g" \
rebuild-wekan.sh
# ── releases/test-download-urls.sh ───────────────────────────────────────────
# Matches URL path: /v22.22.2/
sedi "s|/v22\.[0-9][0-9]*\.[0-9][0-9]*/|/v${NEW_NODE}/|g" \
# Matches URL path: /v24.14.1/
sedi "s|/v24\.[0-9][0-9]*\.[0-9][0-9]*/|/v${NEW_NODE}/|g" \
releases/test-download-urls.sh
# Matches tarball name: node-v22.22.2-
sedi "s|node-v22\.[0-9][0-9]*\.[0-9][0-9]*-|node-v${NEW_NODE}-|g" \
# Matches tarball name: node-v24.14.1-
sedi "s|node-v24\.[0-9][0-9]*\.[0-9][0-9]*-|node-v${NEW_NODE}-|g" \
releases/test-download-urls.sh
# Matches label text: Node.js 22.22.2
sedi "s|Node\.js 22\.[0-9][0-9]*\.[0-9][0-9]*|Node.js ${NEW_NODE}|g" \
# Matches label text: Node.js 24.14.1
sedi "s|Node\.js 24\.[0-9][0-9]*\.[0-9][0-9]*|Node.js ${NEW_NODE}|g" \
releases/test-download-urls.sh
echo " Updated all files to Node.js $NEW_NODE."
+2 -2
View File
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
appTitle = (defaultText = "Wekan"),
# The name of the app as it is displayed to the user.
appVersion = 858,
appVersion = 878,
# Increment this for every release.
appMarketingVersion = (defaultText = "8.58.0~2026-03-23"),
appMarketingVersion = (defaultText = "8.78.0~2026-03-23"),
# Human-readable presentation of the app version.
minUpgradableAppVersion = 0,
+5 -5
View File
@@ -28,11 +28,11 @@ class AttachmentMigrationService {
* @param {string} boardId - The board ID
* @returns {boolean} - True if board has been migrated
*/
isBoardMigrated(boardId) {
async isBoardMigrated(boardId) {
const isMigrated = migratedBoards.has(boardId);
// Update status collection for pub/sub
AttachmentMigrationStatus.upsert(
await AttachmentMigrationStatus.upsertAsync(
{ boardId },
{
$set: {
@@ -53,7 +53,7 @@ class AttachmentMigrationService {
async migrateBoardAttachments(boardId) {
try {
// Check if board has already been migrated
if (this.isBoardMigrated(boardId)) {
if (await this.isBoardMigrated(boardId)) {
console.log(`Board ${boardId} has already been migrated, skipping`);
return { success: true, message: 'Board already migrated' };
}
@@ -102,7 +102,7 @@ class AttachmentMigrationService {
console.log(`Marked board ${boardId} as migrated`);
// Update status collection
AttachmentMigrationStatus.upsert(
await AttachmentMigrationStatus.upsertAsync(
{ boardId },
{
$set: {
@@ -224,7 +224,7 @@ class AttachmentMigrationService {
const migratedCount = total - unconverted.length;
// Update status collection for pub/sub
AttachmentMigrationStatus.upsert(
await AttachmentMigrationStatus.upsertAsync(
{ boardId },
{
$set: {
+4 -4
View File
@@ -75,7 +75,7 @@ Meteor.startup(() => {
process.env.ORACLE_OIM_ENABLED === 'true' ||
process.env.ORACLE_OIM_ENABLED === true
) {
ServiceConfiguration.configurations.upsert(
ServiceConfiguration.configurations.upsertAsync(
// eslint-disable-line no-undef
{ service: 'oidc' },
{
@@ -97,7 +97,7 @@ Meteor.startup(() => {
process.env.OAUTH2_ENABLED === 'true' ||
process.env.OAUTH2_ENABLED === true
) {
ServiceConfiguration.configurations.upsert(
ServiceConfiguration.configurations.upsertAsync(
// eslint-disable-line no-undef
{ service: 'oidc' },
{
@@ -121,7 +121,7 @@ Meteor.startup(() => {
process.env.CAS_ENABLED === 'true' ||
process.env.CAS_ENABLED === true
) {
ServiceConfiguration.configurations.upsert(
ServiceConfiguration.configurations.upsertAsync(
// eslint-disable-line no-undef
{ service: 'cas' },
{
@@ -145,7 +145,7 @@ Meteor.startup(() => {
process.env.SAML_ENABLED === 'true' ||
process.env.SAML_ENABLED === true
) {
ServiceConfiguration.configurations.upsert(
ServiceConfiguration.configurations.upsertAsync(
// eslint-disable-line no-undef
{ service: 'saml' },
{
+9 -9
View File
@@ -1713,7 +1713,7 @@ class CronMigrationManager {
this.startTime = Date.now();
// Update CronJobStatus for immediate pub/sub notification
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
@@ -1784,7 +1784,7 @@ class CronMigrationManager {
this.isRunning = false;
// Update CronJobStatus
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
@@ -1800,7 +1800,7 @@ class CronMigrationManager {
}
// Update to running state
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
@@ -2555,7 +2555,7 @@ class CronMigrationManager {
cronMigrationStatus.set('Migrations paused');
// Update CronJobStatus for immediate pub/sub notification
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
@@ -2577,7 +2577,7 @@ class CronMigrationManager {
}
// Update to final paused state
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
@@ -2597,7 +2597,7 @@ class CronMigrationManager {
*/
async stopAllMigrations() {
// Update CronJobStatus for immediate pub/sub notification
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
@@ -2635,7 +2635,7 @@ class CronMigrationManager {
cronMigrationStatus.set('All migrations stopped');
// Update to final stopped state
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
@@ -2649,9 +2649,9 @@ class CronMigrationManager {
);
// Clear status message after delay
Meteor.setTimeout(() => {
Meteor.setTimeout(async () => {
cronMigrationStatus.set('');
CronJobStatus.upsert(
await CronJobStatus.upsertAsync(
{ jobId: 'migration' },
{
$set: {
+11 -3
View File
@@ -148,7 +148,10 @@ Activities.after.insert(async (userId, doc) => {
if (activity.commentId) {
const comment = await activity.comment();
params.comment = normalizeActivityText(comment.text);
if (!comment) {
console.warn('[Activities.after.insert] Comment not found for commentId:', activity.commentId, '— skipping comment params.');
}
params.comment = normalizeActivityText(comment?.text);
let hasMentions = false;
if (board) {
const knownUsers = [];
@@ -237,7 +240,9 @@ Activities.after.insert(async (userId, doc) => {
}
}
}
params.commentId = comment._id;
if (comment) {
params.commentId = comment._id;
}
params.hasMentions = hasMentions;
}
@@ -328,8 +333,11 @@ Activities.after.insert(async (userId, doc) => {
}
});
const integrationBoardIds = board
? [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID]
: [Integrations.Const.GLOBAL_WEBHOOK_ID];
const integrations = await ReactiveCache.getIntegrations({
boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] },
boardId: { $in: integrationBoardIds },
enabled: true,
activities: { $in: [description, 'all'] },
});
+29 -13
View File
@@ -8,6 +8,10 @@ import CardComments from '/models/cardComments';
async function commentCreation(userId, doc) {
const card = await ReactiveCache.getCard(doc.cardId);
if (!card) {
console.warn('[commentCreation] Card not found for cardId:', doc.cardId, '— skipping activity insert.');
return;
}
await Activities.insertAsync({
userId,
activityType: 'addComment',
@@ -30,6 +34,10 @@ CardComments.after.insert(async (userId, doc) => {
CardComments.after.update(async (userId, doc) => {
const card = await ReactiveCache.getCard(doc.cardId);
if (!card) {
console.warn('[CardComments.after.update] Card not found for cardId:', doc.cardId, '— skipping activity insert.');
return;
}
await Activities.insertAsync({
userId,
activityType: 'editComment',
@@ -42,19 +50,27 @@ CardComments.after.update(async (userId, doc) => {
});
CardComments.before.remove(async (userId, doc) => {
const card = await ReactiveCache.getCard(doc.cardId);
await Activities.insertAsync({
userId,
activityType: 'deleteComment',
boardId: doc.boardId,
cardId: doc.cardId,
commentId: doc._id,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
const activity = await ReactiveCache.getActivity({ commentId: doc._id });
if (activity) {
await Activities.removeAsync(activity._id);
try {
const card = await ReactiveCache.getCard(doc.cardId);
if (!card) {
console.warn('[CardComments.before.remove] Card not found for cardId:', doc.cardId, '— skipping deleteComment activity.');
} else {
await Activities.insertAsync({
userId,
activityType: 'deleteComment',
boardId: doc.boardId,
cardId: doc.cardId,
commentId: doc._id,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
}
const activity = await ReactiveCache.getActivity({ commentId: doc._id });
if (activity) {
await Activities.removeAsync(activity._id);
}
} catch (e) {
console.error('[CardComments.before.remove] Error while processing comment deletion for doc._id:', doc._id, e);
}
});
+14 -1
View File
@@ -166,6 +166,19 @@ Meteor.methods({
await Users.updateAsync(this.userId, { $set: { 'profile.avatarUrl': avatarUrl } });
},
async adminSetAvatarUrl(targetUserId, avatarUrl) {
check(targetUserId, String);
check(avatarUrl, String);
if (!this.userId) throw new Meteor.Error('not-logged-in', 'User must be logged in');
const currentUser = await Users.findOneAsync(this.userId);
if (!currentUser || !currentUser.isAdmin) {
throw new Meteor.Error('not-authorized', 'Only admins can change user avatars');
}
const targetUser = await Users.findOneAsync(targetUserId);
if (!targetUser) throw new Meteor.Error('user-not-found', 'Target user not found');
await Users.updateAsync(targetUserId, { $set: { 'profile.avatarUrl': avatarUrl } });
},
async toggleBoardStar(boardId) {
check(boardId, String);
if (!this.userId) throw new Meteor.Error('not-logged-in', 'User must be logged in');
@@ -858,7 +871,7 @@ const runNotificationCleanup = async function runNotificationCleanup() {
for (const user of await ReactiveCache.getUsers()) {
if (!user.profile || !user.profile.notifications) continue;
for (const notification of user.profile.notifications) {
if (notification.read) {
if (notification && notification.read) {
const removeDate = new Date(notification.read);
removeDate.setDate(removeDate.getDate() + removeAge);
if (removeDate <= new Date()) {
+11
View File
@@ -5,3 +5,14 @@ Meteor.publish('my-avatars', async function() {
const ret = (await ReactiveCache.getAvatars({ userId: this.userId }, {}, true)).cursor;
return ret;
});
Meteor.publish('avatars-for-user', async function(targetUserId) {
check(targetUserId, String);
// Allow admins to view avatars for any user
const currentUser = await Meteor.users.findOneAsync(this.userId);
if (!currentUser || !currentUser.isAdmin) {
return this.ready();
}
const ret = (await ReactiveCache.getAvatars({ userId: targetUserId }, {}, true)).cursor;
return ret;
});
+1
View File
@@ -17,6 +17,7 @@ Meteor.publish('people', async function(query, limit, skip = 0) {
username: 1,
'profile.fullname': 1,
'profile.initials': 1,
'profile.avatarUrl': 1,
isAdmin: 1,
emails: 1,
createdAt: 1,
+1
View File
@@ -1,4 +1,5 @@
import Users from '/models/users';
import { ReactiveCache } from '/imports/reactiveCache';
Meteor.publish('user-miniprofile', async function (usernames) {
check(usernames, Array);
+3 -2
View File
@@ -68,22 +68,23 @@ Meteor.methods({
let mongoOplogEnabled;
try {
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
oplogEnabled = Boolean(
mongoOplogEnabled = Boolean(
mongo._oplogHandle && mongo._oplogHandle.onOplogEntry,
);
const { version, storageEngine } = await mongo.db.command({ serverStatus: 1 });
mongoVersion = version;
mongoStorageEngine = storageEngine.name;
mongoOplogEnabled = oplogEnabled;
} catch (e) {
try {
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
const { version } = await mongo.db.command({ buildinfo: 1 });
mongoVersion = version;
mongoStorageEngine = 'unknown';
mongoOplogEnabled = false;
} catch (e2) {
mongoVersion = 'unknown';
mongoStorageEngine = 'unknown';
mongoOplogEnabled = false;
}
}
statistics.mongo = {
+12 -1
View File
@@ -3,12 +3,19 @@
# All supported keys are defined here together with descriptions and default values
# list of supported keys
keys="DEBUG S3 MONGO_LOG_DESTINATION MONGO_URL MONGODB_BIND_UNIX_SOCKET MONGO_URL MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM MAIL_SERVICE MAIL_SERVICE_USER MAIL_SERVICE_PASSWORD ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API RICHER_CARD_COMMENT_EDITOR CARD_OPENED_WEBHOOK_ENABLED ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS ATTACHMENTS_UPLOAD_EXTERNAL_PROGRAM ATTACHMENTS_UPLOAD_MIME_TYPES ATTACHMENTS_UPLOAD_MAX_SIZE AVATARS_UPLOAD_EXTERNAL_PROGRAM AVATARS_UPLOAD_MIME_TYPES AVATARS_UPLOAD_MAX_SIZE MAX_IMAGE_PIXEL IMAGE_COMPRESS_RATIO BIGEVENTS_PATTERN NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE NOTIFY_DUE_DAYS_BEFORE_AND_AFTER NOTIFY_DUE_AT_HOUR_OF_DAY DEFAULT_BOARD_ID EMAIL_NOTIFICATION_TIMEOUT CORS CORS_ALLOW_HEADERS CORS_EXPOSE_HEADERS MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME METRICS_ALLOWED_IP_ADDRESSES BROWSER_POLICY_ENABLED TRUSTED_URL WEBHOOKS_ATTRIBUTES OAUTH2_ENABLED OIDC_REDIRECTION_ENABLED OAUTH2_CA_CERT OAUTH2_LOGIN_STYLE OAUTH2_CLIENT_ID OAUTH2_SECRET OAUTH2_SERVER_URL OAUTH2_AUTH_ENDPOINT OAUTH2_USERINFO_ENDPOINT OAUTH2_TOKEN_ENDPOINT OAUTH2_ID_MAP OAUTH2_USERNAME_MAP OAUTH2_FULLNAME_MAP OAUTH2_ID_TOKEN_WHITELIST_FIELDS OAUTH2_EMAIL_MAP OAUTH2_REQUEST_PERMISSIONS OAUTH2_ADFS_ENABLED OAUTH2_B2C_ENABLED LDAP_ENABLE LDAP_PORT LDAP_HOST LDAP_AD_SIMPLE_AUTH LDAP_BASEDN LDAP_LOGIN_FALLBACK LDAP_RECONNECT LDAP_TIMEOUT LDAP_IDLE_TIMEOUT LDAP_CONNECT_TIMEOUT LDAP_AUTHENTIFICATION LDAP_AUTHENTIFICATION_USERDN LDAP_AUTHENTIFICATION_PASSWORD LDAP_LOG_ENABLED LDAP_BACKGROUND_SYNC LDAP_BACKGROUND_SYNC_INTERVAL LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS LDAP_ENCRYPTION LDAP_CA_CERT LDAP_REJECT_UNAUTHORIZED LDAP_USER_AUTHENTICATION LDAP_USER_AUTHENTICATION_FIELD LDAP_USER_SEARCH_FILTER LDAP_USER_SEARCH_SCOPE LDAP_USER_SEARCH_FIELD LDAP_SEARCH_PAGE_SIZE LDAP_SEARCH_SIZE_LIMIT LDAP_GROUP_FILTER_ENABLE LDAP_GROUP_FILTER_OBJECTCLASS LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT LDAP_GROUP_FILTER_GROUP_NAME LDAP_UNIQUE_IDENTIFIER_FIELD LDAP_UTF8_NAMES_SLUGIFY LDAP_USERNAME_FIELD LDAP_FULLNAME_FIELD LDAP_MERGE_EXISTING_USERS LDAP_SYNC_USER_DATA LDAP_SYNC_USER_DATA_FIELDMAP LDAP_SYNC_GROUP_ROLES LDAP_DEFAULT_DOMAIN LDAP_EMAIL_MATCH_ENABLE LDAP_EMAIL_MATCH_REQUIRE LDAP_EMAIL_MATCH_VERIFIED LDAP_EMAIL_FIELD LDAP_SYNC_ADMIN_STATUS LDAP_SYNC_ADMIN_GROUPS HEADER_LOGIN_ID HEADER_LOGIN_FIRSTNAME HEADER_LOGIN_LASTNAME HEADER_LOGIN_EMAIL LOGOUT_WITH_TIMER LOGOUT_IN LOGOUT_ON_HOURS LOGOUT_ON_MINUTES DEFAULT_AUTHENTICATION_METHOD PASSWORD_LOGIN_ENABLED CAS_ENABLED CAS_BASE_URL CAS_LOGIN_URL CAS_VALIDATE_URL SAML_ENABLED SAML_PROVIDER SAML_ENTRYPOINT SAML_ISSUER SAML_CERT SAML_IDPSLO_REDIRECTURL SAML_PRIVATE_KEYFILE SAML_PUBLIC_CERTFILE SAML_IDENTIFIER_FORMAT SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE SAML_ATTRIBUTES ORACLE_OIM_ENABLED RESULTS_PER_PAGE WAIT_SPINNER NODE_OPTIONS LDAP_AUTHENTIFICATION_PASSWORD_FILE OAUTH2_SECRET_FILE MAIL_SERVICE_PASSWORD_FILE MONGO_PASSWORD_FILE S3_SECRET_FILE"
keys="DEBUG S3 METEOR_REACTIVITY_ORDER MONGO_LOG_DESTINATION MONGO_URL MONGO_OPLOG_URL MONGODB_BIND_UNIX_SOCKET MONGO_URL MONGODB_BIND_IP MONGODB_PORT MAIL_URL MAIL_FROM MAIL_SERVICE MAIL_SERVICE_USER MAIL_SERVICE_PASSWORD ROOT_URL PORT DISABLE_MONGODB CADDY_ENABLED CADDY_BIND_PORT WITH_API RICHER_CARD_COMMENT_EDITOR CARD_OPENED_WEBHOOK_ENABLED ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW ACCOUNTS_COMMON_LOGIN_EXPIRATION_IN_DAYS ATTACHMENTS_UPLOAD_EXTERNAL_PROGRAM ATTACHMENTS_UPLOAD_MIME_TYPES ATTACHMENTS_UPLOAD_MAX_SIZE AVATARS_UPLOAD_EXTERNAL_PROGRAM AVATARS_UPLOAD_MIME_TYPES AVATARS_UPLOAD_MAX_SIZE MAX_IMAGE_PIXEL IMAGE_COMPRESS_RATIO BIGEVENTS_PATTERN NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE NOTIFY_DUE_DAYS_BEFORE_AND_AFTER NOTIFY_DUE_AT_HOUR_OF_DAY DEFAULT_BOARD_ID EMAIL_NOTIFICATION_TIMEOUT CORS CORS_ALLOW_HEADERS CORS_EXPOSE_HEADERS MATOMO_ADDRESS MATOMO_SITE_ID MATOMO_DO_NOT_TRACK MATOMO_WITH_USERNAME METRICS_ALLOWED_IP_ADDRESSES BROWSER_POLICY_ENABLED TRUSTED_URL WEBHOOKS_ATTRIBUTES OAUTH2_ENABLED OIDC_REDIRECTION_ENABLED OAUTH2_CA_CERT OAUTH2_LOGIN_STYLE OAUTH2_CLIENT_ID OAUTH2_SECRET OAUTH2_SERVER_URL OAUTH2_AUTH_ENDPOINT OAUTH2_USERINFO_ENDPOINT OAUTH2_TOKEN_ENDPOINT OAUTH2_ID_MAP OAUTH2_USERNAME_MAP OAUTH2_FULLNAME_MAP OAUTH2_ID_TOKEN_WHITELIST_FIELDS OAUTH2_EMAIL_MAP OAUTH2_REQUEST_PERMISSIONS OAUTH2_ADFS_ENABLED OAUTH2_B2C_ENABLED LDAP_ENABLE LDAP_PORT LDAP_HOST LDAP_AD_SIMPLE_AUTH LDAP_BASEDN LDAP_LOGIN_FALLBACK LDAP_RECONNECT LDAP_TIMEOUT LDAP_IDLE_TIMEOUT LDAP_CONNECT_TIMEOUT LDAP_AUTHENTIFICATION LDAP_AUTHENTIFICATION_USERDN LDAP_AUTHENTIFICATION_PASSWORD LDAP_LOG_ENABLED LDAP_BACKGROUND_SYNC LDAP_BACKGROUND_SYNC_INTERVAL LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED LDAP_BACKGROUND_SYNC_IMPORT_NEW_USERS LDAP_ENCRYPTION LDAP_CA_CERT LDAP_REJECT_UNAUTHORIZED LDAP_USER_AUTHENTICATION LDAP_USER_AUTHENTICATION_FIELD LDAP_USER_SEARCH_FILTER LDAP_USER_SEARCH_SCOPE LDAP_USER_SEARCH_FIELD LDAP_SEARCH_PAGE_SIZE LDAP_SEARCH_SIZE_LIMIT LDAP_GROUP_FILTER_ENABLE LDAP_GROUP_FILTER_OBJECTCLASS LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT LDAP_GROUP_FILTER_GROUP_NAME LDAP_UNIQUE_IDENTIFIER_FIELD LDAP_UTF8_NAMES_SLUGIFY LDAP_USERNAME_FIELD LDAP_FULLNAME_FIELD LDAP_MERGE_EXISTING_USERS LDAP_SYNC_USER_DATA LDAP_SYNC_USER_DATA_FIELDMAP LDAP_SYNC_GROUP_ROLES LDAP_DEFAULT_DOMAIN LDAP_EMAIL_MATCH_ENABLE LDAP_EMAIL_MATCH_REQUIRE LDAP_EMAIL_MATCH_VERIFIED LDAP_EMAIL_FIELD LDAP_SYNC_ADMIN_STATUS LDAP_SYNC_ADMIN_GROUPS HEADER_LOGIN_ID HEADER_LOGIN_FIRSTNAME HEADER_LOGIN_LASTNAME HEADER_LOGIN_EMAIL LOGOUT_WITH_TIMER LOGOUT_IN LOGOUT_ON_HOURS LOGOUT_ON_MINUTES DEFAULT_AUTHENTICATION_METHOD PASSWORD_LOGIN_ENABLED CAS_ENABLED CAS_BASE_URL CAS_LOGIN_URL CAS_VALIDATE_URL SAML_ENABLED SAML_PROVIDER SAML_ENTRYPOINT SAML_ISSUER SAML_CERT SAML_IDPSLO_REDIRECTURL SAML_PRIVATE_KEYFILE SAML_PUBLIC_CERTFILE SAML_IDENTIFIER_FORMAT SAML_LOCAL_PROFILE_MATCH_ATTRIBUTE SAML_ATTRIBUTES ORACLE_OIM_ENABLED RESULTS_PER_PAGE WAIT_SPINNER NODE_OPTIONS LDAP_AUTHENTIFICATION_PASSWORD_FILE OAUTH2_SECRET_FILE MAIL_SERVICE_PASSWORD_FILE MONGO_PASSWORD_FILE S3_SECRET_FILE"
DESCRIPTION_S3='AWS S3 for files. Example: {"s3":{"key": "xxx", "secret": "xxx", "bucket": "xxx", "region": "eu-west-1"}}'
DEFAULT_S3=""
KEY_S3="s3"
#-------------------- CHANGE STREAMS --------------------
# https://forums.meteor.com/t/meteor-3-5-beta-change-streams-performance-improvements/64461#change-streams-setup-3
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
DESCRIPTION_METEOR_REACTIVITY_ORDER="METEOR_REACTIVITY_ORDER. Default: 'changeStreams,oplog,polling'"
DEFAULT_METEOR_REACTIVITY_ORDER="changeStreams,oplog,polling"
KEY_METEOR_REACTIVITY_ORDER='meteor-reactivity-order'
#-----------------------------------------------------------------
# ==== AWS S3 FOR FILES ====
# Any region. For example:
@@ -49,6 +56,10 @@ DESCRIPTION_MONGO_URL="MONGO_URL. Default: ''"
DEFAULT_MONGO_URL=""
KEY_MONGO_URL='mongo-url'
DESCRIPTION_MONGO_OPLOG_URL="MongoDB Oplog URL. Default: 'mongodb://127.0.0.1:27017/local?replicaSet=rs0'"
DEFAULT_MONGO_OPLOG_URL=""
KEY_MONGO_OPLOG_URL="mongo-oplog-url"
DESCRIPTION_MONGODB_BIND_UNIX_SOCKET="mongodb binding unix socket. Example: /var/snap/wekan/current/share . Default: ''\n"\
"\t\t\t Default behaviour will preffer binding over unix socket, to disable unix socket binding set value to 'nill' string\n"\
"\t\t\t To bind to instance of mongodb provided through content interface,set value to relative path to the socket inside '$SNAP_DATA/shared' directory"
+38 -1
View File
@@ -129,7 +129,44 @@ fi
echo "Starting MongoDB 7.x server..."
# Create PID file
echo $$ > "${SNAP_COMMON}/mongodb.pid"
# Check if this is the first time MongoDB is starting
INIT_MARKER="${SNAP_COMMON}/.replica_set_initialized"
# Start MongoDB directly in foreground with replica set
# The replica set will be initialized by the init marker check below
echo "Starting MongoDB 7.x server in foreground..."
# Start in background briefly to initialize replica set if needed
if [ ! -f "$INIT_MARKER" ]; then
echo "First startup detected - initializing replica set..."
/snap/${SNAP_NAME}/current/bin/mongod \
--dbpath="$MONGO_DATA_DIR" \
--logpath="$MONGO_LOG_FILE" \
--logappend $BIND_OPTIONS \
--replSet rs0 --oplogSize 128 --fork
sleep 5
# Initialize replica set
/snap/${SNAP_NAME}/current/bin/mongosh --port ${MONGODB_PORT:-27019} --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: '127.0.0.1:${MONGODB_PORT:-27019}'}]})" 2>/dev/null || true
# Mark initialization as done
touch "$INIT_MARKER"
# Kill the forked process gracefully
echo "Stopping temporary MongoDB process..."
/snap/${SNAP_NAME}/current/bin/mongosh --port ${MONGODB_PORT:-27019} --eval "db.getSiblingDB('admin').shutdownServer()" 2>/dev/null || true
sleep 2
# Clean up any remaining process
pkill -f "mongod.*${MONGODB_PORT:-27019}" 2>/dev/null || true
sleep 1
fi
# Final start of MongoDB in foreground (this becomes the main process)
exec /snap/${SNAP_NAME}/current/bin/mongod \
--dbpath="$MONGO_DATA_DIR" \
--logpath="$MONGO_LOG_FILE" \
--logappend $BIND_OPTIONS
--logappend $BIND_OPTIONS \
--replSet rs0 --oplogSize 128
+3
View File
@@ -14,6 +14,9 @@ if [ "nill" != "$MONGODB_BIND_UNIX_SOCKET" ] && [ "x" != "x$MONGODB_BIND_UNIX_SO
else
[ "x" == "x$MONGODB_BIND_IP" ] && MONGODB_BIND_IP="127.0.0.1"
export MONGO_URL="mongodb://$MONGODB_BIND_IP:$MONGODB_PORT/wekan"
if [ -z "$MONGO_OPLOG_URL" ]; then
export MONGO_OPLOG_URL="mongodb://$MONGODB_BIND_IP:$MONGODB_PORT/local?replicaSet=rs0"
fi
fi
# If uploads directory does not exist, create it.
+10 -10
View File
@@ -1,5 +1,5 @@
name: wekan
version: '8.58'
version: '8.78'
base: core24
summary: Open Source kanban
description: |
@@ -113,7 +113,7 @@ parts:
wekan:
source: .
plugin: npm
npm-node-version: 22.22.2
npm-node-version: 24.14.1
npm-include-node: true
build-packages:
- build-essential
@@ -135,17 +135,17 @@ parts:
rm -rf .build
mkdir .build
cd .build
wget https://github.com/wekan/wekan/releases/download/v8.58/wekan-8.58-amd64.zip
unzip wekan-8.58-amd64.zip
rm wekan-8.58-amd64.zip
wget https://github.com/wekan/wekan/releases/download/v8.78/wekan-8.78-amd64.zip
unzip wekan-8.78-amd64.zip
rm wekan-8.78-amd64.zip
cd ..
# Copy to Snap
wget https://nodejs.org/dist/latest-v22.x/node-v22.22.2-linux-x64.tar.xz
tar -xf node-v22.22.2-linux-x64.tar.xz node-v22.22.2-linux-x64/bin/node
rm node-v22.22.2-linux-x64.tar.xz
wget https://nodejs.org/dist/latest-v22.x/node-v24.14.1-linux-x64.tar.xz
tar -xf node-v24.14.1-linux-x64.tar.xz node-v24.14.1-linux-x64/bin/node
rm node-v24.14.1-linux-x64.tar.xz
mkdir $SNAPCRAFT_PART_INSTALL/bin
cp -p node-v22.22.2-linux-x64/bin/node $SNAPCRAFT_PART_INSTALL/bin/
rm -rf node-v22.22.2-linux-x64
cp -p node-v24.14.1-linux-x64/bin/node $SNAPCRAFT_PART_INSTALL/bin/
rm -rf node-v24.14.1-linux-x64
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
+4
View File
@@ -3,6 +3,10 @@
# If you want to restart even on crash, uncomment while and done lines.
#while true; do
cd .build/bundle
#-------------------- CHANGE STREAMS --------------------
# https://forums.meteor.com/t/meteor-3-5-beta-change-streams-performance-improvements/64461#change-streams-setup-3
# https://github.com/meteor/meteor/blob/release-3.5/v3-docs/docs/performance/change-streams-observer-driver.md#choosing-the-reactivity-driver-order
- METEOR_REACTIVITY_ORDER=changeStreams,oplog,polling
#-------------------- REQUIRED SETTINGS START --------------------
# WRITEABLE PATH REQUIRED TO EXISTS AND BE WRITABLE FOR ATTACHMENTS TO WORK
export WRITABLE_PATH=..