Files
wekan/models/lib/boardTileData.js
T
Lauri Ojansivu 9ddbc777dd Startup schema upgrade, self-healing attachments, migration speed; fix
#5695 #4897 #4654 #5174 #4825 #5659 #4730 #4419 #4158 #4560 #4036
#1959 #1971.

- Startup schema upgrade (server/lib/schemaUpgradeSteps.js): on start,
  check that data from EVERY old WeKan version is migrated to the newest
  structure and migrate only what is missing - all platforms, MongoDB and
  FerretDB. Version-gated (marker stores WeKan version + datetime of the
  last re-check; one findOne per boot until a new release), non-blocking,
  live dashboard at /schema-upgrade-status showing the product name, fast
  on big databases (distinct() set-joins, updateMany batches). Steps:
  archived-flag backfill, swimlane structure + visibility rescue
  (#1959 #1971: cards under deleted/archived/foreign swimlanes reassigned
  to a visible swimlane; card-insert hook validates supplied swimlaneIds),
  merge-per-swimlane-lists (v7.98/v8.0x list copies merged back into
  shared lists: no duplicate columns in Lists view, everything visible in
  Swimlanes view, cards keep their swimlane), embedded checklist items,
  customFields boardIds, allows* backfills, members isActive, permission
  lowercase, filesystem path healing.
- Migration speed (5-hour reports): insertMany batches in the text phase,
  preloaded target-record maps in the file phases.
- Snap attachment-repair + FILES_ONLY importer mode (see prior commit
  wiring) extended with CFS FileSystem-store coverage
  (ATTACHMENTS_STORE_PATH era), missing-binary reporting, and
  migrate-gridfs-to-fs.mjs bucket fixes.
