Merge pull request #1683 from symonbaikov/feat/negative-label-filter

Add negative label filtering for cards

Two conflicts, both from work that landed while this branch was open.

In the endless list query, master had added an ORDER BY so the cursor and the
limit agree. The exclusion clause belongs in the WHERE part, so it is placed
before it rather than after; the other way round the statement does not parse.

The label item had been restructured here for the tri-state filter and had
gained a tooltip on master. The restructured version is kept and the tooltip
put back on top of it, which also brings back the translation hook this branch
had dropped.
This commit is contained in:
Daniel Hiller
2026-09-17 00:46:31 +02:00
23 changed files with 641 additions and 238 deletions
+30 -6
View File
@@ -7,11 +7,12 @@ import { attr, fk, many } from 'redux-orm';
import BaseModel from './BaseModel';
import buildSearchParts from '../utils/build-search-parts';
import filterCardLabels from '../utils/filter-card-labels';
import { isListKanban } from '../utils/record-helpers';
import { recallBoardView } from '../utils/board-view-memory';
import ActionTypes from '../constants/ActionTypes';
import Config from '../constants/Config';
import { BoardContexts, BoardViews } from '../constants/Enums';
import { BoardContexts, BoardViews, LabelFilterModes } from '../constants/Enums';
const prepareFetchedBoard = (board) => ({
...board,
@@ -67,6 +68,7 @@ export default class extends BaseModel {
}),
filterUsers: many('User', 'filterBoards'),
filterLabels: many('Label', 'filterBoards'),
filterExcludedLabels: many('Label', 'filterExcludedBoards'),
};
static reducer({ type, payload }, Board) {
@@ -258,6 +260,29 @@ export default class extends BaseModel {
Board.withId(payload.boardId).filterLabels.remove(payload.id);
break;
case ActionTypes.LABEL_FILTER_IN_BOARD_UPDATE: {
const boardModel = Board.withId(payload.boardId);
try {
boardModel.filterLabels.remove(payload.id);
} catch {
/* empty */
}
try {
boardModel.filterExcludedLabels.remove(payload.id);
} catch {
/* empty */
}
if (payload.mode === LabelFilterModes.INCLUDE) {
boardModel.filterLabels.add(payload.id);
} else if (payload.mode === LabelFilterModes.EXCLUDE) {
boardModel.filterExcludedLabels.add(payload.id);
}
break;
}
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH:
Board.withId(payload.boardId).update({
isActivitiesFetching: true,
@@ -385,12 +410,10 @@ export default class extends BaseModel {
}
const filterLabelIds = this.filterLabels.toRefArray().map((label) => label.id);
const filterExcludedLabelIds = this.filterExcludedLabels.toRefArray().map((label) => label.id);
if (filterLabelIds.length > 0) {
cardModels = cardModels.filter((cardModel) => {
const labels = cardModel.labels.toRefArray();
return labels.some((label) => filterLabelIds.includes(label.id));
});
if (filterLabelIds.length > 0 || filterExcludedLabelIds.length > 0) {
cardModels = filterCardLabels(cardModels, filterLabelIds, filterExcludedLabelIds);
}
return cardModels;
@@ -448,6 +471,7 @@ export default class extends BaseModel {
deleteClearable() {
this.filterUsers.clear();
this.filterLabels.clear();
this.filterExcludedLabels.clear();
}
deleteRelated(exceptMemberUserId, soft) {