Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (218)

v9.81

2026-07-09

v9.81 2026-07-09 WeKan ® release

This release adds the following new features:

  • Feature #5394: the Link-card popup’s Cards dropdown is now sorted alphabetically: In the “Link to this card” popup, the Cards pull-down list is now sorted alphabetically by card title (case-insensitive, locale/numeric aware) instead of board sort order, so a card can be found on boards with many cards. Thanks to xet7.
  • Feature #5396: edit Lists (title, color) via the REST API + api.py commands: Lists can now be edited through the REST API like cards can. The endpoint PUT /api/boards/:boardId/lists/:listId already accepted title, color, starred and wipLimit, but the color was stored unvalidated; it now validates the color with normalizeListColor (a named palette color or a custom #rrggbb hex) and rejects an unknown color with a clear 400 instead of silently storing None. The pure field/validation logic lives in a Meteor-free helper models/lib/listApiUpdate.js and is unit tested in tests/listApiUpdate.test.cjs. The api.py reference CLI gains two new commands mirroring editcard/editcardcolor: editlist BOARDID LISTID NEWLISTTITLE and editlistcolor BOARDID LISTID COLOR. Thanks to C0rn3j and xet7.
  • Feature #5514: custom color-wheel (RGB/hex) picker with automatic readable text contrast: The color pickers that previously offered only a fixed set of named colors now also include a native color wheel (<input type="color">), so any #rrggbb color can be chosen. The wheel was added alongside the existing swatches for card labels (“categories”), swimlanes (“tabs”), lists and cards. The schema color fields accept a custom hex in addition to the named palette, and existing named-color data keeps working; a stored hex is rendered with an inline background-color instead of the named CSS class. A new pure, Meteor-free helper models/lib/contrastColor.js computes a readable text color from sRGB relative luminance (white text on dark backgrounds, black on light), maps the named palette to hex, and validates/normalizes hex; it is applied as an inline text color wherever a chosen color is a background behind text (label chips, swimlane / list headers, minicard, card details header), so text stays readable on any color. Covered by tests/contrastColor.test.cjs (20 assertions pass). Thanks to Ruyeex and xet7.
  • Feature #5621: Rules can set a date field to a custom time (value + minute/hour/day/week/month later): The Rules “Set date relative to now” action previously only offset a date field (Start / Due / End / Received) by a whole number of DAYS. It now has a unit selector, so a rule can set a date field to now + <value> <unit> later, where the unit is minute(s) / hour(s) / day(s) / week(s) / month(s); negative values move the date earlier. Months use a real calendar-month add (e.g. keeping the same day-of-month) rather than a fixed 30-day approximation. Existing rules created before this change have no stored unit and keep working exactly as before (no unit ⇒ days), so the change is fully backward compatible. The offset math lives in a new pure, Meteor-free helper models/lib/relativeDateOffset.js used by server/rulesHelper.js and covered by tests/relativeDateOffset.test.cjs (15 assertions pass). Note that the related “overdue” Rules trigger and the “set date field to now” action from the same feature request already existed in an earlier release; this change adds only the missing custom-time unit selector. Thanks to xet7.

