- Attachments now import. Trello attachment URLs require OAuth to download, so
WeKan could not fetch them. The Trello import page now has two separate file
fields: a **.json** field for a single Trello board, and a
**.zip** field for a package produced by the
[Trello Card Attachments Downloader](https://github.com/wekan/trello-attachments-downloader).
The .zip may contain one *or more* board .json files together with each
board's attachments in a board-name subfolder. The .zip is uploaded over HTTP
and processed entirely on the server (the attachment bytes never travel over
the realtime WebSocket, which previously overflowed the connection and made
the page flicker): WeKan detects every board .json, matches each board to its
attachment subfolder by name, streams each file straight into the Default
file storage, imports all boards, then opens All Boards. The upload is guarded
against abuse — it rejects oversized uploads, zip bombs (too many files or too
much uncompressed data, per-file size caps) and unsafe entry paths (absolute
paths or `..` traversal), and never overwrites existing files (stored under
generated ids). The importer also reads attachments from `card.attachments`
(newer Trello exports), not only from the action log, and uses the correct
Meteor-Files insert API (the previous calls were no-ops on Meteor 3). Single
board imports (the .json file or the pasted JSON, after the map-members step)
now run through the same server-side HTTP path rather than the realtime
`importBoard` method, so a heavy import can no longer drop the WebSocket and
get retried in an endless loop (the page flickering with "Invalid frame
header" / repeated "logged out" errors). When importing only a single board
.json and you have saved Trello API key and token, WeKan uses them on the
server to download that board's attachments, background image, member
avatars and card stickers from Trello (a .json export often omits stickers;
avatars only fill in mapped users who have none — an existing avatar is never
overwritten); each download is best-effort and a failure never aborts the
import. After a single-board import the new board's data is subscribed and
loaded before opening it, so it shows its swimlanes, lists and cards
immediately instead of needing a browser page reload (the HTTP import, unlike
the old realtime method, does not push the written documents to the client by
itself).
- Import Trello-only card features: stickers (shown as Font Awesome icons on
minicards and the card detail), card cover (color or attachment), card
location (name, address, coordinates with an OpenStreetMap link), and the
due-date "complete" checkbox shown on the minicard. Trello's two named
sticker packs are renamed and highlighted (the icon keeps its normal colour)
so they stay distinct: the "taco" pack becomes "mascot" with an underlined
icon, and the "pete" pack becomes "computer" with a ring around the icon,
e.g. "pete-ghost" imports as "computer ghost" with a Snapchat-ghost icon.
Several named stickers that used to share an icon now get their own Font
Awesome 4.7 icon (mascot active => heartbeat, pixel => qrcode, proto =>
flask, embarrassed => meh-o, clean => bath, computer shipped => truck).
- Live Trello API import: paste your Trello API key and token on the Trello
import page to list all your Trello workspaces and boards, then import all
or selected boards together with their attachments (downloaded
server-side). Imported boards are placed under a personal workspace named
after their Trello workspace, created by name if it does not exist, under an
optional parent workspace you choose. The API client respects Trello's rate
limits: it spaces out requests, honours `Retry-After` on HTTP 429, retries
transient 5xx and network errors with capped exponential backoff, and
reports a clear error on an invalid key/token (401). After a successful API
import you are taken to the All Boards page where the new boards appear.
- The live API import runs as a background job on the server: you can navigate
away and come back to the Trello import page to watch progress, see
per-board results, and read the full error log. Errors are shown in a
selectable text box with a one-click "Copy to clipboard" button (and you can
still select and copy just part of the text) so they are easy to share when
fixing issues. If the import stops on a fatal API error (invalid token, or
rate limit still failing after retries) you can fix the cause and Resume
from where it left off, and a server restart leaves the job paused for
resuming rather than lost. You can also Cancel the import, or Cancel and
delete the boards already imported, to start the whole process over cleanly.
- You can optionally save your Trello API key and token with the Save and
Delete buttons on the import page. When saved, they are stored only on the
server, never sent back to the browser, and are reused automatically when
you list workspaces or import (so you do not have to paste them again); a
"saved" indicator is shown, Save can overwrite them with new values, and
Delete clears them from the database. If you do not save them, they are kept
only in server memory for the running import and re-supplied when you resume.
- Larger Trello JSON exports can be selected as a file instead of pasted.
- Keep imported usernames for later mapping. Imported members that match an
existing WeKan user (by username, or by a previously recorded imported
username) are mapped automatically, and the imported username is remembered
on that user so future imports map too. Members with no matching WeKan user
are no longer lost: their usernames are kept on the board, so an admin can
map them to real users later in Admin Panel / People (Imported Usernames),
without having to map everyone up front at import time.
Thanks to xet7 and Claude.
- Board background images can now be stored in WeKan. A new `backgrounds` file
storage directory is created alongside `attachments` and `avatars` (using the
current default storage backend), and Board menu → Board backgrounds lets a
board admin upload background images, set one as the active board background,
download, and delete them. Board export now includes the board's background
images and re-imports them, and a Trello board's background image is
downloaded and stored on import (so it keeps working even if the original
Trello URL later changes). Thanks to xet7 and Claude.
- Stickers can be added to and removed from cards directly in WeKan, chosen from
a set of Font Awesome icons similar to Trello's stickers (previously stickers
only arrived via Trello import). The picker also includes every "mascot"
(underlined) and "computer" (ringed) highlighted sticker that a Trello import
can produce — generated from the same icon mapping the importer uses — so any
imported sticker can also be added by hand. Stickers show on the minicard and
in the card detail. Thanks to xet7 and Claude.
- The Trello-style "complete" checkbox (mark a card complete/incomplete,
independent of the due date) is shown as an animated green checkbox to the
left of the card title, both on the minicard and in the opened card, with
"Mark as complete" / "Mark as incomplete" tooltips. It uses the same animated
checkbox style as Admin Panel / Settings / Announcements and is vertically
centered with the title text, and the two stay in sync. The minicard and the
opened-card checkboxes use the same checked style (the board theme's colour, or
green when no theme is set), so they always look identical. Subtask checkboxes
and the card-detail custom-field checkbox now use the same animated checkbox as
checklist items (they previously used static square icons), so every checkbox
on a card animates consistently. Thanks to xet7 and Claude.
- Cards can now have multiple locations, similar to multiple members. The card
detail shows a Location section (after Labels and Stickers) listing each
location with its name, address and an OpenStreetMap link, with an "Add
location" button to add more and a button to edit or remove each one. A single
location imported from Trello keeps working and is shown in the same list. The
Add/Edit location popup can also detect a location from any map link (Google
Maps, OpenStreetMap, Bing Maps, Apple Maps, or generic `?q=`/`?ll=` links):
paste the link, press "Detect", and the latitude, longitude and (when present)
the place name/address are filled in automatically. The popup also has an
"Open map links at" setting (OpenStreetMap by default, or Google/Bing/Apple
Maps) saved to the user profile, controlling which map service the location
"Open in map" links use. Thanks to xet7 and Claude.
- The opened card now docks to the top of the window, overlaying the global and
board header bars, instead of opening downward from the clicked minicard, and
it can be dragged all the way to the top without its top hiding behind those
bars. (At 100% zoom the board wrapper no longer sets a `transform: scale(1)`,
which had made it the containing block for the fixed card and trapped it below
the headers.) Thanks to xet7 and Claude.
Fixes#5727,
fixes#2457,
fixes#5818,
fixes#755
Fix import/clone of inconsistent board JSON so the newest WeKan can import
board exports from any newer or older WeKan version. A syntactically valid
export can still be internally inconsistent (a board member whose user
account was deleted is no longer in the export's `users`, cards pointing at a
missing `listId`, orphaned checklists, or a missing `lists` array). Such
exports previously failed import as "error-json-malformed" or silently
dropped data (cards with an undefined list never rendered). The importer now
skips dangling user references in the member mapper (client
`wekanMembersMapper`, server `wekanmapper` used by board cloning, and
`WekanCreator`), falls back to the first imported list — creating one default
list when the export has none — for cards with a dangling `listId`, and skips
orphaned checklists whose card is missing. The importer remains
version-agnostic: it never reads `_format`, normalizes old `id` vs `_id`
fields, and ignores the source `boardId`.
Added `tests/wekanCreator.inconsistent.test.js` covering these cases.
Thanks to titver968, xet7 and Claude !
Fixes#6371
- **Admin Panel / Attachments: the MongoDB GridFS page notes how to make legacy
files visible.** A translatable line under the "MongoDB GridFS Storage" title
reminds admins: "To have attachments and avatars visible, move them from
CollectionFS to any other Storage." Thanks to Claude.
- **Admin Panel / Attachments: the Google Cloud Storage page now documents the
required bucket permission.** A note under the "GCS Storage" title explains that
the service account needs the **Storage Object Admin** role
(`roles/storage.objectAdmin`) on the bucket (Cloud Console → Cloud Storage →
Buckets → your bucket → Permissions → Grant access → add the service account →
role "Storage Object Admin"), and that the bucket must exist in the same
project — otherwise "Test connection" fails with `storage.objects.list denied`.
Thanks to Claude.
- **Admin Panel / Attachments: a "Save" button next to the Enabled / Read toggles
at the top of each cloud storage.** S3, Azure and Google Cloud Storage now have
a Save button directly below the "[ ] Enabled [ ] Read" row, so those toggles
can be saved without scrolling to the bottom of the (now quite long) storage
page. It saves the same way as the existing bottom Save button — the server
merges that provider's config and preserves secrets left blank — and the bottom
Save button is kept. (Filesystem and GridFS have only a Read toggle, which
already saves the instant it is toggled, so they need no button.) Thanks to Claude.
Fixes:
- **Admin Panel / Attachments: "Run MongoDB compact" now actually compacts the
database.** MongoDB refuses `compact` on an active replica-set primary unless
`force: true` is given, so every collection failed with "will not run compact
on an active replica set primary … use force:true to force" — and because the
usual Meteor setup is a single-node replica set (only a primary, no
secondaries), nothing was compacted at all and no disk space was reclaimed. The
primary is now compacted with `force: true` (secondaries are still compacted
without it, so the primary stays available while they run), so the GridFS
collections are actually rewritten and freed space is returned to the
filesystem. Thanks to Claude.
- **Admin Panel / Attachments: clearer cloud "Test connection" errors, and cloud
config inputs are trimmed.** Testing an Azure/GCS/S3 connection used to report a
single generic `Incomplete configuration or adapter not installed` even when the
real problem was specific — e.g. Azure rejecting the config with `Invalid URL`
(a stray space/newline in the account name, or a bad endpoint / connection
string). `testCloudConnection` now reports the actual cause: adapter not
installed, required fields missing, the adapter's own configuration error (such
as `Invalid URL`), or the real `listFiles` error (auth failure, container not
found, …). It also pre-validates the config and gives an actionable message
before the adapter turns it into a cryptic error — e.g. for Azure it explains
that the **Storage account name** must be just the account name (3–24 lowercase
letters/numbers, e.g. `wekanstorage`, not a full `https://…` URL and with no
spaces), or that the **Connection string** is malformed; and when Azure still
returns `Invalid URL` the message appends which field to check. In addition, all
cloud-storage text fields are now trimmed when read from the form, so
leading/trailing whitespace from copy-paste no longer produces an invalid URL.
Thanks to xet7 and Claude.
- **Admin Panel / Attachments: "Calculate file counts" on every storage backend,
shown right below the "Read" toggle.** Azure Blob Storage and Google Cloud
Storage now have a "Calculate file counts" button (new `getAzureStorageStats` /
`getGcsStorageStats` methods) that reports how many attachments and avatars are
stored on that backend, matching what Filesystem, MongoDB GridFS and S3 already
offer. The count is read from the stored file metadata (fast, no cloud API
calls). The S3 button was also moved up to sit directly under its "Read"
checkbox, so all backends are consistent. Thanks to Claude.
- **Admin Panel / Attachments: bilingual, self-documenting cloud-storage fields
(S3, Azure and Google Cloud Storage).** Each cloud-storage setting now guides
the admin from top to bottom with both English and translated text, so it is
easy to follow along while clicking through the provider's Cloud Console:
- the field **label** uses the provider's own console wording in English
(e.g. *Access key ID*, *Secret access key*, *Storage account key*,
*Service account key (JSON)*), with the translated label shown below it;
- a literal **example** of what the value should look like
(e.g. `eu-west-1`, an example bucket name / endpoint / connection string),
which is intentionally not translated;
- a short **description** of the value, in English and then translated;
- the **Cloud Console menu path** showing exactly where to find or create the
value (e.g. *AWS Console → IAM → Users → your user → Security credentials →
Access keys → Create access key*; *Azure Portal → Storage accounts → your
account → Access keys → key1*; *Google Cloud Console → IAM & Admin →
Service accounts → … → Keys → Add key → Create new key → JSON*), again in
English and then translated.
The provider field names, examples and menu paths are kept in English so they
match what the Cloud Console actually shows, while the descriptions and menu
paths are also translatable; section titles, the enable/read/force-path-style
checkboxes and the Test/Save buttons remain fully translated. Thanks to Claude.
- **Admin Panel / Attachments / Move Attachment: "Repair file locations" button
and a persistent "last move" message.** The new **Repair file locations** button
scans all attachments and avatars and finds any whose recorded storage
(`versions.<v>.storage` / `path` / `meta.gridFsFileId`) no longer matches where
the binary actually is — left inconsistent by an interrupted or failed move —
and fixes the database to point at the real location: it detects the binary in
GridFS (by the `metadata.fileId` stamped on upload, recovering files whose
GridFS id reference was lost) or on the filesystem (using the storage
strategy's own thorough path resolution, so the repair agrees with what a real
download/move would find even when `versions.<v>.path` is stale), then corrects
`storage`, `path` and `meta.gridFsFileId` accordingly. Cloud-stored files are
left untouched, and files found nowhere are reported as "Not found". The scan is
streamed (memory-safe) and shows a per-scope searched / repaired / not-found
summary. Separately, after a move finishes the page now keeps showing
the last move operation — source → destination (scope) and the date/time as
`YYYY-MM-DD HH:MM:SS` — so it is clear what was last done. Thanks to Claude.
New fixes:
- **Fixed moving attachments to S3 (and other cloud storage) failing and
crashing the server.** Uploading to S3 failed two ways, each of which crashed
the whole server because the rejection was unhandled and SyncedCron treats
those as fatal. The `@tweedegolf/sab-adapter-amazon-s3` adapter uploaded with a
`PutObjectCommand` whose `Body` was a live stream: (1) with no `ContentLength`
the AWS SDK fell back to a chunked signed upload that needs a decoded content
length, which was undefined for a stream of unknown size — `Invalid value
"undefined" for header "x-amz-decoded-content-length"`; and (2) if the socket
dropped mid-upload (`socket hang up`), the SDK's body-stream promise rejected
unhandled. Fixed by uploading the file as a complete in-memory **buffer**
(`addFileFromBuffer`) instead of a live stream: a buffer has a known length (no
content-length error) and no socket-bound stream to re-reject. The cloud upload
promise now never rejects — any failure is captured and exposed via
`waitUntilStored()`, which `moveToStorage` checks inside a try/catch so a failed
upload leaves the source file intact and is logged cleanly. (Files are moved one
at a time, so peak memory is one file.) Thanks to Claude.
- **Fixed `TypeError: Cannot read properties of undefined (reading 'on')` when
moving attachments between Meteor-Files/GridFS and Filesystem.** If a file's
source binary was missing at its recorded location (e.g. a file left
half-moved by an earlier interrupted run — storage says "gridfs" but the
GridFS id reference is gone, or a filesystem path that no longer exists),
`getReadStream()`/`getWriteStream()` returned `undefined` and `moveToStorage`
(and `copyFile`) called `.on()` on it, throwing. Both now detect a missing
read/write stream, log a clear message (which file, version, from→to storage,
and why), **skip that file and leave the source intact** so no data is lost,
and continue with the rest. Use the new "Repair file locations" button to fix
the underlying inconsistent records. Thanks to Claude.
- **Fixed moving attachments/avatars from CollectionFS to Meteor-Files crashing
the server, hanging the Admin Panel move at "File 1 / N", and leaving broken
avatars.** Three problems in Admin Panel / Attachments / Move Attachment:
- **Server crash on the first file** (`uncaughtException: TypeError: Cannot read
properties of undefined (reading '_id')` at
`AttachmentStoreStrategyGridFs.writeStreamFinished`). The current mongodb
driver's GridFS `GridFSBucketWriteStream` `'finish'` event no longer passes
the stored file document, so `finishedData._id` threw and killed the process
(which is also why the move appeared stuck at "File 1 / 4" — the background
job died mid-file and its persisted status was frozen). The GridFS strategy
now reads the uploaded file id from the write stream itself (`gridFSFile._id`
/ `id`), the `'finish'` handler is wrapped so it can never crash the process,
and a startup reconciliation clears a stale "running" move status left by a
crashed run so the UI un-sticks and a new move can be started.
- **Broken avatar after migrating avatars.** The bulk move only repointed card
cover references (`cards.coverId`); a user's `profile.avatarUrl` still pointed
at the deleted legacy `/cfs/files/avatars/<oldId>` URL, so the avatar rendered
broken. The move now repoints `profile.avatarUrl` to the migrated avatar
(`remapReferences` handles avatars), and migrated files stamp
`meta.migratedFromId` so references can be repaired to the **exact** new file.
The startup repair in `server/models/users.js` (previously a no-op: it
string-replaced the URL prefix while keeping the now-deleted id and never
saved the document) now points each affected user's avatar at their migrated
Meteor-Files avatar — matched precisely by `meta.migratedFromId`, otherwise
strictly by `userId` (newest migrated avatar first), so a user can only ever
be given their own avatar, never another user's.
- **`ObjectID` deprecation warning** from `models/lib/grid/createObjectId.js`
(`MongoInternals.NpmModule.ObjectID` → `ObjectId`).
Thanks to Claude and xet7 !
**Fixed Dropdown custom field options not being addable / saving as empty.**
Creating a "Dropdown" custom field showed the "List Options" box, but pressing
Enter (or typing and saving) never added any option, and on the card the only
selectable value was `(none)`. The custom-fields sidebar component was migrated
from `BlazeComponent` to a plain `Template`, but the template still iterates the
options with `{{#each dropdownItems.get}}` — under BlazeComponent that resolved
to the instance's `dropdownItems` ReactiveVar, whereas a plain Template does not
expose instance variables to the template, so the list rendered nothing. Because
the options were never rendered as inputs, `getDropdownItems()` then overwrote
the ReactiveVar with the empty DOM on save and every entered value was dropped.
Fixed by re-adding the missing `dropdownItems` helper (returning the ReactiveVar),
matching the pattern the other migrated templates already use. Thanks to Claude.
**Fixed `Exception in global helper _` when opening the Create Custom Field
popup (and any translation containing a literal `%`).** i18n is configured with
a global sprintf post-processor (`postProcess: ["sprintf"]`), so every
translation is run through `i18next-sprintf-postprocessor`. The help text
`custom-field-stringtemplate-format` (`"Format (use %{value} as placeholder)"`)
contains a literal `%{value}` that sprintf cannot parse, so `vsprintf` threw and
crashed the global Blaze `_` translation helper — breaking that popup and any
string (in any language, including user translation overrides) that contains a
stray `%`. `TAPi18n.__` now retries without the sprintf post-processor when it
throws, returning the raw string (so `%{value}` is shown literally) instead of
crashing.
Thanks to rouceto1 and Claude.
Fixes#6357
Several issues in the bulk move:
- The GridFS source matcher required `meta.gridFsFileId` to be *absent*, but
Meteor-Files always sets it, so selecting "MongoDB Meteor-Files" matched zero
files and "nothing happened". The matcher now recognizes real GridFS files
(`versions.*.storage === 'gridfs'` or a `meta.gridFsFileId` reference), and a
"nothing to move" message is shown when a source is empty instead of silently
doing nothing.
- Moving files crashed the whole server with
`FilesCollection#findOne() not available in server` — `ReactiveCache.getAttachment`
used a synchronous `findOne()`; it now uses `findOneAsync()`. The background
job is also hardened so a single failing file is skipped instead of crashing
the server via an unhandled rejection.
- The attachment **copy** API now honours the admin API upload limits (it
previously skipped them).
- **Fixed the "MongoDB Meteor-Files" file-count statistic** in Admin Panel /
Attachments, which counted *every* attachment metadata document (so files on
the Filesystem were wrongly reported as being in GridFS). It now counts only
attachments actually stored in GridFS, consistent with the move tool. Also
renamed the mislabeled "Mongo-Files" column to "Meteor-Files". Thanks to Claude.
- **Unified attachment/avatar storage migration: move any → any.** Admin Panel /
Attachments / Move Attachment can now move **Attachments, Avatars, or both**,
from **any source to any destination** across Filesystem, Meteor-Files GridFS,
Cloud (S3/Azure/GCS) and legacy CollectionFS GridFS. The default source is
"All Read-enabled storages" (every backend whose Read flag is enabled and whose
settings work), so everything can be consolidated into one destination in a
single run. All metadata is preserved (board / swimlane / list / card / user /
uploaded date / name / type / size), attachment cover references
(`cards.coverId`) are remapped when an id changes, and the legacy source is
deleted only after the new copy is verified. Thanks to Claude.
- **Legacy CollectionFS GridFS is now a first-class storage backend** (read,
migrate-from, and export-to) for both attachments and avatars, via the new
`models/lib/collectionFsStore.js` (binary keyed by `copies.<coll>.key` in the
`cfs_gridfs.<coll>` bucket, metadata in `cfs.<coll>.filerecord`). Thanks to Claude.
- **The storage strategy layer is now collection-aware.** `moveToStorage` and the
GridFS/Cloud strategies previously hard-coded the `Attachments` collection, so
moving avatars to GridFS/cloud would have updated the wrong collection. The
factory now carries its collection (`Attachments` or `Avatars`), so **avatars
can be stored in Meteor-Files GridFS and cloud**, not only on the filesystem.
Attachment behavior is unchanged. Thanks to Claude.
- **Read legacy CollectionFS attachments and avatars in place** (without
migrating). The backward-compatibility layer
(`models/lib/attachmentBackwardCompatibility.js`) was broken — it looked up the
GridFS binary by the filerecord `_id` and by filename instead of by
`ObjectId(copies.<coll>.key)`, so legacy files were never found. It is fixed and
generalized for attachments and avatars. Legacy attachments now appear in the
card attachment gallery (new `legacyBoardAttachments` publication) and stream
from the `cfs_gridfs.attachments` bucket, and legacy avatars
(`/cfs/files/avatars/<id>` URLs) are served from the `cfs_gridfs.avatars`
bucket instead of redirecting to a 404.
Thanks to Claude.
- Fixed one-finger drag-to-scroll not working in the All Boards view and the
Calendar view on Fairphone 4 Firefox and Ubuntu Touch Morph. These browsers
do not emit the synthetic mouse events the `@wekanteam/dragscroll` library
relies on, so scrolling only worked in the Swimlanes/Lists views. Added a new
native touch-scrolling helper `client/lib/dragscrollTouch.js` that scrolls the
nearest `.dragscroll` container with a one-finger drag (iOS is skipped, since
native momentum scrolling already works there), and added the `dragscroll`
class to the All Boards list and the Calendar view.
- Fixed all text, buttons, the All Boards tables and the top bars (header and
the second bar) rendering about 2x too large on Fairphone 4. `getMobileMode()`
defaulted to mobile mode only for iPhone, so every other phone fell back to
desktop mode, whose large desktop sizing looks oversized on a small phone
screen. The default is now mobile mode for any phone-sized touch device
(iPhone, Android/Mobile browsers, Ubuntu Touch, or a coarse-pointer touch
screen with viewport width ≤ 800px); desktop browsers stay in desktop mode and
the user's own Mobile/Desktop toggle still takes priority.
- Fixed not being able to reorder swimlanes and lists by dragging their drag
handle on touch devices. The new touch-scroll helper was hijacking touches
that started on a drag handle; `.handle` / `.ui-sortable-handle` elements are
now excluded from touch-scrolling so dragging the handle reorders as expected.
- Fixed a card duplicating into two cards when it was dragged by its title-row
drag handle while its Card Details panel was open. The card sortable in
`client/components/lists/list.js` was re-initialized on every list re-render
without first destroying the previous instance, so two `stop` handlers fired
per drag. It now destroys any existing sortable before re-initializing, the
same guard already used for swimlanes.
Attachment storage:
- Added configurable attachment storage backends in the admin panel: choose the
default save storage and store attachments on the local filesystem, GridFS, or
cloud object storage — S3-compatible (AWS S3, MinIO, Cloudflare R2, Backblaze
B2, Wasabi, DigitalOcean Spaces), Azure Blob Storage, or Google Cloud Storage
— via the `@tweedegolf/storage-abstraction` adapters (new
`models/lib/cloudStorage.js`). Includes a "Test connection" action and
English translations for the new settings.
- Added a server-side bulk attachment move (new `server/attachmentBulkMove.js`
and `models/attachmentBulkMoveStatus.js`) that moves all attachments from one
storage backend to another as a background job whose progress survives the
admin navigating away from or closing the page.
Thanks to xet7.
A new global "Roles" tab in Admin Panel / People lets a site admin choose which
board roles are allowed to invite users to a board ("Allow Invite to Board").
Each board role has its own toggle — Board Admin, Normal, Worker, Comment only,
No comments, Only Assigned Normal, Only Assigned Comment, Read Only and Only
Assigned Read — plus an "All Board Members" master toggle that selects them all.
The policy is enforced server-side in the `inviteUserToBoard` and `searchUsers`
methods, and the add-member button in the board sidebar is shown only to roles
the policy allows. Global Admin Panel users (site admins) always have all rights
and cannot be restricted here; this is kept clearly distinct in code from the
per-board "Board Admin" role. Secure default: only Board Admin and Normal may invite.
Thanks to xet7.
Thanks to mimZD and xet7 !
This is temporary fix, using click.
If sometime later browser protections prevents this
(like happened previously with click code),
fix this properly by extracting js-close-card-details() to function
and change to run that function directly, without using click.
Thanks to mimZD and xet7 !
Fixes#6161
The two changes made:
- **[cardDetails.js](client/components/cards/cardDetails.js)**:
Added `currentSwimlaneListsSorted` helper that queries lists
filtered by the card's `swimlaneId`
(mirroring the logic in `dialogWithBoardSwimlaneList.js`),
sorted by `sort` field (left-to-right column order → top-to-bottom in dropdown).
- **[cardDetails.jade](client/components/cards/cardDetails.jade:300)**:
Changed `each currentBoard.lists` to `each currentSwimlaneListsSorted`
so the dropdown only shows lists belonging to the card's current swimlane.
The default swimlane edge case is handled: if the card is in the default swimlane, lists with `swimlaneId` of `null`, `''`, or the default swimlane ID are all included.
Thanks to hmeunier95 and xet7 !
Fixes#6265
**Fix 1 — [`models/cards.js:797`](models/cards.js)**
`labels()` called `this.board().labels` without a null guard.
If `board()` ever returns `undefined` (board not yet in minimongo,
or a card with a bad `boardId`), this throws silently in Blaze
and all label chips disappear. Now returns `[]` safely.
**Fix 2 — [`client/components/swimlanes/swimlanes.js:660`](client/components/swimlanes/swimlanes.js)**
In swimlane view, `Template.swimlane`'s `visible()` helper
called `list.cards()` **without** the current swimlane ID.
This meant a list in swimlane S2 was considered "non-empty"
(and therefore kept visible) if swimlane S1 had
a card matching the label filter — even though S2 itself
had zero matching cards. The list would appear in S2
but be completely empty, explaining
"some lists are empty even when they contain cards with the label."
Now passes `this._id` (the current swimlane) so
only that swimlane's cards are counted.
Thanks to YosserDerbali and xet7 !
Fixes#6258
**Root cause:** When dragging a board in the Remaining view,
there were no visual cues indicating where to drop — users tried
dropping on other boards (which does nothing), not knowing
they need to drop on a workspace in the left sidebar.
**Changes made:**
1. **[boardsList.js](client/components/boards/boardsList.js)** —
`dragstart .js-board`: On drag start, adds `.board-drag-hint`
class to all workspace nodes and the Remaining menu item
in the sidebar, visually signalling valid drop targets.
New `dragend .js-board` cleans up the class.
2. **[boardsList.js](client/components/boards/boardsList.js)** —
`boardWorkspaceDragHint()`: Updated tooltip
from "Drag board: Remaining <-> Workspaces"
to "Drag board to assign to Workspaces
(drop on workspace in left sidebar)" —
removes the ambiguous bidirectional arrow that implied reordering.
3. **[boardsList.css](client/components/boards/boardsList.css)**:
Added green dashed border highlight for
`.workspace-node.board-drag-hint` and
`.js-select-menu.board-drag-hint` so valid drop targets
light up during a board drag.
4. **[en.i18n.json](imports/i18n/data/en.i18n.json)**:
Added `drag-board-to-workspace` key and
updated `board-open-and-move-between-remaining-and-workspaces`
to reflect the correct purpose (workspace assignment, not reordering).
This resolves the UX mismatch without implementing board reorder
(which carries regression risk).
The drag handle stays visible since it's genuinely functional
for workspace assignment — users just now get
a clear visual guide showing where to drop.
Thanks to aminsaidane and xet7 !
Fixes#6219
**Root cause:** [popup.css](client/components/main/popup.css)
had duplicate and conflicting rules at the bottom of the file.
Two rules were overriding the correct `cardDetailsActionsPopup`-specific fix:
1. `.pop-over:has(.pop-over-list) { max-height: 400px !important }` — had
the same CSS specificity as `.pop-over[data-popup="cardDetailsActionsPopup"]`
but appeared later, so it won, forcing the popup to 400px instead of 80vh.
2. `.pop-over .pop-over-list { max-height: 300px !important }` — forced each
individual list element to independently scroll at 300px, which is wrong
(the container should scroll, not each list).
3. There was an entire duplicate of the same block, adding the rules twice.
**What was removed:** The "STRONGER FIX" section and its duplicate (~55 lines),
which contained the conflicting `max-height: 400px` and `max-height: 300px` rules.
**What remains (and works correctly):**
- The `cardDetailsActionsPopup`-specific rules at lines 343–361
(max-height: 80vh, scrollable content-wrapper)
- The general flex-column layout at the end that constrains all popups to
`max-height: 90vh` with a fixed header and scrollable content area
- The `listActionsPopup`-specific rules
- The JS `_getOffset` in [popup.js](client/lib/popup.js:267) which
already correctly calculates available space above/below the opener
and passes `maxHeight` to the template
Thanks to robertjensen, harryadel and xet7 !
Fixes#6266,
fixes https://github.com/wekan/wekan/pull/6278
**Bug:** Cards disappear from lists that were previously minimized.
**Root cause** — [`client/components/lists/listBody.jade:2`](client/components/lists/listBody.jade):
```jade
template(name="listBody")
unless collapsed ← BUG: reads raw list.collapsed from MongoDB
```
`Template.listBody` has no `collapsed` helper defined in `listBody.js`.
In Blaze, when a helper isn't found, it falls through to
the raw data context — the list document from MongoDB.
So it reads `list.collapsed` directly from the database.
The parent template `list.jade` wraps `+listBody` with
its own `unless collapsed` that correctly uses
`Utils.getListCollapseState()`, which reads from the user's profile.
These two checks were reading from **different sources**.
**Failure sequence:**
1. Old WeKan versions (or a DB migration) stored `collapsed: true`
directly on the list document in MongoDB
2. User clicks the expand arrow → `Utils.setListCollapseState(list, false)
updates `profile.collapsedLists` only — it does **not** clear `list.collapsed`
in MongoDB
3. `list.jade`'s helper sees `false` (from profile) → renders `+listBody`
4. `listBody.jade`'s `unless collapsed` reads the raw
MongoDB field → still `true` → hides all cards, the add-card compose
and the `.js-minicards` container (breaking drag-and-drop too)
**Fix:** Remove the redundant `unless collapsed` from `listBody.jade`.
The parent template already guards it with the correct
helper — if `listBody` is being rendered at all, the list is expanded.
The inner check was both redundant and broken.
Thanks to hmeunier95 and xet7 !
Fixes#6267
In [boardHeader.js:429](client/components/boards/boardHeader.js),
the event handler uses `Template.currentData()` which returns
the popup template's data context — not the string from
the inner `with "watching"` / `with "tracking"` / `with "muted"` blocks.
It should use `this` instead, since `this` in a Blaze event handler
refers to the data context of the element that was clicked.
Thanks to titver968 and xet7 !
Fixes#6294
**Bug:** When the jQuery sortable board-reordering code was replaced with HTML5 drag-and-drop (for workspace management), the board-to-board sorting was accidentally dropped. The `dragstart .js-board` handler only set `dataTransfer` data for workspace assignment — there were no `dragover`/`drop` handlers on `.js-board` elements to actually reorder boards.
**Fix in [boardsList.js](client/components/boards/boardsList.js):** Added four new event handlers after `dragstart .js-board`:
- `dragover .js-board` — allows drop (calls `preventDefault`) and shows a blue top/bottom border indicator based on cursor position relative to the target board's midpoint. Skips workspace drags by checking `dataTransfer.types`.
- `dragleave .js-board` — removes the visual indicator.
- `dragend .js-board` — cleans up all drag indicators on all boards.
- `drop .js-board` — determines drop position (before/after target), walks siblings to find the adjacent `.js-board` DOM elements, calls `Utils.calculateIndex()` to get a sort value, then saves it via `currentUser.setBoardSortIndex()` — matching exactly what the old jQuery sortable did.
**Fix in [boardsList.css](client/components/boards/boardsList.css):** Added `board-drag-over-top` and `board-drag-over-bottom` CSS classes with a 3px blue border to give visual feedback while dragging.
Thanks to snowsky and xet7 !
Fixes#6332
Summary of 3 fixes:
**`server/publications/cards.js`** — three publications were returning the raw `{ cursors, sessionData }` object from `findCards()` instead of publishing correctly:
- `brokenCards` (line 907–909): now destructures and pushes `sessionData` via `this.added()`, returns `cursors`
- `nextPage` (line 919–921): same fix
- `previousPage` (line 931–933): same fix
**`client/lib/cardSearch.js`** — `getResults()` had two bugs:
- A `directCards` undefined variable reference (would throw a `ReferenceError` at runtime)
- A fallback when `sessionData` is null that queried `Cards` filtered by `dueAt` — completely unrelated to search, and likely the direct cause of "1 card shown" (whichever card had a due date in MiniMongo)
The replacement uses `Cards.find({ _id: { $in: this.sessionData.cards } })` to fetch all matching cards in a single query, which is both correct and avoids the per-ID `ReactiveCache` staleness issues. When `sessionData` is null (subscription not ready yet), it now returns an empty array rather than random due-date cards.
Thanks to Meridiana and xet7 !
Fixes#6322
**[`client/00-startup.js`](client/00-startup.js)**
1. **Build chunks / language popup** — Sets `__webpack_public_path__` at module evaluation time (before any `import()` expressions run, which only happen inside `Meteor.startup` callbacks). Reads `window.__meteor_runtime_config__.ROOT_URL`, extracts the pathname (e.g. `/wekan`), and sets the prefix so rspack loads chunks from `/wekan/build-chunks/...` instead of `/build-chunks/...`.
2. **Service worker** — Replaces the hardcoded `/pwa-service-worker.js` path with `new URL('pwa-service-worker.js', Meteor.absoluteUrl()).pathname`, which automatically includes the context path.
**[`models/lib/universalUrlGenerator.js`](models/lib/universalUrlGenerator.js)**
3. **Attachment download links** — Added `getRootUrlPath()` which reads the pathname from ROOT_URL on the client (returns `''` on the server, where `WebApp.handlers` already strips the prefix before routing). `generateUniversalFileUrl` and `generateFallbackUrl` now prepend this prefix, so generated URLs are `/wekan/cdn/storage/attachments/...` instead of `/cdn/storage/attachments/...`.
**[`server/routes/universalFileServer.js`](server/routes/universalFileServer.js)**
4. **Redirect Location headers** — The `/attachments/` and `/avatars/` fallback routes issued relative-path redirects (`/cdn/storage/...`) that ignored the context path. They now use `Meteor.absoluteUrl(...)` which produces a correct absolute URL including the path prefix.
- Add !important to list header background/border overrides so they are
not overridden by list.css rules that use !important
- Add !important to list background and border-left overrides to prevent
the default #dedede background and 1px border from showing
- Add color overrides for pop-over-list links: white for CleanDark,
dark for CleanLight, to counter the global color:#000 !important in
popup.css (regression introduced with the popup overflow fixes)
Fixes#6168
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>