- OAuth2/OIDC: OAUTH2_LOGIN_STYLE=redirect honored (#5695); concurrent
  logins no longer contaminate each other's user data (#4897); account
  merge no longer wipes avatars/templates (#4560).
- LDAP: background sync fixed (#4654), password login rejected after LDAP
  migration (#4419), LDAP_ENCRYPTION=true actually encrypts (#4158),
  group filter accepts multiple groups and admits admin-sync groups
  (#4036).
- All Boards tiles: per-list card counts + member avatars restored
  non-reactively (#5174 #4825). Uniform default list widths (#5659).
  Declined invitations no longer resurrect membership (#4730).
- Tests with negative cases for every fix (tests/*.test.cjs; suite green
  except the pre-existing environment-only htmljs failure).

Closes #5600: OAUTH2_CA_CERT already works - the fetch migration
(49a272d6c) applies the CA to all OIDC calls via https.Agent.
Closes #3828: board members seeing all board cards is by design (no
per-card visibility model), and organizations/teams now exist.

Thanks to mueschel, ArturRuta, gerardo-junior, fabianrbz, Miuler,
butteredCat-2021, Griiimm, preciousamorc, farwayer, LeoLu-eng, hnb1,
rangersdo and xet7.
2026-07-17 03:07:59 +03:00

120 lines
5.0 KiB
JavaScript

'use strict';
// Pure helpers for the All Boards page board tiles: the per-list card-count
// line ("To Do: 3 Doing: 1 ...") and the board-member avatar row shown on
// each board tile. Extracted so the decision logic is unit-testable in plain
// Node without Meteor (mirrors models/lib/boardSortReorder.js).
//
// Background (#5174 screenshots / #4825 / #4214):
// - The original client helpers computed the counts from reactive
// ReactiveCache.getLists()/getCards() cursors inside the tile helpers,
// which made the All Boards icons re-render in a loop ("icons random
// dance", #4214). They were stubbed out to `return []`, so NO tile ever
// showed its card counts or member avatars again, regardless of the
// Admin Panel settings (hideCardCounterList / hideBoardMemberList) or the
// per-board flags.
// - Before the stubbing, tiles rendered inconsistently (#4825: "Show card
// count per list doesn't deactivate", members never shown): the per-board
// flags (allowsCardCounterList / allowsBoardMemberList, opt-in, default
// false) were evaluated loosely and boards created before the feature
// (#4691) have no field at all.
//
// The fix: a Meteor method (getAllBoardsTileData, server/publications/
// boards.js) computes everything ONCE server-side — list titles + card counts
// via a single grouped query, member ids from the board doc — and the client
// stores the result in a ReactiveVar it sets exactly once (no reactive
// cursors), so #4214 cannot return. These helpers are the pure core:
//
// - showsCardCounterList / showsBoardMemberList: the per-board flags are
// honored STRICTLY (=== true). Disabled or missing flag -> hidden, for
// every board, deterministically (fixes both directions of #4825).
// - buildBoardTileData: folds boards + lists + card counts into the map the
// tiles render from, already gated by those flags so a disabled board
// never even carries the data.
// Whether this board opted in to showing the per-list card counter line on
// its All Boards tile. The schema default is false (opt-in via the board
// sidebar "Show card count per list" checkbox); boards created before the
// feature existed have no field, which must mean "off" — not "sometimes on"
// (#4825).
function showsCardCounterList(board) {
return !!(board && board.allowsCardCounterList === true);
}
// Whether this board opted in to showing the member avatar row on its tile
// ("Show Board members avatars" checkbox). Same strict semantics.
function showsBoardMemberList(board) {
return !!(board && board.allowsBoardMemberList === true);
}
// One entry of the tile's card-counter line, e.g. "To Do: 3".
function formatListCount(title, count) {
const n = typeof count === 'number' && count > 0 ? count : 0;
return `${title || ''}: ${n}`;
}
// Fold card docs (only their listId is needed) into { listId: count }. Used
// by the server method as the portable alternative to a $group aggregation.
function countCardsByListId(cards) {
const counts = {};
(cards || []).forEach((card) => {
if (card && card.listId) {
counts[card.listId] = (counts[card.listId] || 0) + 1;
}
});
return counts;
}
// Build the per-board tile data map:
// { [boardId]: { showLists, showMembers, lists: ['Title: N', ...],
// memberIds: ['userId', ...] } }
//
// - boards: board docs with _id, members, allowsCardCounterList,
// allowsBoardMemberList.
// - lists: list docs with _id, boardId, title, sort (non-archived lists of
// the boards that show counters). Rendered in board order (sort asc, title
// as tiebreaker), like the lists on the board itself.
// - cardCountByListId: { listId: count } for non-archived cards.
//
// Boards whose flag is off (or missing) get empty arrays, so the tile shows
// nothing for them even if a caller forgets to check the show* flags.
function buildBoardTileData(boards, lists, cardCountByListId) {
const counts = cardCountByListId || {};
const tiles = {};
(boards || []).forEach((board) => {
if (!board || !board._id) return;
const showMembers = showsBoardMemberList(board);
tiles[board._id] = {
showLists: showsCardCounterList(board),
showMembers,
lists: [],
memberIds: showMembers
? (board.members || [])
.filter((m) => m && m.userId && m.isActive !== false)
.map((m) => m.userId)
: [],
};
});
const orderedLists = (lists || []).slice().sort((a, b) => {
const sa = a && typeof a.sort === 'number' ? a.sort : 0;
const sb = b && typeof b.sort === 'number' ? b.sort : 0;
if (sa !== sb) return sa - sb;
return String((a && a.title) || '').localeCompare(String((b && b.title) || ''));
});
orderedLists.forEach((list) => {
if (!list || !list.boardId || !list._id) return;
const tile = tiles[list.boardId];
if (!tile || !tile.showLists) return;
tile.lists.push(formatListCount(list.title, counts[list._id]));
});
return tiles;
}
module.exports = {
showsCardCounterList,
showsBoardMemberList,
formatListCount,
countCardsByListId,
buildBoardTileData,
};