and fixes the following bugs:

  • Fix #5351: users are auto-added to organizations matching their email domain on sign-up: the Organization setting “Automatically add users with the domain name” (org.orgAutoAddUsersWithDomainName) could be configured, but nothing at sign-up ever read it, so a new user whose email domain matched an organization was never added to it; the Accounts.onCreateUser hook now, on every non-admin sign-up path (password registration, LDAP, invitation code, new OIDC user), adds the user to each organization whose configured domain exactly matches the domain part of their email (case-insensitive, exact — a subdomain does not match and an empty org domain matches nobody), using the same { orgId, orgDisplayName } membership shape used everywhere else and never duplicating an existing membership, with the matching decision extracted into a Meteor-free, unit-tested orgsToAutoAddForEmail helper. Thanks to xet7.
  • Fix #5369: the Activities show/hide control is now a clear eye / eye-slash icon toggle: the Activities panel’s show/hide control was a generic, unlabeled material toggle switch whose ON/OFF meaning was counterintuitive; it is replaced on the card details Activities panel with an eye / eye-slash icon toggle (open eye = activities shown, crossed eye = activities hidden) that mirrors the login/register password-visibility toggle, so the icon reflects the current visibility, clicking flips it, and the tooltip states the action (Show activities / Hide activities). For consistency the board sidebar Activities toggle, which drives the same showActivities state, was switched from the check / empty-square icon to the same eye / eye-slash toggle. The underlying show/hide setting and its persistence are unchanged. Thanks to xet7.
  • Fix #5442: outgoing webhooks now include the label name on add/remove label: the addedLabel/removedLabel outgoing webhook (and notification) text showed a bare, generic “label” with no name, because labels are embedded in the board document but the Activities label() helper looked the label id up in the Cards collection and always returned undefined, so the __label__ token was never filled; the hook now resolves the label from the already-loaded board via getLabelById and a pure, unit-tested labelDisplayName helper (name, then color for a nameless label as shown in the UI, then the id), so the webhook always carries the label’s display name. Thanks to xet7.
  • Fix #5482: adding/editing a card description now triggers outgoing webhooks: outgoing webhooks fire only when an operation logs an activity (the Activities.after.insert hook posts to the board’s webhooks), but changing a card’s description created no activity — unlike title/date changes — so no webhook was sent; the Cards before.update hook now logs an a-changedDescription activity on first-time set and later edits (but not on no-op / empty-to-empty saves), so the existing webhook hook fires, with a Meteor-free unit-tested descriptionChanged helper. Thanks to xet7.
  • Fix: the Rules Workflow view is now fully translatable via i18n: the Rules Workflow view rendered its trigger/action palette chips (“Card is created”, “Move card to top”, “Set received date to now”, etc.) as hardcoded English regardless of the UI language, while the rest of the page was translated; each palette entry now carries an i18n key translated at render time via TAPi18n.__ (reusing existing rule keys where they fit, plus new r-w-* keys for workflow-only labels), and the slot clear tooltip now uses the existing r-remove key, so the whole view follows the selected language. Thanks to xet7.
  • Fix: deleting a board rule no longer fails with “Access denied [403]”: deleting a rule ran three separate client-side Collection.remove() calls (Rules + Triggers + Actions), each gated by a per-collection allow() rule that resolved the board from that document’s own boardId; when a trigger/action document had no resolvable boardId (legacy docs, or docs not published to the client) the board came back null, allowIsBoardAdmin returned false, and Meteor rejected the mutation with 403 “Access denied” — so the delete failed even for a legitimate board admin. Rule deletion now goes through a new server method rules.deleteRule that authorizes once (active board admin or site admin) and removes the rule, its trigger and its action server-side, bypassing the brittle client allow/deny; the permission decision is a Meteor-free, unit-tested helper and no permission is loosened. Thanks to xet7.
  • Fix #5510: adding a board label via the REST API no longer errors/hangs: PUT /api/boards/:boardId/labels only sent a response when the body had a label key, so a body without one hung until the client timed out, and a bare-string label pushed a schema-invalid label and returned 200; the handler now always returns JSON (2xx on success, 4xx on bad input, real error status otherwise) via a pure, unit-tested input helper. Thanks to xet7.
  • Fix #5604: CSV export no longer crashes on boards with dangling references: exporting a large/old board to CSV failed with “Couldn’t download - Network issue” because Exporter.buildCsv read .title/.username/.name directly on the result of looking up a card’s deleted list/swimlane/owner/member/assignee/label/customField by id, throwing “Cannot read property ‘title’ of undefined”; the per-card row builder is now a null-safe helper that emits a blank cell for missing references while keeping identical output for well-formed cards. Thanks to xet7.
  • Fix #5656: the Calendar view now honors the active board filters: the Calendar view queried cards by board and date only and ignored the active Filter sidebar (member / assignee / due-date / label / custom-field), so it showed every card in the interval unlike the Board / Lists / Swimlanes views; it now ANDs the Filter selector into its query and refetches when the filter changes. Thanks to kerier and xet7.
  • Fix #6442: All Boards “Custom (drag order)” — drop now persists the reorder: Follow-up to #6439, which restored the drag preview but left the drop a no-op: dragging a board on the All Boards page in Custom (drag order) mode showed the dashed-border preview, but releasing it snapped the board back without reordering. The drop handler built the current on-screen order from el.classList[0] of each .js-board, but the item is li.js-board(class="{{_id}} …") and Jade emits the literal js-board class FIRST, so classList[0] was the string "js-board" for every board — never the board _id. The ordered ids were therefore ['js-board','js-board',…], the (correct, unit-tested) computeReorderedSortIndex helper could not find the dragged/target ids among them, returned null, and nothing was written to profile.boardSortIndex; the preview still worked because it is driven by the dragover CSS class, independent of the id. The fix reads each board’s _id from its Blaze data context (the same source dragstart uses via this._id) instead of the literal class, so the real display order reaches the reorder helper and the drop persists. Guarded by a new case in tests/boardSortReorder.test.cjs (the wrong-class extraction yields no mapping; real ids reorder). Thanks to jullbo and xet7.
  • Fix #6443: cards on a deleted swimlane are invisible in swimlane view: On some old boards a swimlane was deleted while its cards kept the now-dangling swimlaneId (an “orphaned” card), so those cards showed no content in swimlane mode even though they worked in list mode. Cards with no swimlane at all (null / '' / missing) already appear in every swimlane, but an orphaned card matched no existing swimlane and so was visible in NO swimlane (while list view, which applies no swimlane scope, still showed it) — exactly the reported symptom. The fix mirrors the existing orphaned-list fallback (Swimlanes.orphanedSwimlaneLists, which surfaces orphaned lists in the first swimlane) for cards: when a list’s cards are fetched for the board’s FIRST swimlane, the swimlane-membership clause becomes a single { swimlaneId: { $nin: <otherSwimlaneIds> } } (everything not owned by another existing swimlane: own id, null/'', missing, or orphaned). It stays a single field clause with no second $or, so the #6441 board-wide label filter still holds, and orphaned cards appear once — in the first swimlane — without a database migration. Threaded through the pure models/lib/swimlaneFilter.js helpers, the in-memory filterCardsByListAndSwimlane, a new List.orphanedCardsSwimlaneIds helper and the cards()/cardsUnfiltered()/allCards() model methods plus the listBody cardsWithLimit render helper. Covered by new cases in tests/swimlaneFilter.test.cjs (18 assertions pass; the #6441 regression guards stay intact). Thanks to xet7.

Thanks to above GitHub users for their contributions and translators for their translations.