Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b2759f97d | ||
|
|
adf4312e5a | ||
|
|
b1707b8691 | ||
|
|
25dff3f2da | ||
|
|
bc88c0dfc2 | ||
|
|
1ca2dfd1b1 | ||
|
|
bdb9274485 | ||
|
|
14f9ac39ad | ||
|
|
d4b52f7de9 | ||
|
|
bdc65ec6f4 | ||
|
|
7fb31cecd5 | ||
|
|
317c82a9f0 | ||
|
|
9b5c89d235 | ||
|
|
c23e190e8e | ||
|
|
4a73371499 | ||
|
|
6f8af55489 | ||
|
|
2a05f62bf9 | ||
|
|
d6eb1eb76f | ||
|
|
27542f3a51 | ||
|
|
0328e70d5f | ||
|
|
c46932ee30 | ||
|
|
c7408ad8d1 | ||
|
|
05dc8f8f93 | ||
|
|
aea8c285c2 | ||
|
|
a859137971 | ||
|
|
08ebad0fe4 | ||
|
|
d54ebfa0e1 | ||
|
|
304a007d07 | ||
|
|
b6b58c9d95 | ||
|
|
7f0c42cca5 | ||
|
|
9508d46ebd | ||
|
|
bcbf4771ea | ||
|
|
9da5ec25c6 | ||
|
|
028b404377 | ||
|
|
8da0f34ee6 | ||
|
|
cac2628b19 | ||
|
|
9c69530256 | ||
|
|
6e108358d7 | ||
|
|
87f592c67f | ||
|
|
fc95abd587 | ||
|
|
ce3e51b147 | ||
|
|
bb48b23266 | ||
|
|
fdf00e06f5 | ||
|
|
950b246da8 | ||
|
|
6ac0df8448 | ||
|
|
80ae3a18a8 | ||
|
|
972bfb9677 | ||
|
|
b1149e4d6e | ||
|
|
3cd2ba847b | ||
|
|
8bf869654c | ||
|
|
3b98f862bc | ||
|
|
9bc889e274 | ||
|
|
5df924f416 | ||
|
|
5ef94b7079 | ||
|
|
dfcaaf0ae7 | ||
|
|
34e7ff532f | ||
|
|
fbdfcead50 | ||
|
|
b8bbe4431b | ||
|
|
9e31666280 | ||
|
|
5846d9c9b0 | ||
|
|
7707a5b7fc | ||
|
|
18c495940e | ||
|
|
d3155d24c6 | ||
|
|
c0661506c4 | ||
|
|
fa2b34764f | ||
|
|
7a7909b49f | ||
|
|
17d3b3088f | ||
|
|
6a78a178e2 | ||
|
|
749de39e2a | ||
|
|
cd55c0e12e | ||
|
|
d04037739e | ||
|
|
f3a97b9938 | ||
|
|
104fbef37a | ||
|
|
98b3dc71af | ||
|
|
7b28e4d589 | ||
|
|
57291dbe50 | ||
|
|
9f9eefce27 | ||
|
|
af3361b35b | ||
|
|
24d65e1e72 | ||
|
|
29922fca6a | ||
|
|
81319fc48b | ||
|
|
28b7209407 | ||
|
|
0b59f87887 | ||
|
|
b22df9e93d | ||
|
|
1fa72fd34a | ||
|
|
a2f38e7494 | ||
|
|
30a1792c92 | ||
|
|
3d70f24a78 | ||
|
|
b3d14fb5e8 | ||
|
|
59956ca720 | ||
|
|
aaf7690b9a | ||
|
|
9f549dfcd2 | ||
|
|
f191392e3a | ||
|
|
56f26418ea | ||
|
|
118e0e5d5c | ||
|
|
264dcca42b | ||
|
|
73d69f4631 | ||
|
|
485b773338 | ||
|
|
8443e3511d | ||
|
|
b2a349aec5 | ||
|
|
faf8453c6d | ||
|
|
7484c8cce3 | ||
|
|
2b02e7712e | ||
|
|
8a3a7b19aa | ||
|
|
9fa6574c80 | ||
|
|
2bad17010c | ||
|
|
4168385607 | ||
|
|
c0aa1ef375 | ||
|
|
811b79b641 | ||
|
|
dab9755dd4 | ||
|
|
6863479c09 | ||
|
|
e05eebb8b0 | ||
|
|
28ef0e6e89 | ||
|
|
09e65ccac2 | ||
|
|
03e0cfdc15 | ||
|
|
7634cd6c2e | ||
|
|
4594711be4 | ||
|
|
c267933bf7 | ||
|
|
edc6b18b5f | ||
|
|
60657c6e94 | ||
|
|
3c189f9c7f | ||
|
|
bf02c9a453 | ||
|
|
e6b6cd3036 | ||
|
|
6e8589b1f9 | ||
|
|
1354f84a70 | ||
|
|
d07cf92f7c | ||
|
|
e079644ed6 | ||
|
|
6b393178dc | ||
|
|
26e1c9ab69 | ||
|
|
145a17fee5 | ||
|
|
ccc9301ed4 | ||
|
|
f2e46d660f | ||
|
|
8b4676175c | ||
|
|
e9a547c89b | ||
|
|
d07fcceb6a | ||
|
|
6b5002f314 | ||
|
|
5418ffd558 | ||
|
|
ea98afeb68 | ||
|
|
be7d7e26fb | ||
|
|
56377aaf4c | ||
|
|
43601c5e3e | ||
|
|
1db7f3a57d | ||
|
|
e58e99c285 | ||
|
|
b38e25c9f1 | ||
|
|
ce52b8a0ab | ||
|
|
f7800846a5 | ||
|
|
02b75824e5 | ||
|
|
cf52c2f6e4 | ||
|
|
6bc68ba57b | ||
|
|
5ec9cfa537 | ||
|
|
3457cab0b1 | ||
|
|
8349fb6b63 | ||
|
|
33267fa2e4 | ||
|
|
5c4130311a | ||
|
|
ed718f083a | ||
|
|
2aa8ecfc39 | ||
|
|
cdf1d3305b | ||
|
|
fd58bb9e4d | ||
|
|
55ec0b5ff5 | ||
|
|
e5c551f009 | ||
|
|
7666c6c0e1 | ||
|
|
e6ccbfd3b8 | ||
|
|
d8a2e06a95 | ||
|
|
c38fa1c272 | ||
|
|
1a5e23617d | ||
|
|
87916c20bf | ||
|
|
12c223d226 | ||
|
|
9ae2842fa4 | ||
|
|
79853a2fb7 | ||
|
|
ec1e383558 | ||
|
|
df067f01e8 | ||
|
|
4b8cc0683e | ||
|
|
f43b76c323 | ||
|
|
e90b2e384c | ||
|
|
6125822cf7 | ||
|
|
4223c2a79f | ||
|
|
7059cc244a | ||
|
|
5fc608e310 | ||
|
|
29550cbd93 | ||
|
|
35674a205b | ||
|
|
a702596bee | ||
|
|
02fef72e7e | ||
|
|
d5c9e402ce | ||
|
|
b788d398b9 | ||
|
|
5c3b73e3b0 | ||
|
|
82f1de078f | ||
|
|
ddc353023c | ||
|
|
ef75b9ecac | ||
|
|
cf2b3e4fc9 | ||
|
|
cf21991a3f | ||
|
|
9e4f72e2a4 | ||
|
|
f7da243c9e |
No files matched your search
@@ -251,7 +251,7 @@ gosu wekan:wekan /home/wekan/.meteor/meteor -- help
|
||||
# Build app (Development)
|
||||
cd /home/wekan/app
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor add standard-minifier-js
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor npm install
|
||||
gosu wekan:wekan /home/wekan/.meteor/meteor npm install --no-audit
|
||||
|
||||
# Put back the original tar
|
||||
mv $(which tar)~ $(which tar)
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
+164
-177
@@ -1,11 +1,12 @@
|
||||
name: Release All Platforms
|
||||
|
||||
# Triggered when maintainer pushes a version tag, e.g.: git tag v8.43 && git push --tags
|
||||
# Required GitHub Secrets:
|
||||
# DOCKERHUB_USERNAME - Docker Hub username
|
||||
# DOCKERHUB_TOKEN - Docker Hub access token
|
||||
# Does not work:
|
||||
# Required GitHub Secrets:
|
||||
# QUAY_USERNAME - Quay.io username
|
||||
# QUAY_PASSWORD - Quay.io password or token
|
||||
# DOCKERHUB_USERNAME - Docker Hub username
|
||||
# DOCKERHUB_TOKEN - Docker Hub access token
|
||||
# SNAP_STORE_LOGIN - Snapcraft store credentials: run 'snapcraft export-login --snaps wekan --channels stable -' and base64-encode the output
|
||||
|
||||
on:
|
||||
@@ -25,7 +26,7 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
changelog: ${{ steps.changelog.outputs.content }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
@@ -59,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
|
||||
@@ -72,17 +73,17 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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') }}
|
||||
@@ -92,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@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.22.2'
|
||||
node-version: '24.14.1'
|
||||
|
||||
- name: Install Meteor
|
||||
run: |
|
||||
@@ -107,24 +108,28 @@ jobs:
|
||||
|
||||
- name: Build WeKan bundle
|
||||
run: |
|
||||
rm -rf .build node_modules .meteor/local
|
||||
npm install
|
||||
meteor build .build --directory --platforms=web.browser
|
||||
rm -rf .build/bundle/programs/web.browser.legacy
|
||||
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.
|
||||
@@ -140,12 +145,12 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential g++ make python3 curl unzip zip
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- 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,30 +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: |
|
||||
cd bundle
|
||||
find . -type d -name '*-garbage*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '*phantom*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '.*.swp' -delete 2>/dev/null || true
|
||||
find . -name '*.swp' -delete 2>/dev/null || true
|
||||
cd ..
|
||||
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
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- name: Set up QEMU for s390x
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: s390x
|
||||
|
||||
@@ -193,40 +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: |
|
||||
cd bundle
|
||||
find . -type d -name '*-garbage*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '*phantom*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '.*.swp' -delete 2>/dev/null || true
|
||||
find . -name '*.swp' -delete 2>/dev/null || true
|
||||
cd ..
|
||||
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
|
||||
@@ -235,7 +230,7 @@ jobs:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- name: Set up QEMU for ppc64le
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: ppc64le
|
||||
|
||||
@@ -243,40 +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: |
|
||||
cd bundle
|
||||
find . -type d -name '*-garbage*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '*phantom*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '.*.swp' -delete 2>/dev/null || true
|
||||
find . -name '*.swp' -delete 2>/dev/null || true
|
||||
cd ..
|
||||
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]
|
||||
@@ -286,7 +276,7 @@ jobs:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- name: Set up QEMU for arm/v7
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: arm
|
||||
|
||||
@@ -294,31 +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: |
|
||||
cd bundle
|
||||
find . -type d -name '*-garbage*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '*phantom*' -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '.*.swp' -delete 2>/dev/null || true
|
||||
find . -name '*.swp' -delete 2>/dev/null || true
|
||||
cd ..
|
||||
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
|
||||
@@ -332,20 +317,20 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- 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 node-gyp @mapbox/node-pre-gyp
|
||||
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
|
||||
|
||||
@@ -354,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
|
||||
@@ -375,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
|
||||
@@ -394,101 +380,102 @@ jobs:
|
||||
wekan-${{ needs.prepare.outputs.version }}-win64.zip
|
||||
|
||||
# ─── 7. Build and push multi-platform Docker image ──────────────────────────
|
||||
docker:
|
||||
needs: [prepare, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Quay.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_PASSWORD }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image to all registries
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x
|
||||
push: true
|
||||
build-args: VERSION=${{ needs.prepare.outputs.version }}
|
||||
tags: |
|
||||
wekanteam/wekan:v${{ needs.prepare.outputs.version }}
|
||||
wekanteam/wekan:latest
|
||||
quay.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
|
||||
quay.io/wekan/wekan:latest
|
||||
ghcr.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
|
||||
ghcr.io/wekan/wekan:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
#docker:
|
||||
# needs: [prepare, release]
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# VERSION: ${{ needs.prepare.outputs.version }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v6
|
||||
#
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v4
|
||||
#
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v3
|
||||
#
|
||||
# - name: Log in to Docker Hub
|
||||
# uses: docker/login-action@v4
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
#
|
||||
# - name: Log in to Quay.io
|
||||
# uses: docker/login-action@v4
|
||||
# with:
|
||||
# registry: quay.io
|
||||
# username: ${{ secrets.QUAY_USERNAME }}
|
||||
# password: ${{ secrets.QUAY_PASSWORD }}
|
||||
#
|
||||
# - name: Log in to GitHub Container Registry
|
||||
# uses: docker/login-action@v4
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
#
|
||||
# - name: Build and push Docker image to all registries
|
||||
# uses: docker/build-push-action@v7
|
||||
# with:
|
||||
# context: .
|
||||
# platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x
|
||||
# push: true
|
||||
# build-args: VERSION=${{ needs.prepare.outputs.version }}
|
||||
# tags: |
|
||||
# #wekanteam/wekan:v${{ needs.prepare.outputs.version }}
|
||||
# #wekanteam/wekan:latest
|
||||
# quay.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
|
||||
# quay.io/wekan/wekan:latest
|
||||
# ghcr.io/wekan/wekan:v${{ needs.prepare.outputs.version }}
|
||||
# ghcr.io/wekan/wekan:latest
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
|
||||
# ─── 8. Build and publish snap packages (all architectures via Launchpad) ────
|
||||
# Uses snapcraft remote-build on Launchpad's native builders (more reliable
|
||||
# than QEMU for snap builds). Each architecture bundles its own database:
|
||||
# amd64, arm64 — MongoDB Community 8.0 (OpLog via --replSet rs0)
|
||||
# armhf, s390x, ppc64el — FerretDB 2 + PostgreSQL 16 (OpLog via replica set)
|
||||
snap:
|
||||
needs: [prepare, release]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install snapcraft
|
||||
run: sudo snap install snapcraft --classic
|
||||
|
||||
- name: Remote-build snap for all architectures via Launchpad
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
run: |
|
||||
snapcraft remote-build \
|
||||
--build-for=amd64,arm64,armhf,s390x,ppc64el \
|
||||
--launchpad-accept-public-upload
|
||||
|
||||
- name: Upload all snap files as artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: snaps-all-arches
|
||||
path: "*.snap"
|
||||
retention-days: 30
|
||||
|
||||
# Publish to candidate, beta, and edge — NOT stable.
|
||||
# Stable channel holds the old WeKan version until database migrations are ready.
|
||||
# Promote to stable manually via the Snap Store dashboard when migrations exist.
|
||||
- name: Publish all snaps to Snap Store (candidate, beta, edge)
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
run: |
|
||||
for snap_file in *.snap; do
|
||||
echo "Publishing $snap_file ..."
|
||||
snapcraft upload --release=candidate,beta,edge "$snap_file"
|
||||
done
|
||||
|
||||
- name: Upload all snaps to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: "*.snap"
|
||||
# amd64, arm64 — MongoDB 7.x prebuilt native binary
|
||||
# armhf, s390x, ppc64el — MongoDB 7.x amd64 binary run via qemu-x86_64-static
|
||||
# (no prebuilt MongoDB binaries exist for these arches)
|
||||
#snap:
|
||||
# needs: [prepare, release]
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# VERSION: ${{ needs.prepare.outputs.version }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v6
|
||||
#
|
||||
# - name: Install snapcraft
|
||||
# run: sudo snap install snapcraft --classic
|
||||
#
|
||||
# - name: Remote-build snap for all architectures via Launchpad
|
||||
# env:
|
||||
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
# run: |
|
||||
# snapcraft remote-build \
|
||||
# --build-for=amd64,arm64,armhf,s390x,ppc64el \
|
||||
# --launchpad-accept-public-upload
|
||||
#
|
||||
# - name: Upload all snap files as artifacts
|
||||
# uses: actions/upload-artifact@v7
|
||||
# with:
|
||||
# name: snaps-all-arches
|
||||
# path: "*.snap"
|
||||
# retention-days: 30
|
||||
#
|
||||
# # Publish to edge — NOT stable.
|
||||
# # Stable channel holds the old WeKan version until database migrations are ready.
|
||||
# # Promote to stable manually via the Snap Store dashboard when migrations exist.
|
||||
# - name: Publish all snaps to Snap Store (candidate, beta, edge)
|
||||
# env:
|
||||
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
# run: |
|
||||
# for snap_file in *.snap; do
|
||||
# echo "Publishing $snap_file ..."
|
||||
# snapcraft upload --release=edge "$snap_file"
|
||||
# done
|
||||
#
|
||||
# - name: Upload all snaps to GitHub Release
|
||||
# uses: softprops/action-gh-release@v2
|
||||
# with:
|
||||
# files: "*.snap"
|
||||
@@ -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
|
||||
run: meteor npm install
|
||||
|
||||
- name: Run Tests
|
||||
run: sh ./test-wekan.sh -cv
|
||||
|
||||
+20
-20
@@ -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
@@ -1 +1 @@
|
||||
METEOR@3.4
|
||||
METEOR@3.5-beta.7
|
||||
+12
-12
@@ -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
|
||||
|
||||
+440
-10
@@ -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,23 +12,452 @@ 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.
|
||||
|
||||
Upgrading to Meteor 3 progress:
|
||||
and fixes the following bugs:
|
||||
|
||||
- https://harryadel.com/dev-diary-24/
|
||||
- https://harryadel.com/dev-diary-25/
|
||||
- [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
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Updated build docs](https://github.com/wekan/wekan/commit/b3d14fb5e87545635006958686f0340028711820).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Try to fix .zip Bundle, Docker and Snap](https://github.com/wekan/wekan/commit/59956ca720d2103baaa9ecf2befff2ec76430feb).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.57 2026-04-05 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Try to fix Snap](https://github.com/wekan/wekan/commit/9f549dfcd2efa241a2ad3bdf5b0d2d942064d4e3).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.56 2026-04-05 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Try to fix Snap](https://github.com/wekan/wekan/commit/56f26418ea9d1321fc5109c4b39104ff98d65429).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.55 2026-04-05 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- Docs: IMPORTANT: Do NOT `npm audit fix --force`, it breaks builds.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/2bad17010cd445c004a228b732b17c68e18eb316),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/9fa6574c80589218569dc3c49782a1a8036aa542).
|
||||
Thanks to xet7.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/73d69f4631216dfe504d9f05230adadc2850c4f3).
|
||||
Thanks to developers of dependencies.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Cards Reports is not working](https://github.com/wekan/wekan/commit/2b02e7712ee6a8ae8af5d416ce2d288a25386fdf).
|
||||
Thanks to titver968 and xet7.
|
||||
- [Fix Search All Boards](https://github.com/wekan/wekan/commit/faf8453c6d10613edf32a734e549961860fcdd32).
|
||||
Thanks to YosserDerbali and xet7.
|
||||
- [Try to fix Snap](https://github.com/wekan/wekan/commit/8443e3511d61d8c1bb419ed5bca2e28483c5d133).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.54 2026-04-04 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Revert v8.53 Refactor upsert calls to use async method](https://github.com/wekan/wekan/commit/811b79b641c765461f3d696ed00b5459a4747867).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.53 2026-04-04 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Refactor upsert calls to use async method](https://github.com/wekan/wekan/pull/6243).
|
||||
Thanks to snowsky.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.52 2026-04-03 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Try to fix Docker and Snap](https://github.com/wekan/wekan/commit/03e0cfdc15c56ac0219f5e435034a1c0ad5006ae).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.51 2026-04-03 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Updated release scripts](https://github.com/wekan/wekan/commit/4594711be41d1e9cf15b1f8bb070fcb36bf736ca).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Bug: WeKan v8.50 WRITABLE_PATH](https://github.com/wekan/wekan/commit/edc6b18b5f77320e966e56c888575bcb764096dc).
|
||||
Thanks to snowsky and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.50 2026-04-03 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Uncommented build steps. Not yet tested are required dependencies included](https://github.com/wekan/wekan/commit/145a17fee5dc9430de7a60100657566a4a69bc7b).
|
||||
Thanks to xet7.
|
||||
- [Updated npm package @wekanteam/exceljs](https://github.com/wekan/wekan/commit/6e8589b1f96b5f0c2ddec0831b238096ce610b1e).
|
||||
Thanks to xet7.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/bf02c9a453e27edba1c08830eb4783b9dfc7bbc2).
|
||||
Thanks to developers of dependencies.
|
||||
NOTE: Do not `npm audit fix --force`, it downgrades @meteorjs/rspack and breaks builds.
|
||||
`npm audit` shows false info, it does not recognize that elliptic is
|
||||
already updated to 6.6.1 with override at package.json like you can see with
|
||||
`npm list elliptic`.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix indentation for release-all.yml workflow](https://github.com/wekan/wekan/pull/6240).
|
||||
Thanks to snowsky.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.49 2026-03-31 WeKan ® release
|
||||
|
||||
This release fixes the following bugs:
|
||||
|
||||
- [Fix: Missing module @meteorjs/reify/lib/runtime](https://github.com/wekan/wekan/pull/6237).
|
||||
Thanks to mzch and kovacs-andras.
|
||||
- [Fix docker-compose.yml](https://github.com/wekan/wekan/commit/d07fcceb6a884cf1ff0c9a2d36691af0304f36e7).
|
||||
Thanks to xet7.
|
||||
- [Try to fix Snap](https://github.com/wekan/wekan/commit/8b4676175caa4af36bf11567676f3c3f522f9512).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.48 2026-03-31 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- Updated release scripts.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/43601c5e3e95d6026f2e558f261c02a9578dcecd),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/56377aaf4c5e92c1a03dc996b6c649ce17553280).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.47 2026-03-31 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [Updated attachments and avatar path to be at WRITABLE_PATH/files/attachments etc](https://github.com/wekan/wekan/commit/e58e99c2852dd81a040dcffc0f1f2798c4b8cf15).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.46 2026-03-31 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- Updated release scripts.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/6bc68ba57bf15339b80011d131a7da65124e071d),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/ce52b8a0ab29805c562a2af0b79e30d970594e17).
|
||||
Thanks to xet7.
|
||||
- [Updated docker-compose.yml to change from FerretDB/PostgreSQL to MongoDB](https://github.com/wekan/wekan/commit/02b75824e5dc24c968959d5a64f846efe33dee4f).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.45 2026-03-31 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- Updated release scripts.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/4223c2a79f4c276ab1c9734a94314f1d1d1ce033),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/6125822cf7244242832d1fe02f99150abb7350d0),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/e90b2e384c168fbb4cee389d8af9b54a5bc57734),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/29550cbd9335fc95e4f4ac399fe754ece8573450),
|
||||
[Part 5](https://github.com/wekan/wekan/commit/8349fb6b63097970d06af73af388b030761967bd),
|
||||
[Part 6](https://github.com/wekan/wekan/commit/f43b76c323c1f09cf1885251792b29229ef77d2b),
|
||||
[Part 7](https://github.com/wekan/wekan/commit/e90b2e384c168fbb4cee389d8af9b54a5bc57734).
|
||||
Thanks to xet7.
|
||||
- Updated GitHub Actions.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/4b8cc0683e9dfdffb7ff7896f622afe8f908a7f5),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/df067f01e894f43305ad36cebdf48a1bf55ff33a),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/ec1e3835586bdc09d93d5049794e8657b460acef),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/79853a2fb7efb738f2f10b6c2e9fef69def88c48),
|
||||
[Part 5](https://github.com/wekan/wekan/commit/9ae2842fa4bedbd4c132943703ef462cc353ab1a),
|
||||
[Part 6](https://github.com/wekan/wekan/commit/1a5e23617de98e6b65f78dfb25c233440f818d11),
|
||||
[Part 7](https://github.com/wekan/wekan/commit/c38fa1c272c32323d8a164dfcf34b898127e6b1d),
|
||||
[Part 8](https://github.com/wekan/wekan/commit/d8a2e06a95713e33e5676df38951dfd3f991ecb3),
|
||||
[Part 9](https://github.com/wekan/wekan/commit/e6ccbfd3b88299c52fbaad9f85fc3b438e3cdc89),
|
||||
[Part 10](https://github.com/wekan/wekan/commit/7666c6c0e1660b5aba300f4e465877bcf73d8066).
|
||||
Thanks to dependabot.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- Try to fix Snap.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/e5c551f0092ccb71fafa3433bfd99911f9a2e9c0),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/55ec0b5ff5594c10ebbc8cba74339de2ad0557a6),
|
||||
[Part 3](https://github.com/wekan/wekan/commit/fd58bb9e4dd0f825237a50043a171fc223d44101),
|
||||
[Part 4](https://github.com/wekan/wekan/commit/cdf1d3305bd738fe5357a5f3a9df2f7e6858d1d0),
|
||||
[Part 5](https://github.com/wekan/wekan/commit/2aa8ecfc39a6f971134784817b46ba9867bb402d),
|
||||
[Part 6](https://github.com/wekan/wekan/commit/ed718f083aa0c9f82e368a5badfe73393753a617),
|
||||
[Part 7](https://github.com/wekan/wekan/commit/5c4130311a063340bd89dff571fbf685cda7a3ba),
|
||||
[Part 8](https://github.com/wekan/wekan/commit/33267fa2e462fcc6cd2243ae1b5b39d59b03988f),
|
||||
[Part 9](https://github.com/wekan/wekan/commit/87916c20bf579e16e21fa3bd9ac7687019b277eb),
|
||||
[Part 10](https://github.com/wekan/wekan/commit/12c223d2260c9bd9547ae35a394869ef90e8bc83).
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.44 2026-03-30 WeKan ® release
|
||||
|
||||
This release adds the following updates:
|
||||
|
||||
- [From WeKan v8.43 FerretDB/PostgreSQL back to MongoDB 7.x](https://github.com/wekan/wekan/commit/cf2b3e4fc95ce4619c1ba13b63c3ad9d90bf59f6).
|
||||
Thanks to xet7.
|
||||
- [Updated dependencies](https://github.com/wekan/wekan/commit/82f1de078f16e87028ffd101e3f80550a4b18afa).
|
||||
Thanks to developers of dependencies.
|
||||
- Updated release scripts.
|
||||
[Part 1](https://github.com/wekan/wekan/commit/a702596bee6aa812cb69a6673ad34f9b2875c729),
|
||||
[Part 2](https://github.com/wekan/wekan/commit/29550cbd9335fc95e4f4ac399fe754ece8573450).
|
||||
Thanks to xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Always show all data without migrations, like Shared Lists that were before Per-Swimlanes lists, Orphaned Swimlanes/Lists/Cards etc](https://github.com/wekan/wekan/commit/ef75b9ecac8210ff000439aca92765b53a1551a4).
|
||||
Thanks to xet7.
|
||||
- [Fix globalSearch](https://github.com/wekan/wekan/commit/b788d398b9b2cb0caad9c28f79acdceb5b577f25).
|
||||
Thanks to snowsky.
|
||||
- [Fix myCards crash](https://github.com/wekan/wekan/commit/d5c9e402ce4ebe0318bc29d5b83784771cbe8ac3).
|
||||
Thanks to cristomoya.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v8.43 2026-03-27 WeKan ® release
|
||||
|
||||
|
||||
+5
-5
@@ -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.43
|
||||
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="" \
|
||||
@@ -204,6 +203,7 @@ cd /home/wekan/app
|
||||
wget "https://github.com/wekan/wekan/releases/download/v${VERSION}/wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
unzip "wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
rm "wekan-${VERSION}-${WEKAN_ARCH}.zip"
|
||||
npm install --production --no-audit --prefix ./bundle/programs/server
|
||||
mv /home/wekan/app/bundle /build
|
||||
|
||||
# Restore original tar
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM amd64/alpine:latest AS builder
|
||||
|
||||
# Set the environment variables for builder
|
||||
ENV QEMU_VERSION=v7.2.0-1 \
|
||||
QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
WEKAN_VERSION=latest \
|
||||
WEKAN_ARCHITECTURE=arm64
|
||||
|
||||
# Install dependencies
|
||||
#RUN apk update && apk add ca-certificates outils-sha1 && \
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN apt update && apt install ca-certificates wget unzip -y && \
|
||||
\
|
||||
# Download qemu static for our architecture
|
||||
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
|
||||
\
|
||||
# Download wekan and shasum
|
||||
wget https://releases.wekan.team/raspi3/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
wget https://releases.wekan.team/raspi3/SHA256SUMS.txt && \
|
||||
# Verify wekan
|
||||
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
|
||||
\
|
||||
# Unzip wekan
|
||||
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
\
|
||||
# Download node and shasums
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
\
|
||||
# Verify nodejs authenticity
|
||||
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
|
||||
\
|
||||
# Extract node and remove tar.gz
|
||||
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
|
||||
|
||||
# Build wekan dockerfile
|
||||
FROM --platform=linux/arm64 arm64v8/ubuntu:23.04
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Set the environment variables (defaults where required)
|
||||
ENV QEMU_ARCHITECTURE=aarch64 \
|
||||
NODE_ARCHITECTURE=linux-arm64 \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
NODE_ENV=production \
|
||||
NPM_VERSION=latest \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
# Copy the app to the image
|
||||
COPY --from=builder bundle /home/wekan/bundle
|
||||
|
||||
# Copy
|
||||
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
|
||||
|
||||
RUN \
|
||||
set -o xtrace && \
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan && \
|
||||
\
|
||||
# Install Node
|
||||
ln -s /opt/nodejs/bin/node /usr/bin/node && \
|
||||
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
|
||||
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
|
||||
chown wekan --recursive /home/wekan/.config
|
||||
|
||||
# \
|
||||
# # Install Node dependencies
|
||||
# #npm install -g npm@${NPM_VERSION} && \
|
||||
# \
|
||||
# # Install Health Check dependencies
|
||||
# #apk add curl
|
||||
#
|
||||
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
|
||||
# CMD curl --fail "http://localhost:$PORT" || exit 1
|
||||
|
||||
EXPOSE $PORT
|
||||
USER wekan
|
||||
|
||||
# CMD ["bash", "-c", "ulimit -s 65500; exec node --stack-size=65500 --max-old-space-size=8192 /home/wekan/bundle/main.js"]
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM --platform=linux/amd64 amd64/ubuntu:23.04 AS builder
|
||||
#FROM --platform=linux/amd64 ghcr.io/wekan/wekan:main AS builder
|
||||
#FROM arm64v8/ubuntu:23.04 AS builder
|
||||
#FROM amd64/alpine:latest AS builder
|
||||
|
||||
# Set the environment variables for builder
|
||||
ENV QEMU_VERSION=v7.2.0-1 \
|
||||
QEMU_ARCHITECTURE=s390x \
|
||||
NODE_ARCHITECTURE=linux-s390x \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
WEKAN_VERSION=latest \
|
||||
WEKAN_ARCHITECTURE=s390x
|
||||
|
||||
# Install dependencies
|
||||
#RUN apk update && apk add ca-certificates outils-sha1 && \
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN apt update && apt install ca-certificates wget unzip -y && \
|
||||
\
|
||||
# Download qemu static for our architecture
|
||||
wget https://github.com/multiarch/qemu-user-static/releases/download/${QEMU_VERSION}/qemu-${QEMU_ARCHITECTURE}-static.tar.gz -O - | tar -xz && \
|
||||
\
|
||||
# Download wekan and shasum
|
||||
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
wget https://releases.wekan.team/${WEKAN_ARCHITECTURE}/SHA256SUMS.txt && \
|
||||
# Verify wekan
|
||||
grep wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip SHA256SUMS.txt | sha256sum -c - && \
|
||||
\
|
||||
# Unzip wekan
|
||||
unzip wekan-${WEKAN_VERSION}-${WEKAN_ARCHITECTURE}.zip && \
|
||||
\
|
||||
# Download node and shasums
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
wget https://github.com/wekan/node-v14-esm/releases/download/${NODE_VERSION}/SHASUMS256.txt && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz && \
|
||||
#wget https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt.asc && \
|
||||
\
|
||||
# Verify nodejs authenticity
|
||||
grep node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz SHASUMS256.txt | sha256sum -c - && \
|
||||
\
|
||||
# Extract node and remove tar.gz
|
||||
tar xvzf node-${NODE_VERSION}-${NODE_ARCHITECTURE}.tar.gz
|
||||
|
||||
# Build wekan dockerfile
|
||||
FROM --platform=linux/s390x s390x/ubuntu:23.04
|
||||
LABEL maintainer="wekan"
|
||||
|
||||
# Set the environment variables (defaults where required)
|
||||
ENV QEMU_ARCHITECTURE=s390x \
|
||||
NODE_ARCHITECTURE=linux-s390x \
|
||||
NODE_VERSION=v14.21.4 \
|
||||
NODE_ENV=production \
|
||||
NPM_VERSION=latest \
|
||||
WITH_API=true \
|
||||
PORT=8080 \
|
||||
ROOT_URL=http://localhost \
|
||||
MONGO_URL=mongodb://127.0.0.1:27017/wekan \
|
||||
LDAP_AUTHENTIFICATION_PASSWORD_FILE="" \
|
||||
OAUTH2_SECRET_FILE="" \
|
||||
MAIL_SERVICE_PASSWORD_FILE="" \
|
||||
MONGO_PASSWORD_FILE="" \
|
||||
S3_SECRET_FILE=""
|
||||
|
||||
# Copy qemu-static to image
|
||||
COPY --from=builder qemu-${QEMU_ARCHITECTURE}-static /usr/bin
|
||||
|
||||
# Copy the app to the image
|
||||
COPY --from=builder bundle /home/wekan/bundle
|
||||
|
||||
# Copy
|
||||
COPY --from=builder node-${NODE_VERSION}-${NODE_ARCHITECTURE} /opt/nodejs
|
||||
|
||||
RUN \
|
||||
set -o xtrace && \
|
||||
# Add non-root user wekan
|
||||
useradd --user-group --system --home-dir /home/wekan wekan && \
|
||||
\
|
||||
# Install Node
|
||||
ln -s /opt/nodejs/bin/node /usr/bin/node && \
|
||||
ln -s /opt/nodejs/bin/npm /usr/bin/npm && \
|
||||
mkdir -p /opt/nodejs/lib/node_modules/fibers/.node-gyp /root/.node-gyp/8.16.1 /home/wekan/.config && \
|
||||
chown wekan --recursive /home/wekan/.config
|
||||
|
||||
# \
|
||||
# # Install Node dependencies
|
||||
# #npm install -g npm@${NPM_VERSION} && \
|
||||
# \
|
||||
# # Install Health Check dependencies
|
||||
# #apk add curl
|
||||
#
|
||||
#HEALTHCHECK --start-period=30s --interval=30s --timeout=10s --retries=3 \
|
||||
# CMD curl --fail "http://localhost:$PORT" || exit 1
|
||||
|
||||
EXPOSE $PORT
|
||||
USER wekan
|
||||
|
||||
CMD ["bash", "-c", "ulimit -s 65500; exec node /home/wekan/bundle/main.js"]
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v8.43.0"
|
||||
appVersion: "v8.78.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 || [];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,52 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
Template.resultCard.helpers({
|
||||
userId() {
|
||||
return Meteor.userId();
|
||||
},
|
||||
getBoard() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.boardId) {
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
return board || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getSwimlane() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.swimlaneId) {
|
||||
const swimlane = ReactiveCache.getSwimlane(card.swimlaneId);
|
||||
return swimlane || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getList() {
|
||||
const card = Template.currentData();
|
||||
try {
|
||||
if (card && card.listId) {
|
||||
const list = ReactiveCache.getList(card.listId);
|
||||
return list || null;
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently handle errors
|
||||
}
|
||||
return null;
|
||||
},
|
||||
originRelativeUrl() {
|
||||
const card = Template.currentData();
|
||||
if (card && card.boardId && card._id) {
|
||||
return `/board/${card.boardId}?cardId=${card._id}`;
|
||||
}
|
||||
return '#';
|
||||
},
|
||||
});
|
||||
|
||||
Template.resultCard.events({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -309,7 +309,15 @@ Template.listBody.helpers({
|
||||
listId: Template.currentData()._id,
|
||||
archived: false,
|
||||
};
|
||||
if (swimlaneId) selector.swimlaneId = swimlaneId;
|
||||
if (swimlaneId) {
|
||||
// Fallback: also show cards with no swimlaneId (null/empty) so that
|
||||
// pre-migration / orphaned cards are always visible without a DB migration.
|
||||
selector.$or = [
|
||||
{ swimlaneId },
|
||||
{ swimlaneId: null }, // null covers null AND missing field
|
||||
{ swimlaneId: '' }, // empty string from shared-lists era
|
||||
];
|
||||
}
|
||||
const ret = ReactiveCache.getCards(Filter.mongoSelector(selector), {
|
||||
// sort: ['sort'],
|
||||
sort: sortBy,
|
||||
|
||||
@@ -84,6 +84,21 @@
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.js-new-search {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.board-color-belize.js-new-search {
|
||||
background-color: #216694 !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
}
|
||||
.board-color-belize.js-new-search:hover {
|
||||
background-color: #1a4d6a !important;
|
||||
}
|
||||
.board-color-belize.js-new-search:active {
|
||||
background-color: #0d334d !important;
|
||||
}
|
||||
.global-search-operator {
|
||||
font-family: Courier;
|
||||
}
|
||||
|
||||
@@ -11,68 +11,47 @@ template(name="globalSearchModalTitle")
|
||||
| {{_ 'globalSearch-title'}}
|
||||
|
||||
template(name="resultsPaged")
|
||||
if resultsHeading.get
|
||||
h1
|
||||
= resultsHeading.get
|
||||
a(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
|
||||
| 🔗
|
||||
each card in results.get
|
||||
if resultsHeading
|
||||
h1= resultsHeading
|
||||
each card in results
|
||||
+resultCard(card)
|
||||
table.global-search-footer
|
||||
tr
|
||||
td.global-search-previous-page
|
||||
if hasPreviousPage.get
|
||||
if hasPreviousPage
|
||||
button.js-previous-page
|
||||
| {{_ 'previous-page' }}
|
||||
td.global-search-next-page(align="right")
|
||||
if hasNextPage.get
|
||||
if hasNextPage
|
||||
button.js-next-page
|
||||
| {{_ 'next-page' }}
|
||||
|
||||
template(name="globalSearch")
|
||||
if currentUser
|
||||
.wrapper
|
||||
form.global-search-page.js-search-query-form
|
||||
.wrapper(class=boardColorClass)
|
||||
form.global-search-page.js-search-query-form(onsubmit="return false;")
|
||||
input.global-search-query-input(
|
||||
style="{# if hasResults.get #}display: inline-block;{#/if#}"
|
||||
style="display: inline-block;"
|
||||
id="global-search-input"
|
||||
type="text"
|
||||
name="searchQuery"
|
||||
placeholder="{{_ 'search-example'}}"
|
||||
value="{{ query.get }}"
|
||||
value="{{ queryValue }}"
|
||||
autofocus dir="auto"
|
||||
)
|
||||
a.js-new-search
|
||||
button.js-new-search.board-color-belize(type="button" style="background: #216694 !important; background-color: #216694 !important; color: #fff !important; border: none !important;")
|
||||
| 🧹
|
||||
if debug.get.show
|
||||
h1 Debug
|
||||
if debug.get.showSelector
|
||||
h2 Selector
|
||||
button.js-copy-debug-selector
|
||||
= 'Copy'
|
||||
pre(
|
||||
id="debug-selector"
|
||||
)
|
||||
= sessionData.selector
|
||||
if debug.get.showProjection
|
||||
h2 Projection
|
||||
button.js-copy-debug-projection
|
||||
= 'Copy'
|
||||
pre(
|
||||
id="debug-projection"
|
||||
)
|
||||
= sessionData.projection
|
||||
if searching.get
|
||||
if searching
|
||||
+spinner
|
||||
else if hasResults.get
|
||||
else if hasResults
|
||||
.global-search-results-list-wrapper
|
||||
if hasQueryErrors.get
|
||||
if hasQueryErrors
|
||||
ul
|
||||
each msg in errorMessages
|
||||
li.global-search-error-messages
|
||||
= msg
|
||||
+resultsPaged(this)
|
||||
else if serverError.get
|
||||
+resultsPaged getSearchData
|
||||
else if serverError
|
||||
.global-search-page
|
||||
.global-search-help
|
||||
h1 {{_ 'server-error' }}
|
||||
@@ -83,22 +62,22 @@ template(name="globalSearch")
|
||||
.global-search-help
|
||||
h2 {{_ 'boards' }}
|
||||
.lists-wrapper
|
||||
each title in myBoardNames.get
|
||||
each title in myBoardNames
|
||||
span.card-label.list-title.js-board-title
|
||||
= title
|
||||
h2 {{_ 'lists' }}
|
||||
.lists-wrapper
|
||||
each title in myLists.get
|
||||
each title in myLists
|
||||
span.card-label.list-title.js-list-title
|
||||
= title
|
||||
h2 {{_ 'label-colors' }}
|
||||
.palette-colors: each label in labelColors
|
||||
span.card-label.palette-color.js-label-color(class="card-label-{{label.color}}")
|
||||
= label.name
|
||||
if myLabelNames.get.length
|
||||
if myLabelNames
|
||||
h2 {{_ 'label-names' }}
|
||||
.lists-wrapper
|
||||
each name in myLabelNames.get
|
||||
each name in myLabelNames
|
||||
span.card-label.list-title.js-label-name
|
||||
= name
|
||||
.global-search-instructions
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TAPi18n } from '/imports/i18n';
|
||||
import { CardSearchPaged } from '../../lib/cardSearch';
|
||||
import { LABEL_COLORS } from '/models/metadata/colors';
|
||||
import { Query, QueryErrors } from '../../../config/query-classes';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
// const subManager = new SubsManager();
|
||||
|
||||
@@ -98,36 +99,45 @@ Template.globalSearch.helpers({
|
||||
return Meteor.userId();
|
||||
},
|
||||
|
||||
// Return ReactiveVars so jade can use .get pattern
|
||||
// Return ReactiveVar values so Blaze can use them
|
||||
searching() {
|
||||
return Template.instance().search.searching;
|
||||
const val = Template.instance().search.searching.get();
|
||||
return val;
|
||||
},
|
||||
hasResults() {
|
||||
return Template.instance().search.hasResults;
|
||||
const val = Template.instance().search.hasResults.get();
|
||||
return val;
|
||||
},
|
||||
hasQueryErrors() {
|
||||
return Template.instance().search.hasQueryErrors;
|
||||
const val = Template.instance().search.hasQueryErrors.get();
|
||||
return val;
|
||||
},
|
||||
serverError() {
|
||||
return Template.instance().search.serverError;
|
||||
const val = Template.instance().search.serverError.get();
|
||||
return val;
|
||||
},
|
||||
query() {
|
||||
return Template.instance().search.query;
|
||||
},
|
||||
debug() {
|
||||
return Template.instance().search.debug;
|
||||
const val = Template.instance().search.debug.get();
|
||||
return val;
|
||||
},
|
||||
resultsHeading() {
|
||||
return Template.instance().search.resultsHeading;
|
||||
const rv = Template.instance().search.resultsHeading;
|
||||
return rv ? rv.get() : '';
|
||||
},
|
||||
results() {
|
||||
return Template.instance().search.results;
|
||||
const rv = Template.instance().search.results;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
hasNextPage() {
|
||||
return Template.instance().search.hasNextPage;
|
||||
const rv = Template.instance().search.hasNextPage;
|
||||
return rv ? rv.get() : false;
|
||||
},
|
||||
hasPreviousPage() {
|
||||
return Template.instance().search.hasPreviousPage;
|
||||
const rv = Template.instance().search.hasPreviousPage;
|
||||
return rv ? rv.get() : false;
|
||||
},
|
||||
sessionData() {
|
||||
return Template.instance().search.sessionData;
|
||||
@@ -137,124 +147,170 @@ Template.globalSearch.helpers({
|
||||
},
|
||||
|
||||
myLists() {
|
||||
return Template.instance().myLists;
|
||||
const rv = Template.instance().myLists;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
myLabelNames() {
|
||||
return Template.instance().myLabelNames;
|
||||
const rv = Template.instance().myLabelNames;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
myBoardNames() {
|
||||
return Template.instance().myBoardNames;
|
||||
const rv = Template.instance().myBoardNames;
|
||||
return rv ? rv.get() : [];
|
||||
},
|
||||
|
||||
errorMessages() {
|
||||
return errorMessages(Template.instance());
|
||||
},
|
||||
|
||||
searchInstructions() {
|
||||
const tags = {
|
||||
operator_board: TAPi18n.__('operator-board'),
|
||||
operator_list: TAPi18n.__('operator-list'),
|
||||
operator_swimlane: TAPi18n.__('operator-swimlane'),
|
||||
operator_comment: TAPi18n.__('operator-comment'),
|
||||
operator_label: TAPi18n.__('operator-label'),
|
||||
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
|
||||
operator_user: TAPi18n.__('operator-user'),
|
||||
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
|
||||
operator_member: TAPi18n.__('operator-member'),
|
||||
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
|
||||
operator_assignee: TAPi18n.__('operator-assignee'),
|
||||
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
|
||||
operator_creator: TAPi18n.__('operator-creator'),
|
||||
operator_due: TAPi18n.__('operator-due'),
|
||||
operator_created: TAPi18n.__('operator-created'),
|
||||
operator_modified: TAPi18n.__('operator-modified'),
|
||||
operator_status: TAPi18n.__('operator-status'),
|
||||
operator_has: TAPi18n.__('operator-has'),
|
||||
operator_sort: TAPi18n.__('operator-sort'),
|
||||
operator_limit: TAPi18n.__('operator-limit'),
|
||||
operator_debug: TAPi18n.__('operator-debug'),
|
||||
operator_org: TAPi18n.__('operator-org'),
|
||||
operator_team: TAPi18n.__('operator-team'),
|
||||
predicate_overdue: TAPi18n.__('predicate-overdue'),
|
||||
predicate_archived: TAPi18n.__('predicate-archived'),
|
||||
predicate_all: TAPi18n.__('predicate-all'),
|
||||
predicate_ended: TAPi18n.__('predicate-ended'),
|
||||
predicate_week: TAPi18n.__('predicate-week'),
|
||||
predicate_month: TAPi18n.__('predicate-month'),
|
||||
predicate_quarter: TAPi18n.__('predicate-quarter'),
|
||||
predicate_year: TAPi18n.__('predicate-year'),
|
||||
predicate_attachment: TAPi18n.__('predicate-attachment'),
|
||||
predicate_description: TAPi18n.__('predicate-description'),
|
||||
predicate_checklist: TAPi18n.__('predicate-checklist'),
|
||||
predicate_public: TAPi18n.__('predicate-public'),
|
||||
predicate_private: TAPi18n.__('predicate-private'),
|
||||
predicate_due: TAPi18n.__('predicate-due'),
|
||||
predicate_created: TAPi18n.__('predicate-created'),
|
||||
predicate_modified: TAPi18n.__('predicate-modified'),
|
||||
predicate_start: TAPi18n.__('predicate-start'),
|
||||
predicate_end: TAPi18n.__('predicate-end'),
|
||||
predicate_assignee: TAPi18n.__('predicate-assignee'),
|
||||
predicate_member: TAPi18n.__('predicate-member'),
|
||||
predicate_selector: TAPi18n.__('predicate-selector'),
|
||||
predicate_projection: TAPi18n.__('predicate-projection'),
|
||||
};
|
||||
getSearchData() {
|
||||
return Template.instance().search;
|
||||
},
|
||||
|
||||
let text = '';
|
||||
[
|
||||
['# ', 'globalSearch-instructions-heading'],
|
||||
['\n', 'globalSearch-instructions-description'],
|
||||
['\n\n', 'globalSearch-instructions-operators'],
|
||||
['\n- ', 'globalSearch-instructions-operator-board'],
|
||||
['\n- ', 'globalSearch-instructions-operator-list'],
|
||||
['\n- ', 'globalSearch-instructions-operator-swimlane'],
|
||||
['\n- ', 'globalSearch-instructions-operator-comment'],
|
||||
['\n- ', 'globalSearch-instructions-operator-label'],
|
||||
['\n- ', 'globalSearch-instructions-operator-hash'],
|
||||
['\n- ', 'globalSearch-instructions-operator-user'],
|
||||
['\n- ', 'globalSearch-instructions-operator-at'],
|
||||
['\n- ', 'globalSearch-instructions-operator-member'],
|
||||
['\n- ', 'globalSearch-instructions-operator-assignee'],
|
||||
['\n- ', 'globalSearch-instructions-operator-creator'],
|
||||
['\n- ', 'globalSearch-instructions-operator-org'],
|
||||
['\n- ', 'globalSearch-instructions-operator-team'],
|
||||
['\n- ', 'globalSearch-instructions-operator-due'],
|
||||
['\n- ', 'globalSearch-instructions-operator-created'],
|
||||
['\n- ', 'globalSearch-instructions-operator-modified'],
|
||||
['\n- ', 'globalSearch-instructions-operator-status'],
|
||||
['\n - ', 'globalSearch-instructions-status-archived'],
|
||||
['\n - ', 'globalSearch-instructions-status-public'],
|
||||
['\n - ', 'globalSearch-instructions-status-private'],
|
||||
['\n - ', 'globalSearch-instructions-status-all'],
|
||||
['\n - ', 'globalSearch-instructions-status-ended'],
|
||||
['\n- ', 'globalSearch-instructions-operator-has'],
|
||||
['\n- ', 'globalSearch-instructions-operator-sort'],
|
||||
['\n- ', 'globalSearch-instructions-operator-limit'],
|
||||
['\n## ', 'heading-notes'],
|
||||
['\n- ', 'globalSearch-instructions-notes-1'],
|
||||
['\n- ', 'globalSearch-instructions-notes-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-4'],
|
||||
['\n- ', 'globalSearch-instructions-notes-5'],
|
||||
].forEach(([prefix, instruction]) => {
|
||||
text += `${prefix}${TAPi18n.__(instruction, tags)}`
|
||||
// Replace *<text>* with `<text>` so markdown shows correctly
|
||||
.replace(/\*\</, '`<')
|
||||
.replace(/\>\*/, '\>\`')
|
||||
});
|
||||
return text;
|
||||
boardColorClass() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const board = boardId ? ReactiveCache.getBoard(boardId) : null;
|
||||
return board ? board.colorClass() : 'board-color-belize';
|
||||
},
|
||||
|
||||
searchInstructions() {
|
||||
try {
|
||||
const tags = {
|
||||
operator_board: TAPi18n.__('operator-board'),
|
||||
operator_list: TAPi18n.__('operator-list'),
|
||||
operator_swimlane: TAPi18n.__('operator-swimlane'),
|
||||
operator_comment: TAPi18n.__('operator-comment'),
|
||||
operator_label: TAPi18n.__('operator-label'),
|
||||
operator_label_abbrev: TAPi18n.__('operator-label-abbrev'),
|
||||
operator_user: TAPi18n.__('operator-user'),
|
||||
operator_user_abbrev: TAPi18n.__('operator-user-abbrev'),
|
||||
operator_member: TAPi18n.__('operator-member'),
|
||||
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
|
||||
operator_assignee: TAPi18n.__('operator-assignee'),
|
||||
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
|
||||
operator_creator: TAPi18n.__('operator-creator'),
|
||||
operator_due: TAPi18n.__('operator-due'),
|
||||
operator_created: TAPi18n.__('operator-created'),
|
||||
operator_modified: TAPi18n.__('operator-modified'),
|
||||
operator_status: TAPi18n.__('operator-status'),
|
||||
operator_has: TAPi18n.__('operator-has'),
|
||||
operator_sort: TAPi18n.__('operator-sort'),
|
||||
operator_limit: TAPi18n.__('operator-limit'),
|
||||
operator_debug: TAPi18n.__('operator-debug'),
|
||||
operator_org: TAPi18n.__('operator-org'),
|
||||
operator_team: TAPi18n.__('operator-team'),
|
||||
predicate_overdue: TAPi18n.__('predicate-overdue'),
|
||||
predicate_archived: TAPi18n.__('predicate-archived'),
|
||||
predicate_all: TAPi18n.__('predicate-all'),
|
||||
predicate_ended: TAPi18n.__('predicate-ended'),
|
||||
predicate_week: TAPi18n.__('predicate-week'),
|
||||
predicate_month: TAPi18n.__('predicate-month'),
|
||||
predicate_quarter: TAPi18n.__('predicate-quarter'),
|
||||
predicate_year: TAPi18n.__('predicate-year'),
|
||||
predicate_attachment: TAPi18n.__('predicate-attachment'),
|
||||
predicate_description: TAPi18n.__('predicate-description'),
|
||||
predicate_checklist: TAPi18n.__('predicate-checklist'),
|
||||
predicate_public: TAPi18n.__('predicate-public'),
|
||||
predicate_private: TAPi18n.__('predicate-private'),
|
||||
predicate_due: TAPi18n.__('predicate-due'),
|
||||
predicate_created: TAPi18n.__('predicate-created'),
|
||||
predicate_modified: TAPi18n.__('predicate-modified'),
|
||||
predicate_start: TAPi18n.__('predicate-start'),
|
||||
predicate_end: TAPi18n.__('predicate-end'),
|
||||
predicate_assignee: TAPi18n.__('predicate-assignee'),
|
||||
predicate_member: TAPi18n.__('predicate-member'),
|
||||
predicate_selector: TAPi18n.__('predicate-selector'),
|
||||
predicate_projection: TAPi18n.__('predicate-projection'),
|
||||
};
|
||||
|
||||
let text = '';
|
||||
[
|
||||
['# ', 'globalSearch-instructions-heading'],
|
||||
['\n', 'globalSearch-instructions-description'],
|
||||
['\n\n', 'globalSearch-instructions-operators'],
|
||||
['\n- ', 'globalSearch-instructions-operator-board'],
|
||||
['\n- ', 'globalSearch-instructions-operator-list'],
|
||||
['\n- ', 'globalSearch-instructions-operator-swimlane'],
|
||||
['\n- ', 'globalSearch-instructions-operator-comment'],
|
||||
['\n- ', 'globalSearch-instructions-operator-label'],
|
||||
['\n- ', 'globalSearch-instructions-operator-hash'],
|
||||
['\n- ', 'globalSearch-instructions-operator-user'],
|
||||
['\n- ', 'globalSearch-instructions-operator-at'],
|
||||
['\n- ', 'globalSearch-instructions-operator-member'],
|
||||
['\n- ', 'globalSearch-instructions-operator-assignee'],
|
||||
['\n- ', 'globalSearch-instructions-operator-creator'],
|
||||
['\n- ', 'globalSearch-instructions-operator-org'],
|
||||
['\n- ', 'globalSearch-instructions-operator-team'],
|
||||
['\n- ', 'globalSearch-instructions-operator-due'],
|
||||
['\n- ', 'globalSearch-instructions-operator-created'],
|
||||
['\n- ', 'globalSearch-instructions-operator-modified'],
|
||||
['\n- ', 'globalSearch-instructions-operator-status'],
|
||||
['\n - ', 'globalSearch-instructions-status-archived'],
|
||||
['\n - ', 'globalSearch-instructions-status-public'],
|
||||
['\n - ', 'globalSearch-instructions-status-private'],
|
||||
['\n - ', 'globalSearch-instructions-status-all'],
|
||||
['\n - ', 'globalSearch-instructions-status-ended'],
|
||||
['\n- ', 'globalSearch-instructions-operator-has'],
|
||||
['\n- ', 'globalSearch-instructions-operator-sort'],
|
||||
['\n- ', 'globalSearch-instructions-operator-limit'],
|
||||
['\n## ', 'heading-notes'],
|
||||
['\n- ', 'globalSearch-instructions-notes-1'],
|
||||
['\n- ', 'globalSearch-instructions-notes-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3'],
|
||||
['\n- ', 'globalSearch-instructions-notes-3-2'],
|
||||
['\n- ', 'globalSearch-instructions-notes-4'],
|
||||
['\n- ', 'globalSearch-instructions-notes-5'],
|
||||
].forEach(([prefix, instruction]) => {
|
||||
text += `${prefix}${TAPi18n.__(instruction, tags)}`
|
||||
// Replace *<text>* with `<text>` so markdown shows correctly
|
||||
.replace(/\*\</, '`<')
|
||||
.replace(/\>\*/, '\>\`');
|
||||
});
|
||||
return text;
|
||||
} catch (e) {
|
||||
console.error('Error building searchInstructions:', e);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
labelColors() {
|
||||
return LABEL_COLORS.map(
|
||||
color => {
|
||||
return { color, name: TAPi18n.__(`color-${color}`) };
|
||||
},
|
||||
);
|
||||
try {
|
||||
if (!LABEL_COLORS) return [];
|
||||
return LABEL_COLORS.map(
|
||||
color => {
|
||||
return { color, name: TAPi18n.__(`color-${color}`) };
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Error building labelColors:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.globalSearch.events({
|
||||
'click input.global-search-query-input'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
'focus input.global-search-query-input'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
'keyup input.global-search-query-input'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
// Store the query value
|
||||
tpl.search.query.set(evt.target.value);
|
||||
},
|
||||
'keydown input.global-search-query-input'(evt, tpl) {
|
||||
// Allow normal key behavior but capture Enter key
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
searchAllBoards(tpl, evt.target.value);
|
||||
}
|
||||
},
|
||||
'submit .js-search-query-form'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
searchAllBoards(tpl, evt.target.searchQuery.value);
|
||||
@@ -269,30 +325,6 @@ Template.globalSearch.events({
|
||||
);
|
||||
document.getElementById('global-search-input').focus();
|
||||
},
|
||||
'click .js-copy-debug-selector'(evt) {
|
||||
/* Get the text field */
|
||||
const selector = document.getElementById("debug-selector");
|
||||
|
||||
try {
|
||||
navigator.clipboard.writeText(selector.textContent);
|
||||
alert("Selector copied to clipboard");
|
||||
} catch(err) {
|
||||
alert("Error copying text: " + err);
|
||||
}
|
||||
|
||||
},
|
||||
'click .js-copy-debug-projection'(evt) {
|
||||
/* Get the text field */
|
||||
const projection = document.getElementById("debug-projection");
|
||||
|
||||
try {
|
||||
navigator.clipboard.writeText(projection.textContent);
|
||||
alert("Projection copied to clipboard");
|
||||
} catch(err) {
|
||||
alert("Error copying text: " + err);
|
||||
}
|
||||
|
||||
},
|
||||
'click .js-board-title'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const input = document.getElementById('global-search-input');
|
||||
@@ -340,47 +372,46 @@ Template.globalSearch.events({
|
||||
},
|
||||
});
|
||||
|
||||
// resultsPaged template helpers - this template is used by globalSearch, brokenCards, and brokenCardsReport.
|
||||
// It receives the parent's data context (which includes reactive vars) via +resultsPaged(this).
|
||||
// The jade accesses resultsHeading.get, results.get, getSearchHref, hasPreviousPage.get, hasNextPage.get
|
||||
// directly on the data context, so we don't need helpers for those when the parent passes
|
||||
// the right data context. However, we need to ensure the helpers exist for the template.
|
||||
// resultsPaged template helpers - receives search data context from parent template.
|
||||
Template.resultsPaged.helpers({
|
||||
resultsHeading() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.resultsHeading) return data.resultsHeading;
|
||||
// fallback: check parent template
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.resultsHeading;
|
||||
return new ReactiveVar('');
|
||||
if (data && data.resultsHeading) {
|
||||
const rv = data.resultsHeading;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
results() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.results) return data.results;
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.results;
|
||||
return new ReactiveVar([]);
|
||||
if (data && data.results) {
|
||||
const rv = data.results;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
getSearchHref() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.getSearchHref) return data.getSearchHref();
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.getSearchHref();
|
||||
if (data && typeof data.getSearchHref === 'function') {
|
||||
return data.getSearchHref();
|
||||
}
|
||||
return '';
|
||||
},
|
||||
hasPreviousPage() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.hasPreviousPage) return data.hasPreviousPage;
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.hasPreviousPage;
|
||||
return new ReactiveVar(false);
|
||||
if (data && data.hasPreviousPage) {
|
||||
const rv = data.hasPreviousPage;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasNextPage() {
|
||||
const data = Template.currentData();
|
||||
if (data && data.hasNextPage) return data.hasNextPage;
|
||||
const parentTpl = Template.instance().view.parentView.templateInstance && Template.instance().view.parentView.templateInstance();
|
||||
if (parentTpl && parentTpl.search) return parentTpl.search.hasNextPage;
|
||||
return new ReactiveVar(false);
|
||||
if (data && data.hasNextPage) {
|
||||
const rv = data.hasNextPage;
|
||||
return rv && typeof rv.get === 'function' ? rv.get() : rv;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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'}}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -142,10 +142,23 @@ function saveSorting(ui) {
|
||||
|
||||
function currentListIsInThisSwimlane(swimlaneId) {
|
||||
const currentList = Utils.getCurrentList();
|
||||
return (
|
||||
currentList &&
|
||||
(currentList.swimlaneId === swimlaneId || currentList.swimlaneId === '')
|
||||
if (!currentList) return false;
|
||||
// Match the list's own swimlane, or shared/orphaned lists (empty/null
|
||||
// swimlaneId are visible in every swimlane as a fallback).
|
||||
if (currentList.swimlaneId === swimlaneId || !currentList.swimlaneId) {
|
||||
return true;
|
||||
}
|
||||
// Also match when the list has an orphaned swimlaneId (references a deleted
|
||||
// swimlane) and THIS is the first swimlane — orphaned lists are shown there.
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) return false;
|
||||
const allSwimlanes = ReactiveCache.getSwimlanes(
|
||||
{ boardId: currentBoard._id, archived: false },
|
||||
{ sort: ['sort'] },
|
||||
);
|
||||
if (!allSwimlanes.length || allSwimlanes[0]._id !== swimlaneId) return false;
|
||||
const validIds = new Set(allSwimlanes.map(s => s._id));
|
||||
return !validIds.has(currentList.swimlaneId);
|
||||
}
|
||||
|
||||
function currentCardIsInThisList(listId, swimlaneId) {
|
||||
@@ -159,7 +172,10 @@ function currentCardIsInThisList(listId, swimlaneId) {
|
||||
return (
|
||||
currentCard &&
|
||||
currentCard.listId === listId &&
|
||||
currentCard.swimlaneId === swimlaneId
|
||||
// Match cards for this swimlane, AND orphaned/shared cards that have no
|
||||
// swimlaneId assigned (null/empty) — they are shown in every swimlane as
|
||||
// a fallback so no content is ever invisible.
|
||||
(currentCard.swimlaneId === swimlaneId || !currentCard.swimlaneId)
|
||||
);
|
||||
else if (
|
||||
//currentUser &&
|
||||
@@ -582,8 +598,38 @@ Template.swimlane.helpers({
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
lists() {
|
||||
// Return per-swimlane lists for this swimlane
|
||||
return this.myLists();
|
||||
const swimlane = this;
|
||||
// myLists() already covers:
|
||||
// • lists owned by this swimlane (swimlaneId === this._id)
|
||||
// • shared / pre-migration lists (swimlaneId empty or null)
|
||||
const regularLists = swimlane.myLists();
|
||||
|
||||
// Additionally, detect lists whose swimlaneId references a swimlane that
|
||||
// no longer exists ("orphaned-swimlane" lists). These would be invisible
|
||||
// in every swimlane without this fallback. Show them in the FIRST swimlane
|
||||
// on the board so they are always accessible without a DB migration.
|
||||
const allSwimlanes = ReactiveCache.getSwimlanes(
|
||||
{ boardId: swimlane.boardId, archived: false },
|
||||
{ sort: ['sort'] },
|
||||
);
|
||||
// Only the first swimlane picks up orphaned-swimlane lists.
|
||||
if (!allSwimlanes.length || allSwimlanes[0]._id !== swimlane._id) {
|
||||
return regularLists;
|
||||
}
|
||||
const validIds = allSwimlanes.map(s => s._id);
|
||||
const orphaned = swimlane.orphanedSwimlaneLists(validIds);
|
||||
if (!orphaned.length) return regularLists;
|
||||
|
||||
// Merge, deduplicating by _id (regularLists may already contain some).
|
||||
const seen = new Set(regularLists.map(l => l._id));
|
||||
const combined = [...regularLists];
|
||||
for (const l of orphaned) {
|
||||
if (!seen.has(l._id)) {
|
||||
seen.add(l._id);
|
||||
combined.push(l);
|
||||
}
|
||||
}
|
||||
return combined;
|
||||
},
|
||||
collapseSwimlane() {
|
||||
return Utils.getSwimlaneCollapseState(this);
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}),
|
||||
});
|
||||
+40
-98
@@ -32,10 +32,7 @@ export class CardSearchPaged {
|
||||
const that = this;
|
||||
this.subscriptionCallbacks = {
|
||||
onReady() {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Subscription ready, getting results...');
|
||||
console.log('Subscription ready - sessionId:', that.sessionId);
|
||||
}
|
||||
const cardsInCollection = Cards.find().count();
|
||||
|
||||
// Wait for session data to be available (with timeout)
|
||||
let waitCount = 0;
|
||||
@@ -44,21 +41,12 @@ export class CardSearchPaged {
|
||||
const waitForSessionData = () => {
|
||||
waitCount++;
|
||||
const sessionData = that.getSessionData();
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('waitForSessionData - attempt', waitCount, 'session data:', sessionData);
|
||||
}
|
||||
|
||||
if (sessionData) {
|
||||
const results = that.getResults();
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Search results count:', results ? results.length : 0);
|
||||
}
|
||||
|
||||
// If no results and this is a due cards search, try to retry
|
||||
if ((!results || results.length === 0) && that.searchRetryCount !== undefined && that.searchRetryCount < that.maxRetries) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('No results found, retrying search...');
|
||||
}
|
||||
that.searchRetryCount++;
|
||||
Meteor.setTimeout(() => {
|
||||
if (that.performSearch) {
|
||||
@@ -68,47 +56,37 @@ export class CardSearchPaged {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the results in the ReactiveVar so they display in the template
|
||||
that.results.set(results);
|
||||
that.resultsHeading.set(that.getResultsHeading());
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(true);
|
||||
that.serverError.set(false);
|
||||
} else if (waitCount < maxWaitCount) {
|
||||
// Session data not available yet, wait a bit more
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Session data not available yet, waiting... (attempt', waitCount, 'of', maxWaitCount, ')');
|
||||
}
|
||||
Meteor.setTimeout(waitForSessionData, 200);
|
||||
} else {
|
||||
// Timeout reached, try fallback search
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Timeout reached waiting for session data, trying fallback search');
|
||||
}
|
||||
const results = that.getResults();
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Fallback search results count:', results ? results.length : 0);
|
||||
}
|
||||
} else if (waitCount < maxWaitCount) {
|
||||
// Session data not available yet, wait a bit more
|
||||
Meteor.setTimeout(waitForSessionData, 200);
|
||||
} else {
|
||||
// Timeout reached, try fallback search
|
||||
const results = that.getResults();
|
||||
|
||||
if (results && results.length > 0) {
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(true);
|
||||
that.serverError.set(false);
|
||||
} else {
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(false);
|
||||
that.serverError.set(true);
|
||||
}
|
||||
}
|
||||
if (results && results.length > 0) {
|
||||
that.results.set(results);
|
||||
that.resultsHeading.set(that.getResultsHeading());
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(true);
|
||||
that.serverError.set(false);
|
||||
} else {
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(false);
|
||||
that.serverError.set(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start waiting for session data
|
||||
Meteor.setTimeout(waitForSessionData, 100);
|
||||
},
|
||||
onError(error) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Subscription error:', error);
|
||||
console.log('Error.reason:', error.reason);
|
||||
console.log('Error.message:', error.message);
|
||||
console.log('Error.stack:', error.stack);
|
||||
}
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(false);
|
||||
that.serverError.set(true);
|
||||
@@ -131,56 +109,41 @@ export class CardSearchPaged {
|
||||
|
||||
getSessionData(sessionId) {
|
||||
const sessionIdToUse = sessionId || SessionData.getSessionId();
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getSessionData - looking for sessionId:', sessionIdToUse);
|
||||
}
|
||||
|
||||
// Try using the raw SessionData collection instead of ReactiveCache
|
||||
// Use SessionData.findOne() directly - it's synchronous on the client
|
||||
const sessionData = SessionData.findOne({
|
||||
userId: Meteor.userId(),
|
||||
sessionId: sessionIdToUse,
|
||||
});
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getSessionData - found session data (raw):', sessionData);
|
||||
}
|
||||
|
||||
// Also try ReactiveCache for comparison
|
||||
const reactiveSessionData = ReactiveCache.getSessionData({
|
||||
sessionId: sessionIdToUse,
|
||||
});
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getSessionData - found session data (reactive):', reactiveSessionData);
|
||||
}
|
||||
|
||||
return sessionData || reactiveSessionData;
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
getResults() {
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('getting results');
|
||||
this.sessionData = this.getSessionData();
|
||||
// eslint-disable-next-line no-console
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getResults - sessionId:', this.sessionId);
|
||||
console.log('getResults - session data:', this.sessionData);
|
||||
}
|
||||
const cards = [];
|
||||
|
||||
if (this.sessionData && this.sessionData.cards) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getResults - cards array length:', this.sessionData.cards.length);
|
||||
}
|
||||
this.sessionData.cards.forEach(cardId => {
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getResults - card:', cardId, card);
|
||||
if (card && card._id) {
|
||||
cards.push(card);
|
||||
}
|
||||
cards.push(card);
|
||||
});
|
||||
|
||||
// Fallback: if no cards found, try fetching directly from Cards collection
|
||||
if (cards.length === 0 && this.sessionData.cards && this.sessionData.cards.length > 0) {
|
||||
if (directCards && directCards.length > 0) {
|
||||
directCards.forEach(card => {
|
||||
if (card && card._id) {
|
||||
cards.push(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.queryErrors = this.sessionData.errors || [];
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getResults - no sessionData or no cards array, trying direct card search');
|
||||
}
|
||||
// Fallback: try to get cards directly from the client-side collection
|
||||
// Use a more efficient query with limit and sort
|
||||
const selector = {
|
||||
@@ -192,9 +155,6 @@ export class CardSearchPaged {
|
||||
limit: 100 // Limit to 100 cards for performance
|
||||
};
|
||||
const allCards = Cards.find(selector, options).fetch();
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('getResults - direct card search found:', allCards ? allCards.length : 0, 'cards');
|
||||
}
|
||||
|
||||
if (allCards && allCards.length > 0) {
|
||||
allCards.forEach(card => {
|
||||
@@ -212,11 +172,6 @@ export class CardSearchPaged {
|
||||
// return null;
|
||||
}
|
||||
this.debug.set(new QueryDebug(this.sessionData ? this.sessionData.debug : null));
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('debug:', this.debug.get().get());
|
||||
console.log('debug.show():', this.debug.get().show());
|
||||
console.log('debug.showSelector():', this.debug.get().showSelector());
|
||||
}
|
||||
|
||||
if (cards) {
|
||||
if (this.sessionData) {
|
||||
@@ -254,15 +209,7 @@ export class CardSearchPaged {
|
||||
}
|
||||
|
||||
getSubscription(queryParams) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Subscribing to globalSearch with:', {
|
||||
sessionId: this.sessionId,
|
||||
params: queryParams.params,
|
||||
text: queryParams.text
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to both globalSearch and sessionData
|
||||
// Subscribe to globalSearch which includes sessionData as the 11th cursor
|
||||
const globalSearchHandle = Meteor.subscribe(
|
||||
'globalSearch',
|
||||
this.sessionId,
|
||||
@@ -271,11 +218,6 @@ export class CardSearchPaged {
|
||||
this.subscriptionCallbacks,
|
||||
);
|
||||
|
||||
const sessionDataHandle = Meteor.subscribe('sessionData', this.sessionId);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Subscribed to sessionData with sessionId:', this.sessionId);
|
||||
}
|
||||
|
||||
return globalSearchHandle;
|
||||
}
|
||||
|
||||
|
||||
+373
-69
@@ -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!
|
||||
};
|
||||
};
|
||||
+5
-1
@@ -302,7 +302,11 @@ FlowRouter.route('/global-search', {
|
||||
// Set title with product name
|
||||
const settings = Settings.findOne({});
|
||||
const productName = (settings && settings.productName) ? settings.productName : 'Wekan';
|
||||
document.title = `${TAPi18n.__('globalSearch-title')} - ${productName}`;
|
||||
try {
|
||||
document.title = `${TAPi18n.__('globalSearch-title')} - ${productName}`;
|
||||
} catch (e) {
|
||||
document.title = `Search All Boards - ${productName}`;
|
||||
}
|
||||
|
||||
if (FlowRouter.getQueryParam('q')) {
|
||||
Session.set(
|
||||
|
||||
+130
-116
@@ -108,54 +108,54 @@ services:
|
||||
#
|
||||
# If this is a fresh install, wekan-migrate exits instantly (nothing to migrate).
|
||||
#-------------------------------------------------------------------------------------
|
||||
|
||||
wekan-postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: wekan-postgres
|
||||
restart: always
|
||||
networks:
|
||||
- wekan-tier
|
||||
expose:
|
||||
- 5432
|
||||
environment:
|
||||
- POSTGRES_DB=wekan
|
||||
- POSTGRES_USER=wekan
|
||||
- POSTGRES_PASSWORD=wekan
|
||||
# Tune for a kanban workload (low connection count, moderate write rate)
|
||||
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U wekan -d wekan"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
wekan-ferretdb:
|
||||
image: ghcr.io/ferretdb/ferretdb:2
|
||||
container_name: wekan-ferretdb
|
||||
restart: always
|
||||
networks:
|
||||
- wekan-tier
|
||||
expose:
|
||||
- 27017
|
||||
environment:
|
||||
# FerretDB stores everything in PostgreSQL
|
||||
- FERRETDB_POSTGRESQL_URL=postgresql://wekan:wekan@wekan-postgres:5432/wekan
|
||||
# Emulate MongoDB replica-set for Meteor OpLog (real-time updates)
|
||||
- FERRETDB_REPLICATION_SET_NAME=rs0
|
||||
# Suppress verbose debug output
|
||||
- FERRETDB_LOG_LEVEL=warn
|
||||
depends_on:
|
||||
wekan-postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "ferretdb", "--version"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
#
|
||||
#wekan-postgres:
|
||||
# image: postgres:16-alpine
|
||||
# container_name: wekan-postgres
|
||||
# restart: always
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# expose:
|
||||
# - 5432
|
||||
# environment:
|
||||
# - POSTGRES_DB=wekan
|
||||
# - POSTGRES_USER=wekan
|
||||
# - POSTGRES_PASSWORD=wekan
|
||||
# # Tune for a kanban workload (low connection count, moderate write rate)
|
||||
# - POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
|
||||
# volumes:
|
||||
# - /etc/localtime:/etc/localtime:ro
|
||||
# - wekan-postgres:/var/lib/postgresql/data
|
||||
# healthcheck:
|
||||
# test: ["CMD-SHELL", "pg_isready -U wekan -d wekan"]
|
||||
# interval: 5s
|
||||
# timeout: 5s
|
||||
# retries: 10
|
||||
#
|
||||
#wekan-ferretdb:
|
||||
# image: ghcr.io/ferretdb/ferretdb:2
|
||||
# container_name: wekan-ferretdb
|
||||
# restart: always
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# expose:
|
||||
# - 27017
|
||||
# environment:
|
||||
# # FerretDB stores everything in PostgreSQL
|
||||
# - FERRETDB_POSTGRESQL_URL=postgresql://wekan:wekan@wekan-postgres:5432/wekan
|
||||
# # Emulate MongoDB replica-set for Meteor OpLog (real-time updates)
|
||||
# - FERRETDB_REPLICATION_SET_NAME=rs0
|
||||
# # Suppress verbose debug output
|
||||
# - FERRETDB_LOG_LEVEL=warn
|
||||
# depends_on:
|
||||
# wekan-postgres:
|
||||
# condition: service_healthy
|
||||
# healthcheck:
|
||||
# test: ["CMD", "ferretdb", "--version"]
|
||||
# interval: 5s
|
||||
# timeout: 5s
|
||||
# retries: 10
|
||||
#
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== MIGRATION SERVICE ====
|
||||
# Runs once, before WeKan starts. Exits 0 on success, 1 on fatal error.
|
||||
@@ -170,71 +170,79 @@ services:
|
||||
# mongodb://192.168.1.10:27017/wekan
|
||||
# Leave it unset or empty to skip migration.
|
||||
#-------------------------------------------------------------------------------------
|
||||
wekan-migrate:
|
||||
image: node:22.22.2-slim
|
||||
container_name: wekan-migrate
|
||||
restart: "no"
|
||||
networks:
|
||||
- wekan-tier
|
||||
working_dir: /migrate
|
||||
# Install the mongodb driver then run the migration script.
|
||||
# npm ci is used if package-lock.json exists; npm install otherwise.
|
||||
command: >
|
||||
sh -c "
|
||||
cd /migrate &&
|
||||
npm install --omit=dev --prefer-offline 2>/dev/null || npm install --omit=dev &&
|
||||
node migrate-mongodb-to-ferretdb.mjs
|
||||
"
|
||||
environment:
|
||||
# ---- Source (old MongoDB) ----
|
||||
# Set to your old MongoDB URL to trigger data migration.
|
||||
# Leave empty for a fresh install (migration is skipped automatically).
|
||||
- SOURCE_MONGO_URL=
|
||||
#- SOURCE_MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
|
||||
# ---- Target (FerretDB) ----
|
||||
- TARGET_MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
|
||||
|
||||
# ---- File storage ----
|
||||
# Must match the WRITABLE_PATH used by the wekan service.
|
||||
- WRITABLE_PATH=/data
|
||||
|
||||
# ---- Progress dashboard ----
|
||||
# Browse to http://localhost:8080/ while migration runs.
|
||||
- MIGRATION_PORT=8080
|
||||
|
||||
# ---- Tuning (optional) ----
|
||||
#- BATCH_SIZE=200
|
||||
#- DRY_RUN=true # Log only, no writes — useful for testing
|
||||
#- FORCE_MIGRATE=true # Re-run even if already marked complete
|
||||
ports:
|
||||
- 8080:8080
|
||||
depends_on:
|
||||
wekan-ferretdb:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-files:/data:rw
|
||||
- ./releases:/migrate:ro
|
||||
#wekan-migrate:
|
||||
# image: node:24.14.1-slim
|
||||
# container_name: wekan-migrate
|
||||
# restart: "no"
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# working_dir: /migrate
|
||||
# # Install the mongodb driver then run the migration script.
|
||||
# # npm ci is used if package-lock.json exists; npm install otherwise.
|
||||
# command: >
|
||||
# sh -c "
|
||||
# cd /migrate &&
|
||||
# npm install --omit=dev --prefer-offline 2>/dev/null || npm install --omit=dev &&
|
||||
# node migrate-mongodb-to-ferretdb.mjs
|
||||
# "
|
||||
# environment:
|
||||
# # ---- Source (old MongoDB) ----
|
||||
# # Set to your old MongoDB URL to trigger data migration.
|
||||
# # Leave empty for a fresh install (migration is skipped automatically).
|
||||
# - SOURCE_MONGO_URL=
|
||||
# #- SOURCE_MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
#
|
||||
# # ---- Target (FerretDB) ----
|
||||
# - TARGET_MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
|
||||
#
|
||||
# # ---- File storage ----
|
||||
# # Must match the WRITABLE_PATH used by the wekan service.
|
||||
# - WRITABLE_PATH=/data
|
||||
#
|
||||
# # ---- Progress dashboard ----
|
||||
# # Browse to http://localhost:8080/ while migration runs.
|
||||
# - MIGRATION_PORT=8080
|
||||
#
|
||||
# # ---- Tuning (optional) ----
|
||||
# #- BATCH_SIZE=200
|
||||
# #- DRY_RUN=true # Log only, no writes — useful for testing
|
||||
# #- FORCE_MIGRATE=true # Re-run even if already marked complete
|
||||
# ports:
|
||||
# - 8080:8080
|
||||
# depends_on:
|
||||
# wekan-ferretdb:
|
||||
# condition: service_healthy
|
||||
# volumes:
|
||||
# - /etc/localtime:/etc/localtime:ro
|
||||
# - wekan-files:/data:rw
|
||||
# - ./releases:/migrate:ro
|
||||
|
||||
#-------------------------------------------------------------------------------------
|
||||
# ==== LEGACY MONGODB (commented out) ====
|
||||
# Uncomment to keep old MongoDB running alongside FerretDB during migration.
|
||||
# After migration is verified, remove this block entirely.
|
||||
#-------------------------------------------------------------------------------------
|
||||
#wekandb:
|
||||
# image: mongo:7
|
||||
# container_name: wekan-db
|
||||
# restart: always
|
||||
# command: mongod --logpath /dev/null --oplogSize 128 --quiet
|
||||
# networks:
|
||||
# - wekan-tier
|
||||
# expose:
|
||||
# - 27017
|
||||
# volumes:
|
||||
# - /etc/localtime:/etc/localtime:ro
|
||||
# - wekan-db:/data/db
|
||||
# - wekan-db-dump:/dump
|
||||
wekandb:
|
||||
image: mongo:7
|
||||
container_name: wekan-db
|
||||
restart: always
|
||||
#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:
|
||||
- 27017
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-db:/data/db
|
||||
- wekan-db-dump:/dump
|
||||
|
||||
wekan:
|
||||
#-------------------------------------------------------------------------------------
|
||||
@@ -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
|
||||
@@ -295,7 +307,8 @@ services:
|
||||
#- S3_SECRET_FILE=/run/secrets/s3_secret
|
||||
#-----------------------------------------------------------------
|
||||
# ==== MONGO_URL — points to FerretDB (MongoDB-compatible) ====
|
||||
- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
|
||||
#- MONGO_URL=mongodb://wekan-ferretdb:27017/wekan
|
||||
- MONGO_URL=mongodb://wekandb:27017/wekan
|
||||
#- MONGO_PASSWORD_FILE=/run/secrets/mongo_password
|
||||
#---------------------------------------------------------------
|
||||
# ==== ROOT_URL SETTING ====
|
||||
@@ -345,7 +358,8 @@ services:
|
||||
# See: https://blog.meteor.com/tuning-meteor-mongo-livedata-for-scalability-13fe9deb8908
|
||||
# FerretDB emulates OpLog via --replication-set-name=rs0.
|
||||
# HIGHLY RECOMMENDED — enables real-time DDP updates (sub-100ms latency).
|
||||
- MONGO_OPLOG_URL=mongodb://wekan-ferretdb:27017/local?replicaSet=rs0
|
||||
#- MONGO_OPLOG_URL=mongodb://wekan-ferretdb:27017/local?replicaSet=rs0
|
||||
- MONGO_OPLOG_URL=mongodb://wekandb:27017/local?replicaSet=rs0
|
||||
#---------------------------------------------------------------
|
||||
# ==== OPTIONAL: KADIRA PERFORMANCE MONITORING FOR METEOR ====
|
||||
# https://github.com/edemaine/kadira-compose
|
||||
@@ -853,11 +867,11 @@ services:
|
||||
# Wait spinner to use
|
||||
# - WAIT_SPINNER=Bounce
|
||||
#---------------------------------------------------------------------
|
||||
depends_on:
|
||||
wekan-migrate:
|
||||
condition: service_completed_successfully
|
||||
wekan-ferretdb:
|
||||
condition: service_healthy
|
||||
#depends_on:
|
||||
# wekan-migrate:
|
||||
# condition: service_completed_successfully
|
||||
# wekan-ferretdb:
|
||||
# condition: service_healthy
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- wekan-files:/data:rw
|
||||
@@ -921,8 +935,8 @@ services:
|
||||
volumes:
|
||||
wekan-files:
|
||||
driver: local
|
||||
wekan-postgres:
|
||||
driver: local
|
||||
#wekan-postgres:
|
||||
# driver: local
|
||||
# Legacy MongoDB volumes — kept so existing data survives until migration is done.
|
||||
# Remove once migration is verified and old MongoDB is decommissioned.
|
||||
wekan-db:
|
||||
|
||||
@@ -78,7 +78,17 @@ git clone git@github.com:YourGithubUsername/wekan.git
|
||||
|
||||
cd wekan
|
||||
```
|
||||
|
||||
### IMPORTANT: Do NOT `npm audit fix --force`
|
||||
|
||||
IMPORTANT: Do NOT `npm audit fix --force`, it downgrades @meteorjs/rspack and breaks builds.
|
||||
`npm audit` shows false info, it does not recognize that elliptic is already updated
|
||||
to 6.6.1 with override at package.json like you can see with `npm list elliptic`.
|
||||
https://github.com/wekan/wekan/blob/main/CHANGELOG.md#v850-2026-04-03-wekan--release
|
||||
|
||||
### 3. Select option 1 to install dependencies, and then Enter.
|
||||
|
||||
|
||||
```
|
||||
./rebuild-wekan.sh
|
||||
|
||||
@@ -119,83 +129,17 @@ git push
|
||||
```
|
||||
And then at your GitHub for `https://github.com/YOUR-GITHUB-USERNAME/wekan` click `Create pull request`.
|
||||
|
||||
# Meteor 3
|
||||
|
||||
At 2024-06-26, it looks like from https://nodejs.org/en that Node.js LTS version is 20.15.0 , so change to newest Node.js LTS, delete old Meteor:
|
||||
```
|
||||
sudo n 20.15.0
|
||||
|
||||
sudo npm -g install npm
|
||||
|
||||
cd
|
||||
|
||||
rm -rf .meteor
|
||||
```
|
||||
Check how to install newest Meteor from Meteor 3 PR at https://github.com/meteor/meteor/pull/13163 , for example:
|
||||
```
|
||||
npx meteor@rc
|
||||
```
|
||||
Check what branches there are:
|
||||
```
|
||||
cd repos/wekan
|
||||
|
||||
git branch -a
|
||||
```
|
||||
Change to Meteor 3 branch:
|
||||
```
|
||||
git checkout feature-meteor3
|
||||
```
|
||||
Build wekan:
|
||||
```
|
||||
./rebuild-wekan.sh
|
||||
|
||||
2
|
||||
```
|
||||
If there are errors, try to fix them.
|
||||
|
||||
Or try to run wekan:
|
||||
```
|
||||
./rebuild-wekan.sh
|
||||
|
||||
3
|
||||
```
|
||||
# Updating
|
||||
|
||||
There are usually updates both for npm packages and Meteor
|
||||
|
||||
Updating npm packages:
|
||||
```
|
||||
npm update
|
||||
```
|
||||
Checking for vulnerable packages:
|
||||
```
|
||||
npm audit
|
||||
```
|
||||
Fixing vulnerable npm packages by updating to newer packages:
|
||||
```
|
||||
npm audit fix
|
||||
```
|
||||
If that did not help, use force:
|
||||
```
|
||||
npm audit fix --force
|
||||
```
|
||||
If that did not help, read links from that `npm audit` command did show, remove deprecated dependencies, update to other maintained dependencies.
|
||||
## Updating
|
||||
|
||||
Updating to next Meteor release:
|
||||
```
|
||||
meteor update
|
||||
|
||||
meteor npm update
|
||||
```
|
||||
Updating to specific Meteor release:
|
||||
```
|
||||
meteor update --release METEOR@3.0-rc.4
|
||||
```
|
||||
Trying to update all Meteor packages:
|
||||
```
|
||||
meteor update --release METEOR@3.0-rc.4 --all-packages
|
||||
```
|
||||
Allowing incompatible updates, that may sometime work:
|
||||
```
|
||||
meteor update --release METEOR@3.0-rc.4 --all-packages --allow-incompatible-update
|
||||
meteor update --release METEOR@3.4
|
||||
```
|
||||
If you are changing Meteor and Node.js versions, you may need to reset Meteor:
|
||||
```
|
||||
|
||||
@@ -10,9 +10,8 @@ This is without container (without Docker or Snap).
|
||||
|
||||
Right click and download files 1-4:
|
||||
|
||||
1. [wekan-8.43-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.43/wekan-8.43-amd64-windows.zip)
|
||||
|
||||
2. [node.exe](https://nodejs.org/dist/latest-v14.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.43-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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -468,22 +468,22 @@
|
||||
"export-board-html": "ייצוא לוח ל־HTML",
|
||||
"export-card": "ייצוא כרטיס",
|
||||
"export-card-pdf": "ייצוא כרטיס ל־PDF",
|
||||
"export-card-excel": "Export card to Excel",
|
||||
"export-card-excel-fields": "Select fields to include in Excel export:",
|
||||
"export-card-excel": "ייצוא כרטיס ל־Excel",
|
||||
"export-card-excel-fields": "נא לבחור שדות לכלול בייצוא ל־Excel:",
|
||||
"export-card-subtasks": "תת משימות",
|
||||
"export-card-attachments": "קבצים מצורפים",
|
||||
"export-card-field-people": "People (Creator, Owner, Members, Assignees)",
|
||||
"export-card-field-board-info": "Board Info (Board, List, Swimlane)",
|
||||
"export-card-field-dates": "Dates (Created, Received, Start, Due, End)",
|
||||
"export-card-attachment-filename": "Filename",
|
||||
"export-card-field-people": "אנשים (מי יצר, בעלים, חברים, אחראים)",
|
||||
"export-card-field-board-info": "פרטי לוח (לוח, רשימת, מסלולים)",
|
||||
"export-card-field-dates": "תאריכים (יצירה, קבלה, התחלה, סיום מיועד, סיום)",
|
||||
"export-card-attachment-filename": "שם קובץ",
|
||||
"export-card-attachment-size": "גודל",
|
||||
"export-card-attachment-type": "סוג",
|
||||
"export-card-attachment-uploaded-by": "Uploaded By",
|
||||
"export-card-attachment-uploaded-at": "Uploaded At",
|
||||
"export-card-attachment-image-previews": "Image Previews",
|
||||
"export-card-excel-no-disk-space": "Cannot export to Excel: not enough free disk space",
|
||||
"export-card-excel-free": "free",
|
||||
"export-card-excel-needed": "needed",
|
||||
"export-card-attachment-uploaded-by": "הועלה על ידי",
|
||||
"export-card-attachment-uploaded-at": "הועלה ב־",
|
||||
"export-card-attachment-image-previews": "תצוגות מקדימות של תמונות",
|
||||
"export-card-excel-no-disk-space": "לא ניתן לייצא ל־Excel: אין מספיק מקום פנוי בכונן",
|
||||
"export-card-excel-free": "פנוי",
|
||||
"export-card-excel-needed": "דרוש",
|
||||
"user-can-not-export-card-to-pdf": "המשתמש לא יכול לייצא כרטיס ל־PDF",
|
||||
"user-can-not-export-card-to-excel": "User can not export card to Excel",
|
||||
"exportBoardPopup-title": "ייצוא לוח",
|
||||
|
||||
@@ -460,8 +460,8 @@
|
||||
"export-board": "Exportera tavla",
|
||||
"export-board-json": "Exportera tavla till JSON",
|
||||
"export-board-csv": "Expoertera tavla till CSV",
|
||||
"export-board-csv-comma": "Export to CSV (separator: ,)",
|
||||
"export-board-csv-semicolon": "Export to CSV (separator: ;)",
|
||||
"export-board-csv-comma": "Exportera till CSV (avgränsare: ,)",
|
||||
"export-board-csv-semicolon": "Exportera till CSV (avgränsare: ;)",
|
||||
"export-board-tsv": "Exportera tavla till TSV",
|
||||
"export-board-excel": "Exportera tavla till Excel",
|
||||
"user-can-not-export-excel": "Användare kan inte exportera till Excel",
|
||||
@@ -1037,7 +1037,7 @@
|
||||
"hide-minicard-label-text": "Dölj etikett för minikort",
|
||||
"show-desktop-drag-handles": "Visa greppytor i desktop",
|
||||
"assignee": "Tilldelad till",
|
||||
"assignees": "Assignees",
|
||||
"assignees": "Tilldelade",
|
||||
"cardAssigneesPopup-title": "Tilldelad till",
|
||||
"addmore-detail": "Lägg till detaljerad beskrivning",
|
||||
"show-on-card": "Visa på kort",
|
||||
|
||||
@@ -5,10 +5,28 @@ import { generateUniversalAttachmentUrl } from '/models/lib/universalUrlGenerato
|
||||
// XXX Enforce a schema for the Attachments FilesCollection
|
||||
// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
|
||||
|
||||
// Compute storage path:
|
||||
// - Docker (WRITABLE_PATH=/data): /data/files/attachments
|
||||
// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/attachments
|
||||
const computeAttachmentStoragePath = () => {
|
||||
const basePath = process.env.WRITABLE_PATH || process.cwd();
|
||||
const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files');
|
||||
if (endsWithFiles) {
|
||||
// Snap: WRITABLE_PATH already includes /files
|
||||
return basePath + '/attachments';
|
||||
} else {
|
||||
// Docker & Dev: append /files/attachments
|
||||
return basePath + '/files/attachments';
|
||||
}
|
||||
};
|
||||
|
||||
const storagePath = Meteor.isServer ? computeAttachmentStoragePath() : 'assets/app/uploads/attachments';
|
||||
|
||||
const Attachments = new FilesCollection({
|
||||
debug: false, // Change to `true` for debugging
|
||||
collectionName: 'attachments',
|
||||
allowClientCode: true,
|
||||
storagePath: storagePath,
|
||||
namingFunction(opts) {
|
||||
let filenameWithoutExtension = ""
|
||||
let fileId = "";
|
||||
|
||||
@@ -22,7 +22,15 @@ let attachmentUploadMimeTypes = [];
|
||||
let attachmentUploadSize = 0;
|
||||
|
||||
const attachmentBucket = createBucket('attachments');
|
||||
const storagePath = path.join(process.env.WRITABLE_PATH || process.cwd(), 'attachments');
|
||||
|
||||
// Compute storage path:
|
||||
// - Docker (WRITABLE_PATH=/data): /data/files/attachments
|
||||
// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/attachments
|
||||
const basePath = process.env.WRITABLE_PATH || process.cwd();
|
||||
const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files');
|
||||
const storagePath = endsWithFiles
|
||||
? path.join(basePath, 'attachments')
|
||||
: path.join(basePath, 'files', 'attachments');
|
||||
|
||||
if (process.env.ATTACHMENTS_UPLOAD_MIME_TYPES) {
|
||||
attachmentUploadMimeTypes = process.env.ATTACHMENTS_UPLOAD_MIME_TYPES.split(',');
|
||||
|
||||
@@ -7,10 +7,28 @@ const getTAPi18n = () => require('/imports/i18n').TAPi18n;
|
||||
|
||||
let avatarsUploadSize = 72000;
|
||||
|
||||
// Compute storage path:
|
||||
// - Docker (WRITABLE_PATH=/data): /data/files/avatars
|
||||
// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/avatars
|
||||
const computeAvatarStoragePath = () => {
|
||||
const basePath = process.env.WRITABLE_PATH || process.cwd();
|
||||
const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files');
|
||||
if (endsWithFiles) {
|
||||
// Snap: WRITABLE_PATH already includes /files
|
||||
return basePath + '/avatars';
|
||||
} else {
|
||||
// Docker & Dev: append /files/avatars
|
||||
return basePath + '/files/avatars';
|
||||
}
|
||||
};
|
||||
|
||||
const storagePath = Meteor.isServer ? computeAvatarStoragePath() : 'assets/app/uploads/avatars';
|
||||
|
||||
const Avatars = new FilesCollection({
|
||||
debug: false, // Change to `true` for debugging
|
||||
collectionName: 'avatars',
|
||||
allowClientCode: true,
|
||||
storagePath: storagePath,
|
||||
namingFunction(opts) {
|
||||
let filenameWithoutExtension = ""
|
||||
let fileId = "";
|
||||
|
||||
@@ -40,7 +40,15 @@ if (process.env.AVATARS_UPLOAD_EXTERNAL_PROGRAM) {
|
||||
}
|
||||
|
||||
const avatarsBucket = createBucket('avatars');
|
||||
const storagePath = path.join(process.env.WRITABLE_PATH || process.cwd(), 'avatars');
|
||||
|
||||
// Compute storage path:
|
||||
// - Docker (WRITABLE_PATH=/data): /data/files/avatars
|
||||
// - Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/avatars
|
||||
const basePath = process.env.WRITABLE_PATH || process.cwd();
|
||||
const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files');
|
||||
const storagePath = endsWithFiles
|
||||
? path.join(basePath, 'avatars')
|
||||
: path.join(basePath, 'files', 'avatars');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File store strategy factory
|
||||
@@ -72,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);
|
||||
@@ -85,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) {
|
||||
|
||||
+106
-3
@@ -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: {
|
||||
/**
|
||||
@@ -1902,9 +2000,14 @@ Boards.userBoardIds = async (userId, archived = false, selector = {}) => {
|
||||
|
||||
Boards.colorMap = () => {
|
||||
const colors = {};
|
||||
const TAPi18n = getTAPi18n();
|
||||
for (const color of Boards.labelColors()) {
|
||||
colors[TAPi18n.__(`color-${color}`)] = color;
|
||||
try {
|
||||
const TAPi18n = getTAPi18n();
|
||||
for (const color of Boards.labelColors()) {
|
||||
colors[TAPi18n.__(`color-${color}`)] = color;
|
||||
}
|
||||
} catch (e) {
|
||||
// i18n not ready yet, return empty map
|
||||
// The colorMap will be regenerated when i18n is ready
|
||||
}
|
||||
return colors;
|
||||
};
|
||||
|
||||
+6
-6
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+18
-2
@@ -250,7 +250,16 @@ Lists.helpers({
|
||||
listId: this._id,
|
||||
archived: false,
|
||||
};
|
||||
if (swimlaneId) selector.swimlaneId = swimlaneId;
|
||||
if (swimlaneId) {
|
||||
// Fallback: also surface cards with no swimlaneId (null/empty) so that
|
||||
// pre-migration / orphaned cards are always visible in every swimlane
|
||||
// without requiring a database migration.
|
||||
selector.$or = [
|
||||
{ swimlaneId },
|
||||
{ swimlaneId: null }, // null covers null AND missing field
|
||||
{ swimlaneId: '' }, // empty string from shared-lists era
|
||||
];
|
||||
}
|
||||
const filterSelector =
|
||||
typeof Filter !== 'undefined' && typeof Filter.mongoSelector === 'function'
|
||||
? Filter.mongoSelector(selector)
|
||||
@@ -264,7 +273,14 @@ Lists.helpers({
|
||||
listId: this._id,
|
||||
archived: false,
|
||||
};
|
||||
if (swimlaneId) selector.swimlaneId = swimlaneId;
|
||||
if (swimlaneId) {
|
||||
// Same fallback as cards(): include orphaned cards with no swimlaneId.
|
||||
selector.$or = [
|
||||
{ swimlaneId },
|
||||
{ swimlaneId: null },
|
||||
{ swimlaneId: '' },
|
||||
];
|
||||
}
|
||||
const ret = ReactiveCache.getCards(selector, { sort: ['sort'] });
|
||||
return ret;
|
||||
},
|
||||
|
||||
+30
-3
@@ -351,12 +351,39 @@ Swimlanes.helpers({
|
||||
},
|
||||
|
||||
myLists() {
|
||||
// Return per-swimlane lists: provide lists specific to this swimlane
|
||||
// Return per-swimlane lists: this swimlane's own lists.
|
||||
// Also include "shared" / pre-migration lists whose swimlaneId is empty or
|
||||
// null — they existed before the per-swimlane model and should remain
|
||||
// visible in EVERY swimlane as a fallback (matching old shared-list
|
||||
// behaviour) without requiring a database migration.
|
||||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.boardId,
|
||||
swimlaneId: this._id,
|
||||
archived: false
|
||||
$or: [
|
||||
{ swimlaneId: this._id },
|
||||
{ swimlaneId: null }, // null covers null AND missing field
|
||||
{ swimlaneId: '' }, // empty string from old shared-lists era
|
||||
],
|
||||
archived: false,
|
||||
},
|
||||
{ sort: ['sort'] },
|
||||
);
|
||||
},
|
||||
|
||||
// Lists whose swimlaneId references a swimlane that no longer exists
|
||||
// (e.g. the swimlane was deleted after the per-swimlane migration).
|
||||
// These are collected separately so the template can display them in the
|
||||
// first swimlane as a fallback — keeping them visible without touching DB.
|
||||
orphanedSwimlaneLists(validSwimlaneIds) {
|
||||
// validSwimlaneIds: array of _id strings for currently-existing swimlanes.
|
||||
// A list is "orphaned" when its swimlaneId is a non-empty string that is
|
||||
// not among the valid IDs (so it can never appear via myLists()).
|
||||
const excluded = [...validSwimlaneIds, null, ''];
|
||||
return ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.boardId,
|
||||
archived: false,
|
||||
swimlaneId: { $nin: excluded },
|
||||
},
|
||||
{ sort: ['sort'] },
|
||||
);
|
||||
|
||||
Generated
+2844
-602
File diff suppressed because it is too large.
Load diff
+20
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v8.43.0",
|
||||
"version": "v8.78.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"repository": {
|
||||
@@ -12,16 +12,25 @@
|
||||
"url": "https://github.com/wekan/wekan/issues"
|
||||
},
|
||||
"homepage": "https://wekan.fi",
|
||||
"meteor": {
|
||||
"mainModule": {
|
||||
"client": "client/main.js",
|
||||
"server": "server/main.js"
|
||||
},
|
||||
"modern": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.0.1",
|
||||
"@meteorjs/rspack": "^1.0.2",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.9",
|
||||
"@rspack/core": "^1.7.9",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
"css-loader": "^7.1.4",
|
||||
"sinon": "^21.0.3",
|
||||
"style-loader": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1019.0",
|
||||
"@babel/parser": "^7.29.2",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@meteorjs/reify": "^0.25.4",
|
||||
"@rwap/jquery-ui-touch-punch": "^1.0.11",
|
||||
@@ -30,15 +39,17 @@
|
||||
"@textcomplete/core": "^0.1.13",
|
||||
"@textcomplete/textarea": "^0.1.13",
|
||||
"@wekanteam/dragscroll": "^0.0.9",
|
||||
"@wekanteam/exceljs": "^4.5.2",
|
||||
"@wekanteam/exceljs": "^4.7.0",
|
||||
"@wekanteam/html-to-markdown": "^1.0.2",
|
||||
"@wekanteam/meteor-globals": "^1.1.6",
|
||||
"@wekanteam/meteor-reactive-cache": "^1.0.8",
|
||||
"archiver": "^7.0.1",
|
||||
"autosize": "^6.0.1",
|
||||
"bson": "^7.2.0",
|
||||
"dompurify": "^3.3.3",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"filesize": "^11.0.13",
|
||||
"glob": "^13.0.6",
|
||||
"hotkeys-js": "^4.0.2",
|
||||
"i18next": "^25.10.4",
|
||||
"i18next-sprintf-postprocessor": "^0.2.2",
|
||||
@@ -52,15 +63,11 @@
|
||||
"meteor-accounts-t9n": "^2.6.0",
|
||||
"meteor-node-stubs": "^1.2.26",
|
||||
"papaparse": "^5.5.3",
|
||||
"pretty-ms": "^9.3.0"
|
||||
"pretty-ms": "^9.3.0",
|
||||
"underscore": "^1.13.8"
|
||||
},
|
||||
"overrides": {
|
||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.15"
|
||||
},
|
||||
"meteor": {
|
||||
"mainModule": {
|
||||
"client": "client/main.js",
|
||||
"server": "server/main.js"
|
||||
}
|
||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.15",
|
||||
"elliptic": "^6.6.1"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<title>WeKan REST API v8.43</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.43<!-- -->)</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="<p>The REST API allows you to control and extend Wekan with ease.</p>
|
||||
" 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="<p>The REST API allows you to control and extend Wekan with ease.</p>
|
||||
<p>If you are an end-user and not a dev or a tester, <a href="https://github.com/wekan/wekan/issues/new">create an issue</a> to request new APIs.</p>
|
||||
<blockquote>
|
||||
<p>All API calls in the documentation are made using <code>curl</code>. However, you are free to use Java / Python / PHP / Golang / Ruby / Swift / Objective-C / Rust / Scala / C# or any other programming languages.</p>
|
||||
@@ -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.43","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,7 +1,7 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: Wekan REST API
|
||||
version: v8.43
|
||||
version: v8.78
|
||||
description: |
|
||||
The REST API allows you to control and extend Wekan with ease.
|
||||
|
||||
|
||||
+10
-39
@@ -25,14 +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"
|
||||
#softwareupdate --install-rosetta --agree-to-license
|
||||
brew install npm
|
||||
brew install node@22
|
||||
brew install node@24
|
||||
directory_name="~/.npm"
|
||||
if [ ! -d "$directory_name" ]; then
|
||||
mkdir "$directory_name"
|
||||
@@ -70,37 +69,9 @@ do
|
||||
|
||||
"Build Wekan")
|
||||
echo "Building Wekan."
|
||||
#if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# echo "sed at macOS";
|
||||
# sed -i '' 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js
|
||||
#else
|
||||
# echo "sed at ${OSTYPE}"
|
||||
# sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' ~/repos/wekan/packages/meteor-useraccounts-core/package.js
|
||||
#fi
|
||||
#cd ..
|
||||
#sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
|
||||
rm -rf .build/bundle node_modules .meteor/local .build
|
||||
#meteor npm install --production
|
||||
npm install
|
||||
rm -rf node_modules .meteor/local .build
|
||||
meteor npm install --no-audit
|
||||
meteor build .build --directory --platforms=web.browser
|
||||
#rm -rf .build/bundle/programs/web.browser.legacy
|
||||
#(cd .build/bundle/programs/server && rm -rf node_modules && chmod u+w *.json && meteor npm install --production)
|
||||
#(cd .build/bundle/programs/server/node_modules/fibers && node build.js)
|
||||
#(cd .build/bundle/programs/server && npm install fibers --save-dev)
|
||||
#(cd .build/bundle/programs/server/npm/node_modules/meteor/accounts-password && meteor npm remove bcrypt && meteor npm install bcrypt --production)
|
||||
# Cleanup
|
||||
#cd .build/bundle
|
||||
#find . -type d -name '*-garbage*' | xargs rm -rf
|
||||
#find . -name '*phantom*' | xargs rm -rf
|
||||
#find . -name '.*.swp' | xargs rm -f
|
||||
#find . -name '*.swp' | xargs rm -f
|
||||
#cd ../..
|
||||
# Add fibers multi arch
|
||||
#cd .build/bundle/programs/server/node_modules/fibers/bin
|
||||
#curl https://releases.wekan.team/fibers-multi.7z -o fibers-multi.7z
|
||||
#7z x fibers-multi.7z
|
||||
#rm fibers-multi.7z
|
||||
#cd ../../../../../../..
|
||||
echo Done.
|
||||
break
|
||||
;;
|
||||
@@ -110,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
|
||||
;;
|
||||
@@ -120,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
|
||||
;;
|
||||
@@ -130,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
|
||||
;;
|
||||
@@ -147,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
|
||||
;;
|
||||
@@ -164,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
|
||||
;;
|
||||
@@ -181,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
|
||||
;;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# Build WeKan armhf (arm/v7) bundle - Meteor 3 style (no fibers needed).
|
||||
#
|
||||
# This script rebuilds only the native Node.js modules for arm/v7 (armhf).
|
||||
# Note: MongoDB Community Edition is not available for armhf.
|
||||
# Use docker-compose-ferretdb.yml or the snap's built-in FerretDB as the database.
|
||||
# Note: MongoDB Community Edition has no prebuilt armhf binaries.
|
||||
# The WeKan snap bundles qemu-x86_64-static to run the MongoDB amd64 binary on armhf.
|
||||
#
|
||||
# Run on an armhf machine, or use a QEMU arm/v7 environment.
|
||||
#
|
||||
@@ -53,5 +53,5 @@ cd ..
|
||||
zip -r wekan-${VERSION}-armhf.zip bundle
|
||||
|
||||
echo "Done: wekan-${VERSION}-armhf.zip"
|
||||
echo "Note: MongoDB Community is not available for armhf."
|
||||
echo " Use docker-compose-ferretdb.yml or the WeKan snap (built-in FerretDB) as the database."
|
||||
echo "Note: MongoDB Community has no prebuilt armhf binaries."
|
||||
echo " The WeKan snap bundles qemu-x86_64-static to run the MongoDB amd64 binary on armhf."
|
||||
@@ -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
|
||||
|
||||
@@ -9,11 +9,26 @@ 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
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64,linux/s390x \
|
||||
--platform linux/amd64,linux/arm64,linux/armhf,linux/s390x,linux/ppc64le \
|
||||
-t wekanteam/wekan:v${VERSION} \
|
||||
-t wekanteam/wekan:latest \
|
||||
-t quay.io/wekan/wekan:v${VERSION} \
|
||||
@@ -21,3 +36,4 @@ docker buildx build \
|
||||
-t ghcr.io/wekan/wekan:v${VERSION} \
|
||||
-t ghcr.io/wekan/wekan:latest \
|
||||
--push .
|
||||
|
||||
@@ -28,13 +28,7 @@ echo " You can still use any other locale as your main locale."
|
||||
|
||||
echo "Building Wekan."
|
||||
sudo chown -R $(id -u):$(id -g) $HOME/.npm $HOME/.meteor
|
||||
rm -rf node_modules
|
||||
meteor npm install
|
||||
rm -rf .build
|
||||
METEOR_PROFILE=100 meteor build .build --directory --platforms=web.browser
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
rm -rf .build/bundle/programs/web.browser.legacy
|
||||
cd .build/bundle/programs/server
|
||||
rm -rf node_modules
|
||||
meteor npm install
|
||||
cd ../../../..
|
||||
rm -rf .build node_modules .meteor/local
|
||||
meteor npm install --no-audit
|
||||
#METEOR_PROFILE=100 meteor build .build --directory --platforms=web.browser
|
||||
meteor build .build --directory --platforms=web.browser
|
||||
+60
-10
@@ -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 ""
|
||||
|
||||
@@ -100,12 +150,12 @@ echo "=== GitHub Actions is now building and releasing v$NEW ==="
|
||||
echo " Building: amd64 (full Meteor build), arm64, s390x, ppc64le, armhf, win64 bundles"
|
||||
echo " Releasing: GitHub Release with CHANGELOG notes + all .zip bundles"
|
||||
echo " Docker: wekanteam/wekan, quay.io/wekan/wekan, ghcr.io/wekan/wekan"
|
||||
echo " Snap: published to candidate/beta/edge channels + uploaded to release"
|
||||
echo " Snap: published edge, released to candidate"
|
||||
echo " Follow at: https://github.com/wekan/wekan/actions"
|
||||
echo ""
|
||||
|
||||
# ── Step 5: Update website repo (skipped if not present) ─────────────────────
|
||||
WEBSITE_DIR="$HOME/repos/w/wekan.fi"
|
||||
WEBSITE_DIR="../w/wekan.fi"
|
||||
if [ -d "$WEBSITE_DIR" ]; then
|
||||
echo "--- Step 5: Updating website ---"
|
||||
"$RELEASES_DIR/release-website.sh" "$OLD" "$NEW"
|
||||
|
||||
@@ -23,7 +23,14 @@ sedi() {
|
||||
}
|
||||
|
||||
# Go to website directory and pull latest changes
|
||||
cd ~/repos/w/wekan.fi
|
||||
if [ -d "$HOME/repos/w/wekan.fi" ]; then
|
||||
cd "$HOME/repos/w/wekan.fi" || exit 1
|
||||
elif [ -d "$HOME/Documents/repos/w/wekan.fi" ]; then
|
||||
cd "$HOME/Documents/repos/w/wekan.fi" || exit 1
|
||||
else
|
||||
echo "Website directory not found, ignoring."
|
||||
exit 0
|
||||
fi
|
||||
git pull
|
||||
|
||||
# install/index.html
|
||||
@@ -45,7 +52,13 @@ sedi "s|v$OLD\([^0-9]\)|v$NEW\1|g; s|v$OLD$|v$NEW|g" index.html
|
||||
# Create directory for new API docs, copy from WeKan repo, rename entry point
|
||||
cd ..
|
||||
mkdir -p api/v$NEW
|
||||
cp ~/repos/wekan/public/api/* api/v$NEW/
|
||||
if [ -d "$HOME/repos/wekan" ]; then
|
||||
cp "$HOME/repos/wekan/public/api/"* api/v$NEW/
|
||||
elif [ -d "$HOME/Documents/repos/wekan" ]; then
|
||||
cp "$HOME/Documents/repos/wekan/public/api/"* api/v$NEW/
|
||||
else
|
||||
cp public/api/* api/v$NEW/ || true
|
||||
fi
|
||||
mv api/v$NEW/wekan.html api/v$NEW/index.html
|
||||
|
||||
# Commit and push website changes live
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
# Uses HTTP HEAD requests — nothing is downloaded.
|
||||
#
|
||||
# Summary of what the snap bundles per architecture:
|
||||
# amd64, arm64 — MongoDB Community 8.0 (newest) + mongosh + db-tools
|
||||
# armhf, s390x, ppc64el — FerretDB 2.7.0 built from Go 1.24.13 source + PostgreSQL 16
|
||||
# (MongoDB Community Edition has no Community builds for these arches)
|
||||
# amd64, arm64 — MongoDB 7.x prebuilt native binary + mongosh + db-tools
|
||||
# armhf, s390x, ppc64el — MongoDB 7.x amd64 binary run via qemu-x86_64-static
|
||||
# (MongoDB Community Edition has no prebuilt builds for these arches)
|
||||
# Note: on any platform whose CPU lacks instructions required by MongoDB (e.g. AVX),
|
||||
# mongodb-control automatically falls back to running via qemu-x86_64-static.
|
||||
#
|
||||
# Snap base: core24 (Ubuntu 24.04 LTS — core26 is edge-only, not released yet)
|
||||
# Docker base: ubuntu:24.04 (current LTS, support until 2029;
|
||||
@@ -33,63 +35,40 @@ check() {
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== MongoDB Community 8.0.20 (amd64 and arm64 only) ==="
|
||||
check "MongoDB 8.0.20 amd64 (x86_64, ubuntu2404)" \
|
||||
"https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2404-8.0.20.tgz"
|
||||
check "MongoDB 8.0.20 arm64 (aarch64, ubuntu2404)" \
|
||||
"https://fastdl.mongodb.org/linux/mongodb-linux-aarch64-ubuntu2404-8.0.20.tgz"
|
||||
echo " NOTE MongoDB Community has never published builds for s390x, ppc64le, or arm/v7"
|
||||
echo " (those are Enterprise-only). FerretDB 2 + PostgreSQL is used instead."
|
||||
echo "=== MongoDB 7.0.14 (amd64 and arm64 native; amd64 also used via QEMU for armhf/s390x/ppc64el) ==="
|
||||
check "MongoDB 7.0.14 amd64 (x86_64, ubuntu2204)" \
|
||||
"https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.14.tgz"
|
||||
check "MongoDB 7.0.14 arm64 (aarch64, ubuntu2204)" \
|
||||
"https://fastdl.mongodb.org/linux/mongodb-linux-aarch64-ubuntu2204-7.0.14.tgz"
|
||||
echo " NOTE MongoDB Community has no prebuilt builds for s390x, ppc64le, or arm/v7."
|
||||
echo " The amd64 binary above is also used on those arches via qemu-x86_64-static."
|
||||
echo ""
|
||||
|
||||
echo "=== mongosh 2.8.2 (amd64 and arm64) ==="
|
||||
check "mongosh 2.8.2 amd64 (x64)" \
|
||||
echo "=== mongosh 2.8.2 (x64 for amd64/armhf/s390x/ppc64el via QEMU; arm64 native) ==="
|
||||
check "mongosh 2.8.2 x64 (amd64 + armhf/s390x/ppc64el via QEMU)" \
|
||||
"https://downloads.mongodb.com/compass/mongosh-2.8.2-linux-x64.tgz"
|
||||
check "mongosh 2.8.2 arm64" \
|
||||
"https://downloads.mongodb.com/compass/mongosh-2.8.2-linux-arm64.tgz"
|
||||
echo " NOTE mongosh is not published for s390x, ppc64le, or armhf"
|
||||
echo ""
|
||||
|
||||
echo "=== mongodb-database-tools 100.13.0 (amd64 and arm64) ==="
|
||||
check "db-tools 100.13.0 amd64 (x86_64, ubuntu2404)" \
|
||||
echo "=== mongodb-database-tools 100.13.0 (x86_64 for amd64/armhf/s390x/ppc64el; arm64 native) ==="
|
||||
check "db-tools 100.13.0 x86_64 (amd64 + armhf/s390x/ppc64el via QEMU)" \
|
||||
"https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2404-x86_64-100.13.0.tgz"
|
||||
check "db-tools 100.13.0 arm64 (ubuntu2404)" \
|
||||
check "db-tools 100.13.0 arm64" \
|
||||
"https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2404-arm64-100.13.0.tgz"
|
||||
echo " NOTE db-tools is not published for s390x, ppc64le, or armhf"
|
||||
echo ""
|
||||
|
||||
echo "=== Go 1.24.13 toolchain (used to build FerretDB 2 on armhf/s390x/ppc64le) ==="
|
||||
check "Go 1.24.13 linux-armv6l (armhf snap builder)" \
|
||||
"https://dl.google.com/go/go1.24.13.linux-armv6l.tar.gz"
|
||||
check "Go 1.24.13 linux-s390x (s390x snap builder)" \
|
||||
"https://dl.google.com/go/go1.24.13.linux-s390x.tar.gz"
|
||||
check "Go 1.24.13 linux-ppc64le (ppc64el snap builder)" \
|
||||
"https://dl.google.com/go/go1.24.13.linux-ppc64le.tar.gz"
|
||||
echo ""
|
||||
|
||||
echo "=== FerretDB 2.7.0 Go module (built from source via Go proxy) ==="
|
||||
FERRETDB_MOD="https://proxy.golang.org/github.com/!ferret!d!b/!ferret!d!b/v2/@v/v2.7.0.info"
|
||||
HTTP=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 20 "$FERRETDB_MOD")
|
||||
if [ "$HTTP" = "200" ]; then
|
||||
printf " OK FerretDB v2/cmd/ferretdb@v2.7.0 available on Go proxy\n"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
printf " FAIL [HTTP %s] FerretDB v2.7.0 not found on Go proxy\n %s\n" "$HTTP" "$FERRETDB_MOD"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
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 ==="
|
||||
|
||||
@@ -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."
|
||||
@@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
|
||||
appTitle = (defaultText = "Wekan"),
|
||||
# The name of the app as it is displayed to the user.
|
||||
|
||||
appVersion = 843,
|
||||
appVersion = 878,
|
||||
# Increment this for every release.
|
||||
|
||||
appMarketingVersion = (defaultText = "8.43.0~2026-03-23"),
|
||||
appMarketingVersion = (defaultText = "8.78.0~2026-03-23"),
|
||||
# Human-readable presentation of the app version.
|
||||
|
||||
minUpgradableAppVersion = 0,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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' },
|
||||
{
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
// Loaded by server/main.js after bootstrap (collectionHelpers) completes.
|
||||
// ============================================================================
|
||||
|
||||
// ****IMPORTANT**** Initialize upload directories BEFORE models are loaded
|
||||
// This ensures ostrio:files can create necessary directories without permission errors
|
||||
import '/server/initializeDirs';
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 0. API middleware & auth routes (must register before model routes)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// ============================================================================
|
||||
// Initialize Upload Directories
|
||||
// Runs at server startup before models are loaded to ensure ostrio:files
|
||||
// can create necessary upload directories without permission errors.
|
||||
// ============================================================================
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Compute upload subdirectory path:
|
||||
// - On Docker (WRITABLE_PATH=/data): /data/files/attachments, /data/files/avatars
|
||||
// - On Snap (WRITABLE_PATH=$SNAP_COMMON/files): $SNAP_COMMON/files/attachments, $SNAP_COMMON/files/avatars
|
||||
// - On dev (WRITABLE_PATH=..): ../files/attachments, ../files/avatars
|
||||
//
|
||||
// Snap already appends '/files' to WRITABLE_PATH, so we detect and handle that case.
|
||||
function getUploadPaths() {
|
||||
const basePath = process.env.WRITABLE_PATH || process.cwd();
|
||||
|
||||
// Check if basePath already ends with '/files' (Snap case)
|
||||
const endsWithFiles = basePath.endsWith('/files') || basePath.endsWith('\\files');
|
||||
|
||||
if (endsWithFiles) {
|
||||
// Snap: WRITABLE_PATH is already $SNAP_COMMON/files, so append subdirectories directly
|
||||
return {
|
||||
attachments: path.join(basePath, 'attachments'),
|
||||
avatars: path.join(basePath, 'avatars'),
|
||||
};
|
||||
} else {
|
||||
// Docker & Dev: WRITABLE_PATH is /data or .., so append /files/{subdir}
|
||||
return {
|
||||
attachments: path.join(basePath, 'files', 'attachments'),
|
||||
avatars: path.join(basePath, 'files', 'avatars'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const uploadPaths = getUploadPaths();
|
||||
const dirsToCreate = [uploadPaths.attachments, uploadPaths.avatars];
|
||||
|
||||
dirsToCreate.forEach(dirPath => {
|
||||
try {
|
||||
// Create directory recursively with proper permissions (0o755)
|
||||
// Ignore error if it already exists
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true, mode: 0o755 });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.info(`[initializeDirs] Created directory: ${dirPath}`);
|
||||
}
|
||||
} else {
|
||||
// Verify the directory is writable
|
||||
try {
|
||||
fs.accessSync(dirPath, fs.constants.W_OK);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.info(`[initializeDirs] Directory exists and is writable: ${dirPath}`);
|
||||
}
|
||||
} catch (accessError) {
|
||||
console.warn(`[initializeDirs] WARNING: Directory exists but may not be writable: ${dirPath}`, accessError.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[initializeDirs] FAILED to initialize directory: ${dirPath}`, error.message);
|
||||
throw new Error(`Cannot create or access upload directory: ${dirPath}\n${error.message}`);
|
||||
}
|
||||
});
|
||||
@@ -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'] },
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -177,10 +177,10 @@ Meteor.methods({
|
||||
},
|
||||
|
||||
async getOrgsCollectionCount(query = {}) {
|
||||
check(query, Match.OneOf(Object, null, undefined));
|
||||
if (!(await ReactiveCache.getCurrentUser())?.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
check(query, Match.OneOf(Object, null, undefined));
|
||||
const cursor = await ReactiveCache.getOrgs(query || {}, {}, true);
|
||||
return typeof cursor.countAsync === 'function' ? await cursor.countAsync() : cursor.count();
|
||||
},
|
||||
|
||||
@@ -155,10 +155,10 @@ Meteor.methods({
|
||||
},
|
||||
|
||||
async getTeamsCollectionCount(query = {}) {
|
||||
check(query, Match.OneOf(Object, null, undefined));
|
||||
if (!(await ReactiveCache.getCurrentUser())?.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
check(query, Match.OneOf(Object, null, undefined));
|
||||
const cursor = await ReactiveCache.getTeams(query || {}, {}, true);
|
||||
return typeof cursor.countAsync === 'function' ? await cursor.countAsync() : cursor.count();
|
||||
},
|
||||
|
||||
+14
-1
@@ -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()) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Attachments from '/models/attachments';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
|
||||
Meteor.publish('attachmentsList', async function(limit) {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
+59
-157
@@ -40,7 +40,6 @@ import {
|
||||
OPERATOR_COMMENT,
|
||||
OPERATOR_CREATED_AT,
|
||||
OPERATOR_CREATOR,
|
||||
OPERATOR_DEBUG,
|
||||
OPERATOR_DUE,
|
||||
OPERATOR_HAS,
|
||||
OPERATOR_LABEL,
|
||||
@@ -80,10 +79,10 @@ import { CARD_TYPES } from '../../config/const';
|
||||
import Org from "../../models/org";
|
||||
import Team from "../../models/team";
|
||||
|
||||
Meteor.publish('card', async cardId => {
|
||||
Meteor.publish('card', async function(cardId) {
|
||||
check(cardId, String);
|
||||
|
||||
const userId = Meteor.userId();
|
||||
const userId = this.userId;
|
||||
const card = await ReactiveCache.getCard({ _id: cardId });
|
||||
|
||||
if (!card || !card.boardId) {
|
||||
@@ -233,14 +232,14 @@ Meteor.publish('myCards', async function(sessionId) {
|
||||
queryParams.addPredicate(OPERATOR_USER, (await ReactiveCache.getCurrentUser()).username);
|
||||
queryParams.setPredicate(OPERATOR_LIMIT, 200);
|
||||
|
||||
const query = buildQuery(queryParams);
|
||||
const query = Promise.await(buildQuery(queryParams, this.userId));
|
||||
query.projection.sort = {
|
||||
boardId: 1,
|
||||
swimlaneId: 1,
|
||||
listId: 1,
|
||||
};
|
||||
|
||||
const ret = findCards(sessionId, query);
|
||||
const ret = await findCards(sessionId, query, this.userId);
|
||||
return ret;
|
||||
});
|
||||
|
||||
@@ -253,10 +252,6 @@ Meteor.publish('dueCards', async function(allUsers = false) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards publication called for user:', userId, 'allUsers:', allUsers);
|
||||
}
|
||||
|
||||
// Get user's board memberships for efficient filtering
|
||||
const userBoards = (await ReactiveCache.getBoards({
|
||||
$or: [
|
||||
@@ -265,24 +260,7 @@ Meteor.publish('dueCards', async function(allUsers = false) {
|
||||
]
|
||||
})).map(board => board._id);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards userBoards:', userBoards);
|
||||
console.log('dueCards userBoards count:', userBoards.length);
|
||||
|
||||
// Also check if there are any cards with due dates in the system at all
|
||||
const cursor = Cards.find({
|
||||
type: 'cardType-card',
|
||||
archived: false,
|
||||
dueAt: { $exists: true, $nin: [null, ''] }
|
||||
});
|
||||
const allCardsWithDueDates = typeof cursor.countAsync === 'function' ? await cursor.countAsync() : cursor.count();
|
||||
console.log('dueCards: total cards with due dates in system:', allCardsWithDueDates);
|
||||
}
|
||||
|
||||
if (userBoards.length === 0) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: No boards found for user, returning ready');
|
||||
}
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
@@ -320,27 +298,8 @@ Meteor.publish('dueCards', async function(allUsers = false) {
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards selector:', JSON.stringify(selector, null, 2));
|
||||
console.log('dueCards options:', JSON.stringify(options, null, 2));
|
||||
}
|
||||
|
||||
const result = Cards.find(selector, options);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
const count = typeof result.countAsync === 'function' ? await result.countAsync() : result.count();
|
||||
console.log('dueCards publication: returning', count, 'cards');
|
||||
if (count > 0) {
|
||||
const sampleCards = (typeof result.fetchAsync === 'function' ? await result.fetchAsync() : result.fetch()).slice(0, 3);
|
||||
console.log('dueCards publication: sample cards:', sampleCards.map(c => ({
|
||||
id: c._id,
|
||||
title: c.title,
|
||||
dueAt: c.dueAt,
|
||||
boardId: c.boardId
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -349,44 +308,46 @@ Meteor.publish('globalSearch', async function(sessionId, params, text) {
|
||||
check(params, Object);
|
||||
check(text, String);
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('globalSearch publication called with:', { sessionId, params, text });
|
||||
const ret = await findCards(sessionId, await buildQuery(new QueryParams(params, text), this.userId), this.userId);
|
||||
|
||||
if (!Array.isArray(ret)) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const ret = findCards(sessionId, await buildQuery(new QueryParams(params, text)));
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('globalSearch publication returning:', ret);
|
||||
// Check if last item is the sessiondata marker
|
||||
let sessionDataToPublish = null;
|
||||
if (ret.length > 0 && ret[ret.length - 1] && ret[ret.length - 1]._sessionDataToFetch) {
|
||||
sessionDataToPublish = ret[ret.length - 1]._sessionDataToFetch;
|
||||
ret.pop(); // Remove the marker
|
||||
}
|
||||
|
||||
// Manually publish sessiondata if it exists
|
||||
if (sessionDataToPublish) {
|
||||
this.added('sessiondata', sessionDataToPublish._id, sessionDataToPublish);
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
Meteor.publish('sessionData', function(sessionId) {
|
||||
Meteor.publish('sessionData', async function(sessionId) {
|
||||
check(sessionId, String);
|
||||
const userId = Meteor.userId();
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('sessionData publication called with:', { sessionId, userId });
|
||||
const userId = this.userId;
|
||||
|
||||
if (!userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return the cursor immediately - data should already be in the collection
|
||||
// from the globalSearch publication or earlier searches
|
||||
const cursor = SessionData.find({ userId, sessionId });
|
||||
const countPromise = typeof cursor.countAsync === 'function' ? cursor.countAsync() : Promise.resolve(cursor.count());
|
||||
if (process.env.DEBUG === 'true') {
|
||||
countPromise.then(count => {
|
||||
console.log('sessionData publication returning cursor with count:', count);
|
||||
});
|
||||
}
|
||||
return cursor;
|
||||
});
|
||||
|
||||
async function buildSelector(queryParams) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
async function buildSelector(queryParams, userId) {
|
||||
const errors = new QueryErrors();
|
||||
|
||||
let selector = {};
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('queryParams:', queryParams);
|
||||
|
||||
if (queryParams.selector) {
|
||||
selector = queryParams.selector;
|
||||
} else {
|
||||
@@ -479,15 +440,12 @@ async function buildSelector(queryParams) {
|
||||
selector.boardId = {
|
||||
$in: await Boards.userBoardIds(userId, false, boardsSelector),
|
||||
};
|
||||
selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
|
||||
selector.listId = { $nin: Lists.archivedListIds() };
|
||||
selector.swimlaneId = { $nin: await Swimlanes.archivedSwimlaneIds() };
|
||||
selector.listId = { $nin: await Lists.archivedListIds() };
|
||||
selector.archived = false;
|
||||
}
|
||||
} else {
|
||||
const userBoardIds = await Boards.userBoardIds(userId, null, boardsSelector);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('buildSelector - userBoardIds:', userBoardIds);
|
||||
}
|
||||
selector.boardId = {
|
||||
$in: userBoardIds,
|
||||
};
|
||||
@@ -662,8 +620,6 @@ async function buildSelector(queryParams) {
|
||||
}
|
||||
}
|
||||
if (queryLabels.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('queryLabels:', queryLabels);
|
||||
selector.labelIds = { $in: [...new Set(queryLabels)] };
|
||||
}
|
||||
}
|
||||
@@ -805,10 +761,6 @@ async function buildSelector(queryParams) {
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('buildSelector - final selector:', JSON.stringify(selector, null, 2));
|
||||
}
|
||||
|
||||
const query = new Query();
|
||||
query.selector = selector;
|
||||
query.setQueryParams(queryParams);
|
||||
@@ -818,8 +770,7 @@ async function buildSelector(queryParams) {
|
||||
}
|
||||
|
||||
function buildProjection(query) {
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('query:', query);
|
||||
|
||||
let skip = 0;
|
||||
if (query.getQueryParams().skip) {
|
||||
skip = query.getQueryParams().skip;
|
||||
@@ -913,16 +864,15 @@ function buildProjection(query) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('projection:', projection);
|
||||
|
||||
|
||||
query.projection = projection;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
async function buildQuery(queryParams) {
|
||||
const query = await buildSelector(queryParams);
|
||||
async function buildQuery(queryParams, userId) {
|
||||
const query = await buildSelector(queryParams, userId);
|
||||
|
||||
return buildProjection(query);
|
||||
}
|
||||
@@ -932,16 +882,15 @@ Meteor.publish('brokenCards', async function(sessionId) {
|
||||
|
||||
const params = new QueryParams();
|
||||
params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL);
|
||||
const query = await buildQuery(params);
|
||||
const query = await buildQuery(params, this.userId);
|
||||
query.selector.$or = [
|
||||
{ boardId: { $in: [null, ''] } },
|
||||
{ swimlaneId: { $in: [null, ''] } },
|
||||
{ listId: { $in: [null, ''] } },
|
||||
{ type: { $nin: CARD_TYPES } },
|
||||
];
|
||||
// console.log('brokenCards selector:', query.selector);
|
||||
|
||||
const ret = findCards(sessionId, query);
|
||||
const ret = await findCards(sessionId, query, this.userId);
|
||||
return ret;
|
||||
});
|
||||
|
||||
@@ -952,7 +901,7 @@ Meteor.publish('nextPage', async function(sessionId) {
|
||||
const projection = session.getProjection();
|
||||
projection.skip = session.lastHit;
|
||||
|
||||
const ret = findCards(sessionId, new Query(session.getSelector(), projection));
|
||||
const ret = await findCards(sessionId, new Query(session.getSelector(), projection), this.userId);
|
||||
return ret;
|
||||
});
|
||||
|
||||
@@ -963,21 +912,11 @@ Meteor.publish('previousPage', async function(sessionId) {
|
||||
const projection = session.getProjection();
|
||||
projection.skip = session.lastHit - session.resultsCount - projection.limit;
|
||||
|
||||
const ret = findCards(sessionId, new Query(session.getSelector(), projection));
|
||||
const ret = await findCards(sessionId, new Query(session.getSelector(), projection), this.userId);
|
||||
return ret;
|
||||
});
|
||||
|
||||
async function findCards(sessionId, query) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - userId:', userId);
|
||||
console.log('findCards - selector:', JSON.stringify(query.selector, null, 2));
|
||||
console.log('findCards - selector.$and:', query.selector.$and);
|
||||
console.log('findCards - projection:', query.projection);
|
||||
}
|
||||
|
||||
async function findCards(sessionId, query, userId) {
|
||||
let textMatches = query.getQueryParams().text;
|
||||
let isTextSearch = !!textMatches;
|
||||
let dbProjection = query.projection;
|
||||
@@ -1014,9 +953,7 @@ async function findCards(sessionId, query) {
|
||||
cards = await ReactiveCache.getCards({ _id: { $in: orderedIds } }, { fields: query.projection.fields }, true);
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - cards count:', totalCardsCount);
|
||||
}
|
||||
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
@@ -1027,12 +964,11 @@ async function findCards(sessionId, query) {
|
||||
selector: SessionData.pickle(query.selector),
|
||||
projection: SessionData.pickle(query.projection),
|
||||
errors: query.errors(),
|
||||
debug: query.getQueryParams().getPredicate(OPERATOR_DEBUG),
|
||||
modifiedAt: new Date()
|
||||
},
|
||||
};
|
||||
|
||||
if (cards) {
|
||||
if (cards && totalCardsCount > 0) {
|
||||
update.$set.totalHits = totalCardsCount;
|
||||
update.$set.lastHit =
|
||||
query.projection.skip + query.projection.limit < totalCardsCount
|
||||
@@ -1043,27 +979,17 @@ async function findCards(sessionId, query) {
|
||||
if (isTextSearch) {
|
||||
update.$set.cards = orderedIds;
|
||||
} else {
|
||||
update.$set.cards = cards.map(card => card._id);
|
||||
const cardArray = typeof cards.fetchAsync === 'function' ? await cards.fetchAsync() : cards.fetch();
|
||||
update.$set.cards = cardArray.map(card => card._id);
|
||||
}
|
||||
update.$set.resultsCount = update.$set.cards.length;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - sessionId:', sessionId);
|
||||
console.log('findCards - userId:', userId);
|
||||
console.log('findCards - update:', JSON.stringify(update, null, 2));
|
||||
}
|
||||
const upsertResult = SessionData.upsert({ userId, sessionId }, update);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - upsertResult:', upsertResult);
|
||||
}
|
||||
|
||||
const upsertResult = typeof SessionData.upsertAsync === 'function' ? await SessionData.upsertAsync({ userId, sessionId }, update) : SessionData.upsert({ userId, sessionId }, update);
|
||||
|
||||
// Check if the session data was actually stored
|
||||
const storedSessionData = typeof SessionData.findOneAsync === 'function' ? await SessionData.findOneAsync({ userId, sessionId }) : SessionData.findOne({ userId, sessionId });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - stored session data:', storedSessionData);
|
||||
console.log('findCards - stored session data count:', storedSessionData ? 1 : 0);
|
||||
}
|
||||
|
||||
// remove old session data
|
||||
const removeSelector = {
|
||||
@@ -1080,14 +1006,17 @@ async function findCards(sessionId, query) {
|
||||
SessionData.remove(removeSelector);
|
||||
}
|
||||
|
||||
if (cards) {
|
||||
if (cards && totalCardsCount > 0) {
|
||||
const boards = [];
|
||||
const swimlanes = [];
|
||||
const lists = [];
|
||||
const customFieldIds = [];
|
||||
const users = [this.userId];
|
||||
const users = [userId];
|
||||
|
||||
cards.forEach(card => {
|
||||
const cardArray = typeof cards.fetchAsync === 'function' ? await cards.fetchAsync() : cards.fetch();
|
||||
const cardIds = cardArray.map(c => c._id);
|
||||
|
||||
cardArray.forEach(card => {
|
||||
if (card.boardId) boards.push(card.boardId);
|
||||
if (card.swimlaneId) swimlanes.push(card.swimlaneId);
|
||||
if (card.listId) lists.push(card.listId);
|
||||
@@ -1119,27 +1048,7 @@ async function findCards(sessionId, query) {
|
||||
type: 1,
|
||||
};
|
||||
|
||||
// Add a small delay to ensure the session data is committed to the database
|
||||
Meteor.setTimeout(() => {
|
||||
const sessionDataCursor = SessionData.find({ userId, sessionId });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - publishing session data cursor (after delay):', sessionDataCursor);
|
||||
const countPromise = typeof sessionDataCursor.countAsync === 'function' ? sessionDataCursor.countAsync() : Promise.resolve(sessionDataCursor.count());
|
||||
countPromise.then(count => {
|
||||
console.log('findCards - session data count (after delay):', count);
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const sessionDataCursor = SessionData.find({ userId, sessionId });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - publishing session data cursor:', sessionDataCursor);
|
||||
const countPromise = typeof sessionDataCursor.countAsync === 'function' ? sessionDataCursor.countAsync() : Promise.resolve(sessionDataCursor.count());
|
||||
countPromise.then(count => {
|
||||
console.log('findCards - session data count:', count);
|
||||
});
|
||||
}
|
||||
|
||||
// Return all cursors except sessiondata - we'll add sessiondata separately after fetch
|
||||
return [
|
||||
cards,
|
||||
await ReactiveCache.getBoards(
|
||||
@@ -1155,21 +1064,14 @@ async function findCards(sessionId, query) {
|
||||
await ReactiveCache.getLists({ _id: { $in: lists } }, { fields: { ...fields, color: 1 } }, true),
|
||||
await ReactiveCache.getCustomFields({ _id: { $in: customFieldIds } }, {}, true),
|
||||
await ReactiveCache.getUsers({ _id: { $in: users } }, { fields: Users.safeFields }, true),
|
||||
await ReactiveCache.getChecklists({ cardId: { $in: cards.map(c => c._id) } }, {}, true),
|
||||
await ReactiveCache.getChecklistItems({ cardId: { $in: cards.map(c => c._id) } }, {}, true),
|
||||
(await ReactiveCache.getAttachments({ 'meta.cardId': { $in: cards.map(c => c._id) } }, {}, true)).cursor,
|
||||
await ReactiveCache.getCardComments({ cardId: { $in: cards.map(c => c._id) } }, {}, true),
|
||||
sessionDataCursor,
|
||||
await ReactiveCache.getChecklists({ cardId: { $in: cardIds } }, {}, true),
|
||||
await ReactiveCache.getChecklistItems({ cardId: { $in: cardIds } }, {}, true),
|
||||
(await ReactiveCache.getAttachments({ 'meta.cardId': { $in: cardIds } }, {}, true)).cursor,
|
||||
await ReactiveCache.getCardComments({ cardId: { $in: cardIds } }, {}, true),
|
||||
{ _sessionDataToFetch: storedSessionData }, // Pass sessiondata separately
|
||||
];
|
||||
} else {
|
||||
// Return just a marker for sessiondata
|
||||
return [{ _sessionDataToFetch: storedSessionData }];
|
||||
}
|
||||
|
||||
const sessionDataCursor = SessionData.find({ userId, sessionId });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - publishing session data cursor (no cards):', sessionDataCursor);
|
||||
const countPromise = typeof sessionDataCursor.countAsync === 'function' ? sessionDataCursor.countAsync() : Promise.resolve(sessionDataCursor.count());
|
||||
countPromise.then(count => {
|
||||
console.log('findCards - session data count (no cards):', count);
|
||||
});
|
||||
}
|
||||
return [sessionDataCursor];
|
||||
}
|
||||
@@ -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,4 +1,5 @@
|
||||
import Users from '/models/users';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
|
||||
Meteor.publish('user-miniprofile', async function (usernames) {
|
||||
check(usernames, Array);
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
+143
-268
@@ -1,297 +1,172 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WeKan Database Control Script
|
||||
#
|
||||
# All snap platforms now use FerretDB 2 + PostgreSQL 16.
|
||||
# bin/mongod is a wrapper that initialises PostgreSQL and starts FerretDB.
|
||||
#
|
||||
# On first start after upgrading from an old snap that bundled MongoDB:
|
||||
# 1. Detects WiredTiger files at SNAP_COMMON (legacy MongoDB data).
|
||||
# 2. Temporarily starts the previous snap revision's mongod on port 27020.
|
||||
# 3. Dumps all data with mongodump.
|
||||
# 4. Stops old mongod.
|
||||
# 5. Starts FerretDB + PostgreSQL via bin/mongod.
|
||||
# 6. Restores data to FerretDB with mongorestore.
|
||||
# 7. Applies schema v8.43 upgrades (swimlaneId, orphan cards, file URLs).
|
||||
# 8. Migrates CollectionFS / GridFS binary files to
|
||||
# $SNAP_COMMON/files/attachments/ and $SNAP_COMMON/files/avatars/.
|
||||
# 9. Renames WiredTiger → WiredTiger.migrated so the step is never repeated.
|
||||
# MongoDB Control Script
|
||||
# Starts MongoDB 7.x server only
|
||||
|
||||
# ── Read snap settings ──────────────────────────────────────────────────────
|
||||
# get wekan/mongo settings
|
||||
echo "Reading snap settings..."
|
||||
source "$SNAP/bin/wekan-read-settings"
|
||||
source $SNAP/bin/wekan-read-settings
|
||||
|
||||
# Debug: Show what we got from snap settings
|
||||
echo "Snap settings loaded:"
|
||||
echo " MONGODB_BIND_IP: '${MONGODB_BIND_IP}'"
|
||||
echo " MONGODB_PORT: '${MONGODB_PORT}'"
|
||||
echo " MONGODB_BIND_IP: '${MONGODB_BIND_IP}'"
|
||||
echo " MONGODB_PORT: '${MONGODB_PORT}'"
|
||||
echo " MONGODB_BIND_UNIX_SOCKET: '${MONGODB_BIND_UNIX_SOCKET}'"
|
||||
|
||||
if [ "true" = "${DISABLE_MONGODB}" ]; then
|
||||
echo "Database service is disabled. Stopping."
|
||||
snapctl stop --disable "${SNAP_NAME}.mongodb"
|
||||
# Debug: Check snap settings directly
|
||||
echo "Direct snap settings check:"
|
||||
echo " mongodb-port: $(snapctl get mongodb-port 2>/dev/null || echo 'not set')"
|
||||
echo " mongodb-bind-ip: $(snapctl get mongodb-bind-ip 2>/dev/null || echo 'not set')"
|
||||
echo " mongodb-bind-unix-socket: $(snapctl get mongodb-bind-unix-socket 2>/dev/null || echo 'not set')"
|
||||
|
||||
if [ "true" == "${DISABLE_MONGODB}" ]; then
|
||||
echo "mongodb is disabled. Stop service"
|
||||
snapctl stop --disable ${SNAP_NAME}.mongodb
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Locale / path setup ─────────────────────────────────────────────────────
|
||||
[ -z "${LANG}" ] && export LANG=en_US.UTF-8
|
||||
# make sure we have set minimum env variables for locale
|
||||
if [ -z "${LANG}" ]; then
|
||||
export LANG=en_US.UTF-8
|
||||
fi
|
||||
|
||||
export LC_ALL=C
|
||||
export PATH="/snap/${SNAP_NAME}/current/bin:/snap/${SNAP_NAME}/current/usr/bin:${PATH}"
|
||||
export LD_LIBRARY_PATH="/snap/${SNAP_NAME}/current/lib:/snap/${SNAP_NAME}/current/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
|
||||
# If CPU does not support required instructions (AVX for x86_64, ARMv8.2-A for arm64),
|
||||
# migratemongo/avx wrapper scripts transparently re-run MongoDB commands via QEMU.
|
||||
# See https://github.com/wekan/migratemongo
|
||||
export PATH=/snap/${SNAP_NAME}/current/migratemongo/avx:/snap/${SNAP_NAME}/current/usr/bin:/snap/${SNAP_NAME}/current/bin:${PATH}
|
||||
export LD_LIBRARY_PATH=/snap/${SNAP_NAME}/current/lib:/snap/${SNAP_NAME}/current/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}
|
||||
|
||||
# Remove stale settings log
|
||||
[ -f "${SNAP_COMMON}/settings.log" ] && rm "${SNAP_COMMON}/settings.log"
|
||||
# If temporary settings log exists, delete it
|
||||
if [ -f ${SNAP_COMMON}/settings.log ]; then
|
||||
rm ${SNAP_COMMON}/settings.log
|
||||
fi
|
||||
|
||||
# ── Effective port ──────────────────────────────────────────────────────────
|
||||
FERRET_PORT="${MONGODB_PORT:-27017}"
|
||||
# Set MongoDB log destination to snapcommon for log file detection
|
||||
export MONGO_LOG_DESTINATION="snapcommon"
|
||||
|
||||
# Prevent multiple instances
|
||||
check_port_in_use() {
|
||||
if netstat -tuln 2>/dev/null | grep -q ":${FERRET_PORT} "; then
|
||||
# Set MongoDB data directory
|
||||
export MONGO_DATA_DIR="${SNAP_COMMON}"
|
||||
|
||||
# Create MongoDB data directory if it doesn't exist
|
||||
if [ ! -d "$MONGO_DATA_DIR" ]; then
|
||||
mkdir -p "$MONGO_DATA_DIR"
|
||||
chmod 755 "$MONGO_DATA_DIR"
|
||||
fi
|
||||
|
||||
# Set MongoDB log file path
|
||||
export MONGO_LOG_FILE="${SNAP_COMMON}/mongodb.log"
|
||||
|
||||
# Build bind options from snap settings
|
||||
BIND_OPTIONS=""
|
||||
if [ "nill" != "${MONGODB_BIND_UNIX_SOCKET}" ] && [ -n "${MONGODB_BIND_UNIX_SOCKET}" ]; then
|
||||
BIND_OPTIONS+=" --unixSocketPrefix ${MONGODB_BIND_UNIX_SOCKET}"
|
||||
fi
|
||||
if [ -n "${MONGODB_BIND_IP}" ]; then
|
||||
BIND_OPTIONS+=" --bind_ip ${MONGODB_BIND_IP}"
|
||||
else
|
||||
BIND_OPTIONS+=" --bind_ip 127.0.0.1"
|
||||
fi
|
||||
if [ -n "${MONGODB_PORT}" ]; then
|
||||
BIND_OPTIONS+=" --port ${MONGODB_PORT}"
|
||||
else
|
||||
BIND_OPTIONS+=" --port 27019"
|
||||
fi
|
||||
|
||||
# Debug: Show what settings we're using
|
||||
echo "MongoDB settings:"
|
||||
echo " MONGODB_BIND_IP: ${MONGODB_BIND_IP:-127.0.0.1}"
|
||||
echo " MONGODB_PORT: ${MONGODB_PORT:-27017}"
|
||||
echo " MONGODB_BIND_UNIX_SOCKET: ${MONGODB_BIND_UNIX_SOCKET:-not set}"
|
||||
echo " BIND_OPTIONS: ${BIND_OPTIONS}"
|
||||
|
||||
# Check if MongoDB is already running
|
||||
check_mongodb_running() {
|
||||
local port="${MONGODB_PORT:-27017}"
|
||||
|
||||
# Check if MongoDB is already running on the configured port
|
||||
if netstat -tuln 2>/dev/null | grep -q ":${port} "; then
|
||||
echo "MongoDB is already running on port ${port}"
|
||||
return 0
|
||||
fi
|
||||
if command -v lsof >/dev/null 2>&1 && lsof -i ":${FERRET_PORT}" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if [ -f "${SNAP_COMMON}/ferretdb.pid" ]; then
|
||||
local pid
|
||||
pid=$(cat "${SNAP_COMMON}/ferretdb.pid" 2>/dev/null)
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
|
||||
# Alternative check using lsof if netstat is not available
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
if lsof -i ":${port}" >/dev/null 2>&1; then
|
||||
echo "MongoDB is already running on port ${port} (detected via lsof)"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${SNAP_COMMON}/ferretdb.pid"
|
||||
fi
|
||||
|
||||
# Check if there's a PID file and the process is still running
|
||||
if [ -f "${SNAP_COMMON}/mongodb.pid" ]; then
|
||||
local pid=$(cat "${SNAP_COMMON}/mongodb.pid" 2>/dev/null)
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
echo "MongoDB is already running (PID: $pid)"
|
||||
return 0
|
||||
else
|
||||
# Remove stale PID file
|
||||
rm -f "${SNAP_COMMON}/mongodb.pid"
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if check_port_in_use; then
|
||||
echo "Database already running on port ${FERRET_PORT}. Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# PID-file cleanup on exit
|
||||
cleanup() { rm -f "${SNAP_COMMON}/ferretdb.pid"; }
|
||||
# Cleanup function to remove PID file on exit
|
||||
cleanup() {
|
||||
rm -f "${SNAP_COMMON}/mongodb.pid"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ── Helper: wait for mongod-compatible port ─────────────────────────────────
|
||||
wait_for_mongo_port() {
|
||||
local port="$1"
|
||||
local label="$2"
|
||||
echo "Waiting for ${label} on port ${port}..."
|
||||
for i in $(seq 1 90); do
|
||||
if "/snap/${SNAP_NAME}/current/bin/mongosh" \
|
||||
--quiet --port "$port" \
|
||||
--eval "db.adminCommand('ping')" >/dev/null 2>&1; then
|
||||
echo "${label} is ready."
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "ERROR: ${label} did not become ready within 90 s." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Migration: MongoDB WiredTiger → FerretDB ────────────────────────────────
|
||||
WIREDTIGER_MARKER="${SNAP_COMMON}/WiredTiger"
|
||||
MIGRATION_DONE_MARKER="${SNAP_COMMON}/WiredTiger.migrated"
|
||||
DUMP_DIR="${SNAP_COMMON}/_migration_dump"
|
||||
MIGRATION_LOG="${SNAP_COMMON}/migration.log"
|
||||
ATTACH_DIR="${SNAP_COMMON}/files/attachments"
|
||||
AVATAR_DIR="${SNAP_COMMON}/files/avatars"
|
||||
|
||||
run_mongodb_to_ferretdb_migration() {
|
||||
echo "=== Starting MongoDB → FerretDB migration ===" | tee -a "$MIGRATION_LOG"
|
||||
echo "$(date)" | tee -a "$MIGRATION_LOG"
|
||||
|
||||
# Find old mongod binary in the previous snap revision.
|
||||
# 'snap refresh' keeps the previous revision in /snap/<name>/<rev>/.
|
||||
OLD_MONGOD=""
|
||||
if [ -f "/snap/${SNAP_NAME}/previous/bin/mongod" ]; then
|
||||
# Check it is the real MongoDB binary (the new wrapper contains 'ferretdb')
|
||||
if ! grep -q 'ferretdb' "/snap/${SNAP_NAME}/previous/bin/mongod" 2>/dev/null; then
|
||||
OLD_MONGOD="/snap/${SNAP_NAME}/previous/bin/mongod"
|
||||
fi
|
||||
fi
|
||||
# Also check by revision number (snap keeps at least 2 revisions)
|
||||
if [ -z "$OLD_MONGOD" ]; then
|
||||
for rev_dir in $(ls /snap/${SNAP_NAME}/ 2>/dev/null | grep -E '^[0-9]+$' | sort -rn); do
|
||||
candidate="/snap/${SNAP_NAME}/${rev_dir}/bin/mongod"
|
||||
if [ -f "$candidate" ] && ! grep -q 'ferretdb' "$candidate" 2>/dev/null; then
|
||||
OLD_MONGOD="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$OLD_MONGOD" ]; then
|
||||
echo "WARNING: Cannot find old MongoDB binary in any previous snap revision." | tee -a "$MIGRATION_LOG"
|
||||
echo " Skipping data migration. Your old MongoDB data is preserved at:" | tee -a "$MIGRATION_LOG"
|
||||
echo " $SNAP_COMMON (WiredTiger format)" | tee -a "$MIGRATION_LOG"
|
||||
echo " To migrate manually:" | tee -a "$MIGRATION_LOG"
|
||||
echo " 1. Install mongodb-community or use Docker: mongo:8" | tee -a "$MIGRATION_LOG"
|
||||
echo " 2. mongodump --dbpath=$SNAP_COMMON --out=$DUMP_DIR" | tee -a "$MIGRATION_LOG"
|
||||
echo " 3. mongorestore --uri=mongodb://127.0.0.1:${FERRET_PORT} $DUMP_DIR" | tee -a "$MIGRATION_LOG"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Found old MongoDB binary: $OLD_MONGOD" | tee -a "$MIGRATION_LOG"
|
||||
|
||||
# Start old MongoDB on a temp port (avoid conflict with FerretDB target port)
|
||||
OLD_PORT=27020
|
||||
rm -rf "$DUMP_DIR"
|
||||
mkdir -p "$DUMP_DIR"
|
||||
|
||||
echo "Starting old MongoDB on port ${OLD_PORT}..." | tee -a "$MIGRATION_LOG"
|
||||
"$OLD_MONGOD" \
|
||||
--dbpath="$SNAP_COMMON" \
|
||||
--port="$OLD_PORT" \
|
||||
--bind_ip=127.0.0.1 \
|
||||
--logpath="${SNAP_COMMON}/old_mongod_migration.log" \
|
||||
--logappend \
|
||||
--noauth &
|
||||
OLD_MONGOD_PID=$!
|
||||
|
||||
# Wait for old MongoDB to start
|
||||
for i in $(seq 1 90); do
|
||||
if "/snap/${SNAP_NAME}/current/bin/mongosh" \
|
||||
--quiet --port "$OLD_PORT" \
|
||||
--eval "db.adminCommand('ping')" >/dev/null 2>&1; then
|
||||
echo "Old MongoDB is ready on port ${OLD_PORT}." | tee -a "$MIGRATION_LOG"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
if [ "$i" -eq 90 ]; then
|
||||
echo "ERROR: Old MongoDB failed to start within 90 s. Migration aborted." | tee -a "$MIGRATION_LOG"
|
||||
kill "$OLD_MONGOD_PID" 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Dump all data from old MongoDB
|
||||
echo "Dumping data from old MongoDB..." | tee -a "$MIGRATION_LOG"
|
||||
"/snap/${SNAP_NAME}/current/bin/mongodump" \
|
||||
--port="$OLD_PORT" \
|
||||
--out="$DUMP_DIR" \
|
||||
--quiet 2>>"$MIGRATION_LOG"
|
||||
DUMP_EXIT=$?
|
||||
|
||||
# Stop old MongoDB
|
||||
echo "Stopping old MongoDB..." | tee -a "$MIGRATION_LOG"
|
||||
kill "$OLD_MONGOD_PID" 2>/dev/null
|
||||
wait "$OLD_MONGOD_PID" 2>/dev/null
|
||||
|
||||
if [ "$DUMP_EXIT" -ne 0 ]; then
|
||||
echo "ERROR: mongodump failed (exit $DUMP_EXIT). Migration aborted." | tee -a "$MIGRATION_LOG"
|
||||
return 1
|
||||
fi
|
||||
echo "Dump complete: $DUMP_DIR" | tee -a "$MIGRATION_LOG"
|
||||
return 0
|
||||
}
|
||||
|
||||
restore_dump_to_ferretdb() {
|
||||
echo "Restoring dump to FerretDB on port ${FERRET_PORT}..." | tee -a "$MIGRATION_LOG"
|
||||
"/snap/${SNAP_NAME}/current/bin/mongorestore" \
|
||||
--uri="mongodb://127.0.0.1:${FERRET_PORT}" \
|
||||
--drop \
|
||||
--quiet \
|
||||
"$DUMP_DIR" 2>>"$MIGRATION_LOG"
|
||||
local exit_code=$?
|
||||
if [ "$exit_code" -ne 0 ]; then
|
||||
echo "ERROR: mongorestore failed (exit $exit_code)." | tee -a "$MIGRATION_LOG"
|
||||
return 1
|
||||
fi
|
||||
echo "Restore complete." | tee -a "$MIGRATION_LOG"
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_schema_and_migrate_files() {
|
||||
# ── Phase A: mongosh schema upgrade script (swimlaneIds, CFS→Meteor-Files) ─
|
||||
local mongosh_script="/snap/${SNAP_NAME}/current/bin/migrate-schema-v843.js"
|
||||
if [ -f "$mongosh_script" ] && [ -f "/snap/${SNAP_NAME}/current/bin/mongosh" ]; then
|
||||
echo "Applying schema v8.43 upgrades via mongosh..." | tee -a "$MIGRATION_LOG"
|
||||
"/snap/${SNAP_NAME}/current/bin/mongosh" \
|
||||
--quiet \
|
||||
--port "$FERRET_PORT" \
|
||||
--file "$mongosh_script" 2>>"$MIGRATION_LOG"
|
||||
echo "Schema migration done." | tee -a "$MIGRATION_LOG"
|
||||
else
|
||||
echo "WARNING: mongosh schema script not found — skipping schema upgrade." | tee -a "$MIGRATION_LOG"
|
||||
fi
|
||||
|
||||
# ── Phase B: GridFS / CollectionFS binary files → filesystem ──────────────
|
||||
# Uses migrate-mongodb-to-ferretdb.mjs (bundled in the 'migration' directory
|
||||
# together with its 'mongodb' npm dependency) and the snap's Node.js binary.
|
||||
local node_bin="/snap/${SNAP_NAME}/current/usr/bin/node"
|
||||
[ -f "$node_bin" ] || node_bin=$(which node 2>/dev/null)
|
||||
local migrate_script="/snap/${SNAP_NAME}/current/migration/migrate-mongodb-to-ferretdb.mjs"
|
||||
|
||||
if [ ! -f "$node_bin" ] || [ ! -f "$migrate_script" ]; then
|
||||
echo "WARNING: migration script or node not found — GridFS files stay in FerretDB." | tee -a "$MIGRATION_LOG"
|
||||
echo " WeKan's backward-compatibility layer will serve them until manually migrated." | tee -a "$MIGRATION_LOG"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$ATTACH_DIR" "$AVATAR_DIR"
|
||||
echo "Migrating GridFS/CollectionFS files to filesystem ($ATTACH_DIR)..." | tee -a "$MIGRATION_LOG"
|
||||
|
||||
# SOURCE and TARGET both point to FerretDB — data is already there after
|
||||
# mongorestore. The script reads GridFS chunks and writes them to filesystem,
|
||||
# then updates the attachment/avatar records in place (idempotent).
|
||||
NODE_PATH="/snap/${SNAP_NAME}/current/migration/node_modules" \
|
||||
WRITABLE_PATH="$SNAP_COMMON" \
|
||||
MIGRATION_PORT=8082 \
|
||||
SOURCE_MONGO_URL="mongodb://127.0.0.1:${FERRET_PORT}/wekan" \
|
||||
TARGET_MONGO_URL="mongodb://127.0.0.1:${FERRET_PORT}/wekan" \
|
||||
FORCE_MIGRATE=true \
|
||||
"$node_bin" "$migrate_script" 2>>"$MIGRATION_LOG"
|
||||
|
||||
echo "File migration done." | tee -a "$MIGRATION_LOG"
|
||||
}
|
||||
|
||||
# ── Main startup logic ──────────────────────────────────────────────────────
|
||||
NEED_MIGRATION=false
|
||||
if [ -f "$WIREDTIGER_MARKER" ] && [ ! -f "$MIGRATION_DONE_MARKER" ]; then
|
||||
NEED_MIGRATION=true
|
||||
# Check if MongoDB is already running
|
||||
if check_mongodb_running; then
|
||||
echo "MongoDB is already running. Exiting to prevent multiple instances."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$NEED_MIGRATION" = "true" ]; then
|
||||
echo "=== WiredTiger (MongoDB) data detected — migration required ==="
|
||||
# Phase 1: dump from old MongoDB
|
||||
if run_mongodb_to_ferretdb_migration; then
|
||||
HAVE_DUMP=true
|
||||
else
|
||||
HAVE_DUMP=false
|
||||
echo "Migration dump skipped or failed; starting FerretDB with existing data." | tee -a "$MIGRATION_LOG"
|
||||
fi
|
||||
# Start MongoDB 7.x server
|
||||
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
|
||||
|
||||
# ── Start FerretDB + PostgreSQL (always) ────────────────────────────────────
|
||||
echo "Starting FerretDB 2 + PostgreSQL 16..."
|
||||
echo $$ > "${SNAP_COMMON}/ferretdb.pid"
|
||||
|
||||
# bin/mongod is the wrapper script written by snapcraft's database part
|
||||
"/snap/${SNAP_NAME}/current/bin/mongod" &
|
||||
FERRETDB_PID=$!
|
||||
|
||||
# Wait for FerretDB to accept connections
|
||||
wait_for_mongo_port "$FERRET_PORT" "FerretDB"
|
||||
FERRET_READY=$?
|
||||
|
||||
if [ "$NEED_MIGRATION" = "true" ] && [ "$FERRET_READY" -eq 0 ] && [ "$HAVE_DUMP" = "true" ]; then
|
||||
# Phase 2: restore dump into FerretDB
|
||||
if restore_dump_to_ferretdb; then
|
||||
# Phase 3: schema upgrades + Phase 4: GridFS → filesystem (combined)
|
||||
apply_schema_and_migrate_files
|
||||
# Mark migration complete so we never re-run it
|
||||
mv "$WIREDTIGER_MARKER" "$MIGRATION_DONE_MARKER"
|
||||
echo "=== Migration complete. Old WiredTiger data renamed to WiredTiger.migrated ===" | tee -a "$MIGRATION_LOG"
|
||||
rm -rf "$DUMP_DIR"
|
||||
else
|
||||
echo "ERROR: Restore failed. FerretDB running but data may be incomplete." | tee -a "$MIGRATION_LOG"
|
||||
echo " Manual intervention required. Dump preserved at: $DUMP_DIR" | tee -a "$MIGRATION_LOG"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Database ready on port ${FERRET_PORT}."
|
||||
|
||||
# Stay alive — snap service manager watches this process
|
||||
wait $FERRETDB_PID
|
||||
# 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 \
|
||||
--replSet rs0 --oplogSize 128
|
||||
@@ -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.
|
||||
|
||||
+40
-256
@@ -1,5 +1,5 @@
|
||||
name: wekan
|
||||
adopt-info: wekan
|
||||
version: '8.78'
|
||||
base: core24
|
||||
summary: Open Source kanban
|
||||
description: |
|
||||
@@ -22,29 +22,11 @@ issues: https://github.com/wekan/wekan/issues
|
||||
source-code: https://github.com/wekan/wekan
|
||||
website: https://wekan.fi
|
||||
|
||||
# Platforms supported by the snap (database bundled inside):
|
||||
# ALL platforms — FerretDB 2 + PostgreSQL 16
|
||||
# amd64, arm64 — FerretDB 2 prebuilt static binary (GitHub Releases)
|
||||
# + mongosh + mongodb-database-tools (backup/restore/migrate)
|
||||
# armhf, s390x, ppc64el — FerretDB 2 built from Go source
|
||||
# Upgrading from an older snap that bundled MongoDB: mongodb-control
|
||||
# automatically migrates existing WiredTiger data to FerretDB on first start.
|
||||
# Use platforms instead of architectures for core24 base
|
||||
platforms:
|
||||
amd64:
|
||||
build-on: amd64
|
||||
build-for: amd64
|
||||
arm64:
|
||||
build-on: arm64
|
||||
build-for: arm64
|
||||
armhf:
|
||||
build-on: armhf
|
||||
build-for: armhf
|
||||
s390x:
|
||||
build-on: s390x
|
||||
build-for: s390x
|
||||
ppc64el:
|
||||
build-on: ppc64el
|
||||
build-for: ppc64el
|
||||
|
||||
plugs:
|
||||
mongodb-plug:
|
||||
@@ -95,156 +77,43 @@ apps:
|
||||
|
||||
|
||||
parts:
|
||||
# database: FerretDB 2 + PostgreSQL 16 on ALL platforms.
|
||||
#
|
||||
# FerretDB speaks the MongoDB wire protocol so WeKan/Meteor connect to it
|
||||
# identically to MongoDB. OpLog is emulated via --replication-set-name=rs0
|
||||
# which gives Meteor the real-time pub/sub it needs.
|
||||
#
|
||||
# amd64, arm64 — FerretDB 2 prebuilt static binary (from GitHub Releases)
|
||||
# + mongosh 2.8.2 and mongodb-database-tools 100.13.0
|
||||
# (both work against FerretDB's MongoDB wire protocol,
|
||||
# used for backup / restore / migration)
|
||||
# + PostgreSQL 16 (storage backend, via stage-packages)
|
||||
#
|
||||
# armhf, s390x, ppc64el — FerretDB 2 built from Go source (no prebuilt
|
||||
# binaries exist for these platforms yet) + PostgreSQL 16
|
||||
#
|
||||
# Upgrade path from MongoDB (snap):
|
||||
# On first startup after snap refresh, mongodb-control detects whether
|
||||
# the SNAP_COMMON directory still contains WiredTiger (old MongoDB) data.
|
||||
# If found it:
|
||||
# 1. Starts the previous snap revision's mongod briefly on a temp port.
|
||||
# 2. Dumps all data with mongodump.
|
||||
# 3. Stops old mongod.
|
||||
# 4. Starts FerretDB + PostgreSQL.
|
||||
# 5. Restores data with mongorestore.
|
||||
# 6. Applies schema v8.43 upgrades via mongosh script.
|
||||
# 7. Migrates GridFS files to $SNAP_COMMON/files/attachments|avatars/.
|
||||
# 8. Renames WiredTiger → WiredTiger.migrated to mark completion.
|
||||
#
|
||||
# A DATABASE_BACKEND file (always "ferretdb") is written at build time so
|
||||
# mongodb-control can confirm the expected backend without calling dpkg.
|
||||
database:
|
||||
plugin: nil
|
||||
build-packages:
|
||||
- wget
|
||||
- ca-certificates
|
||||
mongodb:
|
||||
source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.31.tgz
|
||||
plugin: dump
|
||||
stage-packages:
|
||||
# PostgreSQL 16 is the FerretDB storage backend on all platforms.
|
||||
# FerretDB itself is a static Go binary with no runtime library deps.
|
||||
- postgresql-16
|
||||
- postgresql-client-16
|
||||
override-build: |
|
||||
SNAP_ARCH=$(dpkg --print-architecture)
|
||||
mkdir -p $SNAPCRAFT_PART_INSTALL/bin $SNAPCRAFT_PART_INSTALL/usr
|
||||
|
||||
FERRETDB_VERSION="2.7.0"
|
||||
|
||||
# ── FerretDB binary ──────────────────────────────────────────────
|
||||
case "$SNAP_ARCH" in
|
||||
amd64)
|
||||
# FerretDB 2 prebuilt static binary — amd64
|
||||
wget -q -O "$SNAPCRAFT_PART_INSTALL/bin/ferretdb" \
|
||||
"https://github.com/FerretDB/FerretDB/releases/download/v${FERRETDB_VERSION}/ferretdb-linux-amd64"
|
||||
chmod +x "$SNAPCRAFT_PART_INSTALL/bin/ferretdb"
|
||||
# mongosh 2.8.2 (MongoDB-shell, works against FerretDB)
|
||||
wget -q https://downloads.mongodb.com/compass/mongosh-2.8.2-linux-x64.tgz
|
||||
tar xzf mongosh-2.8.2-linux-x64.tgz --strip-components=1 -C $SNAPCRAFT_PART_INSTALL
|
||||
rm mongosh-2.8.2-linux-x64.tgz
|
||||
# mongodb-database-tools 100.13.0 (mongodump/mongorestore, work with FerretDB)
|
||||
wget -q https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2404-x86_64-100.13.0.tgz
|
||||
tar xzf mongodb-database-tools-ubuntu2404-x86_64-100.13.0.tgz --strip-components=1 -C $SNAPCRAFT_PART_INSTALL
|
||||
rm mongodb-database-tools-ubuntu2404-x86_64-100.13.0.tgz
|
||||
;;
|
||||
|
||||
arm64)
|
||||
# FerretDB 2 prebuilt static binary — arm64
|
||||
wget -q -O "$SNAPCRAFT_PART_INSTALL/bin/ferretdb" \
|
||||
"https://github.com/FerretDB/FerretDB/releases/download/v${FERRETDB_VERSION}/ferretdb-linux-arm64"
|
||||
chmod +x "$SNAPCRAFT_PART_INSTALL/bin/ferretdb"
|
||||
# mongosh 2.8.2 arm64
|
||||
wget -q https://downloads.mongodb.com/compass/mongosh-2.8.2-linux-arm64.tgz
|
||||
tar xzf mongosh-2.8.2-linux-arm64.tgz --strip-components=1 -C $SNAPCRAFT_PART_INSTALL
|
||||
rm mongosh-2.8.2-linux-arm64.tgz
|
||||
# mongodb-database-tools 100.13.0 arm64
|
||||
wget -q https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2404-arm64-100.13.0.tgz
|
||||
tar xzf mongodb-database-tools-ubuntu2404-arm64-100.13.0.tgz --strip-components=1 -C $SNAPCRAFT_PART_INSTALL
|
||||
rm mongodb-database-tools-ubuntu2404-arm64-100.13.0.tgz
|
||||
;;
|
||||
|
||||
armhf|s390x|ppc64el)
|
||||
# No prebuilt FerretDB 2 binaries for these architectures.
|
||||
# Build from Go source using Go 1.24.13 (Ubuntu 24.04 ships Go 1.22, too old).
|
||||
GOVERSION="1.24.13"
|
||||
case "$SNAP_ARCH" in
|
||||
armhf) GO_DL_ARCH="armv6l" ;;
|
||||
s390x) GO_DL_ARCH="s390x" ;;
|
||||
ppc64el) GO_DL_ARCH="ppc64le" ;;
|
||||
esac
|
||||
wget -q "https://dl.google.com/go/go${GOVERSION}.linux-${GO_DL_ARCH}.tar.gz"
|
||||
tar xzf "go${GOVERSION}.linux-${GO_DL_ARCH}.tar.gz" -C /tmp
|
||||
rm "go${GOVERSION}.linux-${GO_DL_ARCH}.tar.gz"
|
||||
export PATH="/tmp/go/bin:$PATH"
|
||||
export GOPATH="$(mktemp -d)"
|
||||
CGO_ENABLED=0 GOBIN="$SNAPCRAFT_PART_INSTALL/bin" \
|
||||
go install "github.com/FerretDB/FerretDB/v2/cmd/ferretdb@v${FERRETDB_VERSION}"
|
||||
rm -rf "$GOPATH" /tmp/go
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Shared mongod wrapper (all platforms) ────────────────────────
|
||||
# mongodb-control execs bin/mongod; this wrapper starts PostgreSQL
|
||||
# and FerretDB so no changes are needed in the control script.
|
||||
cat > "$SNAPCRAFT_PART_INSTALL/bin/mongod" << 'WRAPPER'
|
||||
#!/bin/sh
|
||||
# FerretDB 2 + PostgreSQL 16 startup wrapper (all snap platforms).
|
||||
# Called by mongodb-control; replaces the MongoDB mongod binary.
|
||||
PG_DATA="${SNAP_COMMON}/postgresql"
|
||||
PG_LOG="${SNAP_COMMON}/postgresql.log"
|
||||
PG_BIN="$SNAP/usr/lib/postgresql/16/bin"
|
||||
|
||||
# Initialize PostgreSQL data directory on first run
|
||||
if [ ! -f "$PG_DATA/PG_VERSION" ]; then
|
||||
mkdir -p "$PG_DATA"
|
||||
"$PG_BIN/initdb" -D "$PG_DATA" --encoding=UTF8 --auth=trust
|
||||
printf 'listen_addresses = '"'"'127.0.0.1'"'"'\n' >> "$PG_DATA/postgresql.conf"
|
||||
printf 'unix_socket_directories = '"'"'%s'"'"'\n' "${SNAP_COMMON}" >> "$PG_DATA/postgresql.conf"
|
||||
fi
|
||||
|
||||
# Start PostgreSQL (wait for readiness before continuing)
|
||||
"$PG_BIN/pg_ctl" -D "$PG_DATA" -l "$PG_LOG" start -w
|
||||
|
||||
# Create FerretDB role and database (idempotent)
|
||||
"$PG_BIN/psql" -h 127.0.0.1 -U postgres \
|
||||
-c "CREATE ROLE ferretdb WITH LOGIN PASSWORD 'ferretdb';" 2>/dev/null || true
|
||||
"$PG_BIN/psql" -h 127.0.0.1 -U postgres \
|
||||
-c "CREATE DATABASE wekan OWNER ferretdb;" 2>/dev/null || true
|
||||
|
||||
# Start FerretDB on MongoDB wire protocol port with OpLog emulation
|
||||
exec "$SNAP/bin/ferretdb" \
|
||||
--listen-addr=127.0.0.1:27017 \
|
||||
--replication-set-name=rs0 \
|
||||
--postgresql-url="postgres://ferretdb:ferretdb@127.0.0.1:5432/wekan"
|
||||
WRAPPER
|
||||
chmod +x "$SNAPCRAFT_PART_INSTALL/bin/mongod"
|
||||
|
||||
# ── Build-time backend marker ────────────────────────────────────
|
||||
echo "ferretdb" > "$SNAPCRAFT_PART_INSTALL/DATABASE_BACKEND"
|
||||
- libssl3
|
||||
- libcurl4
|
||||
- libstemmer0d
|
||||
- zlib1g
|
||||
- libsnappy1v5
|
||||
- libyaml-cpp0.8
|
||||
- libpcre3
|
||||
- libpcrecpp0v5
|
||||
- libboost-system1.74.0
|
||||
- libboost-iostreams1.74.0
|
||||
- libboost-filesystem1.74.0
|
||||
- libboost-program-options1.74.0
|
||||
- libgoogle-perftools4
|
||||
stage:
|
||||
- bin
|
||||
- usr
|
||||
- DATABASE_BACKEND
|
||||
prime:
|
||||
- bin
|
||||
- usr
|
||||
- DATABASE_BACKEND
|
||||
|
||||
mongosh:
|
||||
source: https://downloads.mongodb.com/compass/mongosh-2.8.2-linux-x64.tgz
|
||||
plugin: dump
|
||||
|
||||
mongotools:
|
||||
source: https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2404-x86_64-100.16.0.tgz
|
||||
plugin: dump
|
||||
|
||||
|
||||
wekan:
|
||||
source: .
|
||||
plugin: npm
|
||||
npm-node-version: 22.22.2
|
||||
npm-node-version: 24.14.1
|
||||
npm-include-node: true
|
||||
build-packages:
|
||||
- build-essential
|
||||
@@ -263,119 +132,34 @@ WRAPPER
|
||||
- libfontconfig1
|
||||
override-build: |
|
||||
echo "Cleaning environment first"
|
||||
#rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
|
||||
rm -rf .build
|
||||
#echo "Using http npm packages so speedup install process https://stackoverflow.com/questions/39760113/callback-called-more-than-once-while-running-npm-install"
|
||||
#echo "registry=http://registry.npmjs.org/" > ~/.npmrc
|
||||
#echo "Installing npm, node-gyp, node-pre-gyp, fibers"
|
||||
#npm -g install n --unsafe-perm
|
||||
#n 14.21.4
|
||||
#npm -g install node-gyp --unsafe-perm
|
||||
#npm -g install node-pre-gyp --unsafe-perm
|
||||
#npm -g install fibers --unsafe-perm
|
||||
##echo "Installing meteor"
|
||||
##curl https://install.meteor.com/ -o install_meteor.sh
|
||||
##chmod +x install_meteor.sh
|
||||
##sh install_meteor.sh
|
||||
##rm install_meteor.sh
|
||||
#npm -g install meteor --unsafe-perm --allow-superuser
|
||||
#rm -rf .build
|
||||
##chmod u+w *.json
|
||||
#npm install --unsafe-perm
|
||||
##npm install
|
||||
##meteor build .build --directory --allow-superuser
|
||||
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
|
||||
##rm -rf .build/bundle/programs/web.browser.legacy
|
||||
# Change to directory .build/bundle/programs/server
|
||||
##cd .build/bundle/programs/server
|
||||
##chmod u+w *.json
|
||||
#npm install --unsafe-perm
|
||||
##npm install
|
||||
##cd node_modules/fibers
|
||||
##node build.js
|
||||
##cd ../../../../../..
|
||||
# Cleanup
|
||||
# Read version from package.json and set snap version
|
||||
WEKAN_VERSION=$(python3 -c "import json; print(json.load(open('package.json'))['version'])")
|
||||
snapcraftctl set-version "${WEKAN_VERSION}"
|
||||
|
||||
# Detect target architecture and map to bundle/Node.js names
|
||||
SNAP_ARCH=$(dpkg --print-architecture)
|
||||
case "$SNAP_ARCH" in
|
||||
amd64) WEKAN_ARCH="amd64" NODE_ARCH="x64" ;;
|
||||
arm64) WEKAN_ARCH="arm64" NODE_ARCH="arm64" ;;
|
||||
armhf) WEKAN_ARCH="armhf" NODE_ARCH="armv7l" ;;
|
||||
ppc64el) WEKAN_ARCH="ppc64le" NODE_ARCH="ppc64le" ;;
|
||||
s390x) WEKAN_ARCH="s390x" NODE_ARCH="s390x" ;;
|
||||
*) echo "Unsupported snap architecture: $SNAP_ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Download and extract the pre-built WeKan bundle for this architecture
|
||||
mkdir .build
|
||||
cd .build
|
||||
wget -q https://github.com/wekan/wekan/releases/download/v${WEKAN_VERSION}/wekan-${WEKAN_VERSION}-${WEKAN_ARCH}.zip
|
||||
unzip -q wekan-${WEKAN_VERSION}-${WEKAN_ARCH}.zip
|
||||
rm wekan-${WEKAN_VERSION}-${WEKAN_ARCH}.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 ..
|
||||
|
||||
# Download the matching Node.js 22.22.2 binary for this architecture
|
||||
NODE_VERSION="22.22.2"
|
||||
wget -q https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz
|
||||
tar -xf node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz \
|
||||
node-v${NODE_VERSION}-linux-${NODE_ARCH}/bin/node
|
||||
rm node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz
|
||||
mkdir -p $SNAPCRAFT_PART_INSTALL/bin
|
||||
cp -p node-v${NODE_VERSION}-linux-${NODE_ARCH}/bin/node $SNAPCRAFT_PART_INSTALL/bin/
|
||||
rm -rf node-v${NODE_VERSION}-linux-${NODE_ARCH}
|
||||
|
||||
# Install the WeKan bundle into the snap
|
||||
# Copy to Snap
|
||||
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-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
|
||||
|
||||
|
||||
# Delete phantomjs that is in accounts-lockout
|
||||
#rm -rf $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/lucasantoniassi_accounts-lockout/node_modules/phantomjs-prebuilt
|
||||
# Delete temporary files
|
||||
#rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
|
||||
#rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
#rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
# Meteor 1.8.x additional .swp remove
|
||||
#rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
|
||||
# Delete fibers for other archs
|
||||
#rm -rf $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/fibers/bin/linux-ia32*
|
||||
# ostrio tmp remove
|
||||
#rm -rf $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/ostrio_files/node_modules/request-libcurl/.node_modules-garbage*
|
||||
|
||||
organize:
|
||||
README: README.wekan
|
||||
prime:
|
||||
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
|
||||
- -lib/node_modules/weka*
|
||||
#prime:
|
||||
# - -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
|
||||
# - -lib/node_modules/weka*
|
||||
|
||||
helpers:
|
||||
source: snap-src
|
||||
plugin: dump
|
||||
|
||||
# migration-tools: bundles releases/migrate-mongodb-to-ferretdb.mjs and its
|
||||
# only npm dependency (mongodb ^6) so mongodb-control can run it with the
|
||||
# snap's bundled Node.js without requiring internet access at runtime.
|
||||
migration-tools:
|
||||
source: releases
|
||||
plugin: nil
|
||||
build-packages:
|
||||
- nodejs
|
||||
- npm
|
||||
override-build: |
|
||||
mkdir -p "$SNAPCRAFT_PART_INSTALL/migration"
|
||||
cp migrate-mongodb-to-ferretdb.mjs package.json "$SNAPCRAFT_PART_INSTALL/migration/"
|
||||
cd "$SNAPCRAFT_PART_INSTALL/migration"
|
||||
npm install --omit=dev --prefer-offline 2>/dev/null || npm install --omit=dev
|
||||
stage:
|
||||
- migration
|
||||
prime:
|
||||
- migration
|
||||
|
||||
caddy:
|
||||
plugin: nil
|
||||
build-packages:
|
||||
|
||||
@@ -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=..
|
||||
|
||||
Reference in new issue
Block a user