mirror of
https://github.com/wekan/wekan.git
synced 2026-11-04 17:10:08 -05:00
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:
1 parent
501ed7a377
commit
c90d6eaa40
3 files changed
+43
-26
No files matched your search
@@ -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
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user