Swimlanes: a shared card no longer appears in every swimlane (doubled cards).

In swimlanes view a card with swimlaneId null/'' (a 'shared' / pre-migration card) was surfaced in EVERY swimlane: swimlaneMembershipSelector used { swimlaneId: { $in: [swimlaneId, null, ''] } } for non-first swimlanes, and filterCardsByListAndSwimlane matched null/''/missing there too. On a board with several swimlanes the same shared card therefore rendered once per swimlane -- the reported 'doubled cards/entries', which also bloats the DOM (contributing to the search hang / can't-scroll on big boards). A non-first swimlane now matches ONLY its own cards ({ swimlaneId }); shared and orphaned cards surface ONCE, in the FIRST swimlane (the $nin branch, unchanged), so a card is never shown in more than one swimlane. Still a single field clause, so the #6441 board-wide label-filter fix is preserved (verified in the tests).

Tests: tests/swimlaneFilter.test.cjs updated -- a non-first swimlane matches only its own cards; shared/orphaned cards appear once in the first swimlane and are still filtered by the active label filter; #6441 board-wide filtering for cards that HAVE a swimlaneId is unchanged. 18 checks pass.

Thanks to xet7.
This commit is contained in:
Lauri Ojansivu committed 2026-07-22 16:22:39 +03:00
1 parent 501ed7a377
commit c90d6eaa40
3 files changed
+43 -26

No files matched your search

+13 -5
View File
@@ -52,13 +52,21 @@
function swimlaneMembershipSelector(swimlaneId, otherSwimlaneIds) {
if (!swimlaneId) return {};
if (Array.isArray(otherSwimlaneIds)) {
// First swimlane: everything in the list that is NOT owned by another
// existing swimlane (own id, null/'', missing, or orphaned).
// FIRST swimlane: everything in the list NOT owned by another existing
// swimlane — its own cards, shared cards (null/''/missing) AND orphaned cards
// (a swimlaneId pointing at a deleted swimlane). This is where shared/orphaned
// cards surface — ONCE.
return { swimlaneId: { $nin: otherSwimlaneIds } };
}
// `$in` with null matches swimlaneId === null AND a missing swimlaneId field;
// '' covers the empty-string value from the shared-lists era.
return { swimlaneId: { $in: [swimlaneId, null, ''] } };
// NON-first swimlane: ONLY its own cards. Shared cards (null/'') are NOT
// surfaced here — they appear once, in the FIRST swimlane above — so a card is
// never shown in more than one swimlane. Previously this was
// `{ swimlaneId: { $in: [swimlaneId, null, ''] } }`, which put every shared /
// pre-migration card (swimlaneId null) into EVERY swimlane, so on a multi-
// swimlane board those cards appeared once per swimlane — the reported "doubled
// cards". Still a single field clause (no second $or), so the #6441 board-wide
// filter fix is preserved.
return { swimlaneId };
}
// Base (unfiltered) selector for the active cards of one list, optionally
+9 -10
View File
@@ -18,10 +18,12 @@ const Lists = new Mongo.Collection('lists');
//
// - When `swimlaneId` is undefined (no swimlane context), every card in the
// list is returned (preserves the historical "select all in list" behavior).
// - When `swimlaneId` is provided, a card matches when its `swimlaneId` equals
// the given value, OR the card has no swimlane at all (null / '' / missing),
// mirroring `cards()` so orphaned/pre-migration cards stay selectable in
// every swimlane.
// - When `swimlaneId` is provided (a NON-first swimlane), a card matches only
// when its `swimlaneId` equals the given value. Shared / pre-migration cards
// (swimlaneId null / '' / missing) are NOT surfaced here — they surface once,
// in the FIRST swimlane (the otherSwimlaneIds branch), so a card is never
// selected in more than one swimlane (mirrors swimlaneMembershipSelector; the
// "doubled cards" fix).
// - #6443: when `otherSwimlaneIds` is provided (the board's OTHER swimlane ids,
// i.e. this is the FIRST swimlane), a card also matches when its swimlaneId is
// "orphaned" — a non-empty value that is not one of the board's existing
@@ -46,12 +48,9 @@ export function filterCardsByListAndSwimlane(cards, listId, swimlaneId, otherSwi
// swimlane (own id, null/'', missing, or orphaned all pass).
return !otherSwimlaneIds.includes(cardSwimlaneId);
}
return (
cardSwimlaneId === swimlaneId ||
cardSwimlaneId === null ||
cardSwimlaneId === undefined ||
cardSwimlaneId === ''
);
// NON-first swimlane: ONLY its own cards — shared cards surface once, in the
// first swimlane above.
return cardSwimlaneId === swimlaneId;
});
}
+21 -11
View File
@@ -88,9 +88,11 @@ const BACKGROUND = 'swimlane-background';
const LIST = 'list-1';
// --- swimlaneMembershipSelector ---------------------------------------------
test('swimlaneMembershipSelector uses a single $in clause, not a top-level $or', () => {
test('a NON-first swimlane clause is a single own-swimlane match (no $or)', () => {
const sel = swimlaneMembershipSelector(BACKGROUND);
assert.deepStrictEqual(sel, { swimlaneId: { $in: [BACKGROUND, null, ''] } });
// Doubled-cards fix: a non-first swimlane matches ONLY its own cards; shared
// cards (null/'') surface once, in the first swimlane (the $nin branch).
assert.deepStrictEqual(sel, { swimlaneId: BACKGROUND });
assert.ok(!('$or' in sel), 'must NOT introduce a top-level $or (#6441)');
});
@@ -99,12 +101,12 @@ test('swimlaneMembershipSelector is empty without a swimlane context', () => {
assert.deepStrictEqual(swimlaneMembershipSelector(''), {});
});
test('the swimlane clause matches the swimlane plus orphaned/shared cards', () => {
test('a non-first swimlane clause matches ONLY its own cards, not shared/orphaned', () => {
const sel = swimlaneMembershipSelector(BACKGROUND);
assert.ok(docMatches(sel, { swimlaneId: BACKGROUND }));
assert.ok(docMatches(sel, { swimlaneId: null }));
assert.ok(docMatches(sel, { swimlaneId: '' }));
assert.ok(docMatches(sel, {}), 'missing swimlaneId is treated as orphaned');
assert.ok(!docMatches(sel, { swimlaneId: null }), 'shared card surfaces in the FIRST swimlane, not here');
assert.ok(!docMatches(sel, { swimlaneId: '' }), 'empty/shared surfaces in the first swimlane');
assert.ok(!docMatches(sel, {}), 'missing swimlaneId surfaces in the first swimlane');
assert.ok(!docMatches(sel, { swimlaneId: FOCUS }), 'other swimlane excluded');
});
@@ -162,15 +164,22 @@ test('#6441: matching result is identical across swimlanes for the same card', (
});
});
test('#6441: an orphaned (no swimlane) card is filtered consistently in every swimlane', () => {
test('shared/orphaned (no-swimlane) cards surface ONCE, in the first swimlane (not every)', () => {
// Doubled-cards fix: previously these matched in EVERY swimlane; now a non-first
// swimlane does not show them, and the first swimlane shows them once. The label
// filter still applies (an unlabelled card is hidden even in the first swimlane).
const orphanLabelled = { _id: 'c7', listId: LIST, archived: false }; // no swimlaneId, no labels
const orphanWithLabel = { _id: 'c8', listId: LIST, archived: false, labelIds: [RECURRING] };
// NON-first swimlanes: shared cards are not shown (no duplication).
[FOCUS, BACKGROUND].forEach(swimlaneId => {
const sel = filteredListCardsSelector(LIST, swimlaneId, labelFilterSelector(RECURRING));
assert.ok(!docMatches(sel, orphanLabelled), `orphaned unlabelled card hidden in ${swimlaneId}`);
assert.ok(docMatches(sel, orphanWithLabel), `orphaned labelled card shown in ${swimlaneId}`);
assert.ok(!docMatches(sel, orphanWithLabel), `shared card NOT in non-first swimlane ${swimlaneId}`);
});
// FIRST swimlane (otherSwimlaneIds passed): shown once, still filtered by label.
const firstSel = filteredListCardsSelector(LIST, BACKGROUND, labelFilterSelector(RECURRING), [FOCUS]);
assert.ok(docMatches(firstSel, orphanWithLabel), 'shared labelled card shown once in the first swimlane');
assert.ok(!docMatches(firstSel, orphanLabelled), 'unlabelled shared card still hidden by the label filter');
});
// --- do not regress other filter types --------------------------------------
@@ -212,10 +221,11 @@ test('#6443: first swimlane surfaces own, shared AND orphaned cards; excludes ot
assert.ok(!docMatches(sel, { swimlaneId: FOCUS }), 'card owned by another existing swimlane excluded');
});
test('#6443: a non-first swimlane does NOT show orphaned cards (own + shared only)', () => {
test('a non-first swimlane shows ONLY its own cards (shared + orphaned surface once, in the first)', () => {
const sel = swimlaneMembershipSelector(FOCUS); // no otherSwimlaneIds passed
assert.ok(docMatches(sel, { swimlaneId: FOCUS }));
assert.ok(docMatches(sel, { swimlaneId: null }));
assert.ok(!docMatches(sel, { swimlaneId: null }), 'shared card is NOT duplicated into non-first swimlanes');
assert.ok(!docMatches(sel, { swimlaneId: '' }), 'empty/shared card not duplicated here');
assert.ok(!docMatches(sel, { swimlaneId: DELETED }), 'orphaned card is not duplicated into non-first swimlanes');
});