Skip to content

Commit bcc8f92

Browse files
✨ add no member/label option in filters
1 parent 1d97b58 commit bcc8f92

File tree

11 files changed

+213
-32
lines changed

11 files changed

+213
-32
lines changed

client/src/components/BoardActions/BoardActions.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ const BoardActions = React.memo(
1313
filterText,
1414
allUsers,
1515
filterUsers,
16+
includeCardsWithoutMembers,
1617
boardLabels,
1718
filterLabels,
19+
includeCardsWithoutLabels,
1820
boardMemberships,
1921
isCurrentUserMember,
2022
canEdit,
@@ -100,7 +102,9 @@ const BoardActions = React.memo(
100102
filterText={filterText}
101103
boardLabels={boardLabels}
102104
filterLabels={filterLabels}
105+
includeCardsWithoutLabels={includeCardsWithoutLabels}
103106
filterUsers={filterUsers}
107+
includeCardsWithoutMembers={includeCardsWithoutMembers}
104108
boardMemberships={boardMemberships}
105109
canEdit={canEdit}
106110
onTextFilterUpdate={onTextFilterUpdate}
@@ -204,8 +208,10 @@ BoardActions.propTypes = {
204208
/* eslint-disable react/forbid-prop-types */
205209
allUsers: PropTypes.array.isRequired,
206210
filterUsers: PropTypes.array.isRequired,
211+
includeCardsWithoutMembers: PropTypes.bool.isRequired,
207212
boardLabels: PropTypes.array.isRequired,
208213
filterLabels: PropTypes.array.isRequired,
214+
includeCardsWithoutLabels: PropTypes.bool.isRequired,
209215
boardMemberships: PropTypes.array.isRequired,
210216
/* eslint-enable react/forbid-prop-types */
211217
canEdit: PropTypes.bool.isRequired,

client/src/components/BoardActions/Filters/Filters.jsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ const Filters = React.memo(
1818
filterText,
1919
boardLabels,
2020
filterLabels,
21+
includeCardsWithoutLabels,
2122
filterUsers,
23+
includeCardsWithoutMembers,
2224
boardMemberships,
2325
canEdit,
2426
onTextFilterUpdate,
@@ -61,6 +63,8 @@ const Filters = React.memo(
6163
<BoardMembershipsPopover
6264
items={boardMemberships}
6365
currentUserIds={filterUsers.map((user) => user.id)}
66+
displayNoMemberOption
67+
includeCardsWithoutMembers={includeCardsWithoutMembers}
6468
title="common.filterByMembers"
6569
onUserSelect={onUserAdd}
6670
onUserDeselect={onUserRemove}
@@ -76,11 +80,16 @@ const Filters = React.memo(
7680
filterUsers.length > 0 && styles.membersButtonFilled,
7781
)}
7882
>
79-
{filterUsers.length === 0 && <span className={styles.filterTitle}>Membres</span>}
83+
{filterUsers.length === 0 && !includeCardsWithoutMembers && (
84+
<span className={styles.filterTitle}>Membres</span>
85+
)}
86+
{includeCardsWithoutMembers && (
87+
<div className={styles.noMember}>
88+
<Icon name="person_off" type="outlined" aria-hidden="true" size="small" />
89+
</div>
90+
)}
8091
{filterUsers.map((user) => (
81-
<span key={user.id} className={styles.filterItem}>
82-
<User name={user.name} avatarUrl={user.avatarUrl} size="small" />
83-
</span>
92+
<User key={user.id} name={user.name} avatarUrl={user.avatarUrl} size="small" />
8493
))}
8594
</Button>
8695
</BoardMembershipsPopover>
@@ -89,6 +98,8 @@ const Filters = React.memo(
8998
<LabelsPopover
9099
items={boardLabels}
91100
currentIds={filterLabels.map((label) => label.id)}
101+
displayNoLabelOption
102+
includeCardsWithoutLabels={includeCardsWithoutLabels}
92103
title="common.filterByLabels"
93104
canEdit={canEdit}
94105
onSelect={onLabelAdd}
@@ -109,11 +120,16 @@ const Filters = React.memo(
109120
filterLabels.length > 0 && styles.labelsButtonFilled,
110121
)}
111122
>
112-
{filterLabels.length === 0 && <span className={styles.filterTitle}>Etiquettes</span>}
123+
{filterLabels.length === 0 && !includeCardsWithoutLabels && (
124+
<span className={styles.filterTitle}>Etiquettes</span>
125+
)}
126+
{includeCardsWithoutLabels && (
127+
<div className={styles.noLabel}>
128+
<Icon name="label_off" type="outlined" aria-hidden="true" size="small" />
129+
</div>
130+
)}
113131
{filterLabels.map((label) => (
114-
<span key={label.id} className={styles.filterItem}>
115-
<Label name={label.name} color={label.color} size="small" />
116-
</span>
132+
<Label key={label.id} name={label.name} color={label.color} size="small" />
117133
))}
118134
</Button>
119135
</LabelsPopover>
@@ -128,7 +144,9 @@ Filters.propTypes = {
128144
/* eslint-disable react/forbid-prop-types */
129145
boardLabels: PropTypes.array.isRequired,
130146
filterLabels: PropTypes.array.isRequired,
147+
includeCardsWithoutLabels: PropTypes.bool.isRequired,
131148
filterUsers: PropTypes.array.isRequired,
149+
includeCardsWithoutMembers: PropTypes.bool.isRequired,
132150
boardMemberships: PropTypes.array.isRequired,
133151
/* eslint-enable react/forbid-prop-types */
134152
canEdit: PropTypes.bool.isRequired,

client/src/components/BoardActions/Filters/Filters.module.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,14 @@
2424
}
2525
}
2626

27+
.noMember, .noLabel {
28+
display: flex;
29+
align-items: center;
30+
justify-content: center;
31+
width: 24px;
32+
height: 24px;
33+
border: 1px solid var(--c--theme--colors--greyscale-800);
34+
border-radius: 100%;
35+
}
36+
2737
}

client/src/containers/BoardActionsContainer.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const mapStateToProps = (state) => {
1515
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
1616
const allUsers = selectors.selectUsers(state);
1717
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
18+
const includeCardsWithoutMembers =
19+
selectors.selectIncludeCardsWithoutMembersForCurrentBoard(state);
20+
const includeCardsWithoutLabels = selectors.selectIncludeCardsWithoutLabelsForCurrentBoard(state);
1821
const boardMemberships = selectors.selectMembershipsForCurrentBoard(state);
1922

2023
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@@ -30,8 +33,10 @@ const mapStateToProps = (state) => {
3033
filterText,
3134
allUsers,
3235
filterUsers,
36+
includeCardsWithoutMembers,
3337
boardLabels,
3438
filterLabels,
39+
includeCardsWithoutLabels,
3540
boardMemberships,
3641
canEdit: isCurrentUserEditor || isCurrentUserOwner,
3742
isCurrentUserMember,

client/src/models/Board.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export default class extends BaseModel {
3030
}),
3131
filterUsers: many('User', 'filterBoards'),
3232
filterLabels: many('Label', 'filterBoards'),
33+
includeCardsWithoutMembers: attr({
34+
getDefault: () => false,
35+
}),
36+
includeCardsWithoutLabels: attr({
37+
getDefault: () => false,
38+
}),
3339
filterText: attr({
3440
getDefault: () => '',
3541
}),
@@ -97,11 +103,19 @@ export default class extends BaseModel {
97103

98104
break;
99105
case ActionTypes.USER_TO_BOARD_FILTER_ADD:
100-
Board.withId(payload.boardId).filterUsers.add(payload.id);
106+
if (payload.id === null) {
107+
Board.withId(payload.boardId).update({ includeCardsWithoutMembers: true });
108+
} else {
109+
Board.withId(payload.boardId).filterUsers.add(payload.id);
110+
}
101111

102112
break;
103113
case ActionTypes.USER_FROM_BOARD_FILTER_REMOVE:
104-
Board.withId(payload.boardId).filterUsers.remove(payload.id);
114+
if (payload.id === null) {
115+
Board.withId(payload.boardId).update({ includeCardsWithoutMembers: false });
116+
} else {
117+
Board.withId(payload.boardId).filterUsers.remove(payload.id);
118+
}
105119

106120
break;
107121
case ActionTypes.PROJECT_CREATE_HANDLE:
@@ -170,11 +184,19 @@ export default class extends BaseModel {
170184
// Initial action only contains boardId and targetProjectId
171185
break;
172186
case ActionTypes.LABEL_TO_BOARD_FILTER_ADD:
173-
Board.withId(payload.boardId).filterLabels.add(payload.id);
187+
if (payload.id === null) {
188+
Board.withId(payload.boardId).update({ includeCardsWithoutLabels: true });
189+
} else {
190+
Board.withId(payload.boardId).filterLabels.add(payload.id);
191+
}
174192

175193
break;
176194
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
177-
Board.withId(payload.boardId).filterLabels.remove(payload.id);
195+
if (payload.id === null) {
196+
Board.withId(payload.boardId).update({ includeCardsWithoutLabels: false });
197+
} else {
198+
Board.withId(payload.boardId).filterLabels.remove(payload.id);
199+
}
178200

179201
break;
180202
case ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD: {

client/src/models/List.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,20 +135,33 @@ export default class extends BaseModel {
135135

136136
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
137137
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.id);
138+
const { includeCardsWithoutMembers } = this.board;
138139

139-
if (filterUserIds.length > 0) {
140+
if (filterUserIds.length > 0 || includeCardsWithoutMembers) {
140141
cardModels = cardModels.filter((cardModel) => {
141142
const users = cardModel.users.toRefArray();
143+
const hasNoMembers = users.length === 0;
144+
const hasMatchingMember = filterUserIds.some((userId) =>
145+
users.some((user) => user.id === userId),
146+
);
142147

143-
return filterUserIds.some((userId) => users.some((user) => user.id === userId));
148+
// Show cards that have no members OR cards that have matching members
149+
return (includeCardsWithoutMembers && hasNoMembers) || hasMatchingMember;
144150
});
145151
}
146152

147-
if (filterLabelIds.length > 0) {
153+
const { includeCardsWithoutLabels } = this.board;
154+
155+
if (filterLabelIds.length > 0 || includeCardsWithoutLabels) {
148156
cardModels = cardModels.filter((cardModel) => {
149157
const labels = cardModel.labels.toRefArray();
158+
const hasNoLabels = labels.length === 0;
159+
const hasMatchingLabel = filterLabelIds.some((labelId) =>
160+
labels.some((label) => label.id === labelId),
161+
);
150162

151-
return filterLabelIds.some((labelId) => labels.some((label) => label.id === labelId));
163+
// Show cards that have no labels OR cards that have matching labels
164+
return (includeCardsWithoutLabels && hasNoLabels) || hasMatchingLabel;
152165
});
153166
}
154167

client/src/selectors/boards.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,42 @@ export const selectFilterUsersForCurrentBoard = createSelector(
282282
},
283283
);
284284

285+
export const selectIncludeCardsWithoutMembersForCurrentBoard = createSelector(
286+
orm,
287+
(state) => selectPath(state).boardId,
288+
({ Board }, id) => {
289+
if (!id) {
290+
return false;
291+
}
292+
293+
const boardModel = Board.withId(id);
294+
295+
if (!boardModel) {
296+
return false;
297+
}
298+
299+
return boardModel.includeCardsWithoutMembers || false;
300+
},
301+
);
302+
303+
export const selectIncludeCardsWithoutLabelsForCurrentBoard = createSelector(
304+
orm,
305+
(state) => selectPath(state).boardId,
306+
({ Board }, id) => {
307+
if (!id) {
308+
return false;
309+
}
310+
311+
const boardModel = Board.withId(id);
312+
313+
if (!boardModel) {
314+
return false;
315+
}
316+
317+
return boardModel.includeCardsWithoutLabels || false;
318+
},
319+
);
320+
285321
export const selectFilterLabelsForCurrentBoard = createSelector(
286322
orm,
287323
(state) => selectPath(state).boardId,
@@ -340,4 +376,6 @@ export default {
340376
selectFilterLabelsForCurrentBoard,
341377
selectFilterTextForCurrentBoard,
342378
selectIsBoardWithIdExists,
379+
selectIncludeCardsWithoutMembersForCurrentBoard,
380+
selectIncludeCardsWithoutLabelsForCurrentBoard,
343381
};

client/src/steps/BoardMembershipsStep/BoardMembershipsStep.jsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@ import User from '../../ui/User';
1111
import styles from './BoardMembershipsStep.module.scss';
1212

1313
const BoardMembershipsStep = React.memo(
14-
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
14+
({
15+
items,
16+
currentUserIds,
17+
displayNoMemberOption,
18+
includeCardsWithoutMembers,
19+
title,
20+
onUserSelect,
21+
onUserDeselect,
22+
onBack,
23+
}) => {
1524
const [t] = useTranslation();
1625
const [search, handleSearchChange] = useField('');
1726
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
@@ -63,6 +72,28 @@ const BoardMembershipsStep = React.memo(
6372
/>
6473
</div>
6574
<div className={styles.filterList}>
75+
{displayNoMemberOption && (
76+
<div className={styles.filterItem}>
77+
<Checkbox
78+
checked={includeCardsWithoutMembers}
79+
onChange={() => {
80+
if (includeCardsWithoutMembers) {
81+
handleUserDeselect(null);
82+
} else {
83+
handleUserSelect(null);
84+
}
85+
}}
86+
label={
87+
<div className={styles.filterLabel}>
88+
<div className={styles.userAvatar}>
89+
<Icon name="person_off" type="outlined" aria-hidden="true" />
90+
</div>
91+
<span className={styles.userName}>Aucun membre</span>
92+
</div>
93+
}
94+
/>
95+
</div>
96+
)}
6697
{filteredItems.map((membership) => (
6798
<div className={styles.filterItem} key={membership.user.id}>
6899
<Checkbox
@@ -100,7 +131,9 @@ BoardMembershipsStep.propTypes = {
100131
/* eslint-disable react/forbid-prop-types */
101132
items: PropTypes.array.isRequired,
102133
currentUserIds: PropTypes.array.isRequired,
134+
displayNoMemberOption: PropTypes.bool.isRequired,
103135
/* eslint-enable react/forbid-prop-types */
136+
includeCardsWithoutMembers: PropTypes.bool.isRequired,
104137
title: PropTypes.string,
105138
onUserSelect: PropTypes.func.isRequired,
106139
onUserDeselect: PropTypes.func.isRequired,

client/src/steps/BoardMembershipsStep/BoardMembershipsStep.module.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959

6060
.userAvatar {
6161
flex-shrink: 0;
62+
display: flex;
63+
align-items: center;
64+
justify-content: center;
6265
}
6366

6467
span {

0 commit comments

Comments
 (0)