{
+ const selectListById = useMemo(() => selectors.makeSelectListById(), []);
+
+ const list = useSelector((state) => selectListById(state, id));
+
+ const [t] = useTranslation();
+
+ const handleToggleClick = useCallback(() => {
+ if (!list.isPersisted) {
+ return;
+ }
+
+ if (isActive) {
+ onDeselect(id);
+ } else {
+ onSelect(id);
+ }
+ }, [id, isActive, onSelect, onDeselect, list.isPersisted]);
+
+ return (
+
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
+ jsx-a11y/no-static-element-interactions */}
+
+ {list.type !== ListTypes.ACTIVE && (
+
+ )}
+ {list.name || t(`common.${list.type}`)}
+
+
+ );
+});
+
+Item.propTypes = {
+ id: PropTypes.string.isRequired,
+ isActive: PropTypes.bool.isRequired,
+ onSelect: PropTypes.func.isRequired,
+ onDeselect: PropTypes.func.isRequired,
+};
+
+export default Item;
diff --git a/client/src/components/lists/ListsFilterStep/Item.module.scss b/client/src/components/lists/ListsFilterStep/Item.module.scss
new file mode 100644
index 00000000..9329e733
--- /dev/null
+++ b/client/src/components/lists/ListsFilterStep/Item.module.scss
@@ -0,0 +1,54 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+:global(#app) {
+ .name {
+ background: rgba(9, 30, 66, 0.04);
+ border-radius: 3px;
+ color: #17394d;
+ cursor: pointer;
+ flex: 1 1 auto;
+ font-size: 14px;
+ overflow: hidden;
+ padding: 8px 32px 8px 10px;
+ position: relative;
+ text-overflow: ellipsis;
+
+ &:hover {
+ background: rgba(9, 30, 66, 0.08);
+ }
+ }
+
+ .nameActive {
+ opacity: 0.45;
+
+ &:before {
+ bottom: 1px;
+ content: "Г";
+ font-size: 18px;
+ font-weight: normal;
+ line-height: 36px;
+ position: absolute;
+ right: 2px;
+ text-align: center;
+ transform: rotate(-135deg);
+ width: 36px;
+ }
+ }
+
+ .nameIcon {
+ color: rgba(9, 30, 66, 0.24);
+ font-size: 12px;
+ margin: 0 8px 0 0;
+ width: 14px;
+ }
+
+ .wrapper {
+ display: flex;
+ margin-bottom: 4px;
+ max-width: 280px;
+ white-space: nowrap;
+ }
+}
diff --git a/client/src/components/lists/ListsFilterStep/ListsFilterStep.jsx b/client/src/components/lists/ListsFilterStep/ListsFilterStep.jsx
new file mode 100644
index 00000000..2d7f3ac6
--- /dev/null
+++ b/client/src/components/lists/ListsFilterStep/ListsFilterStep.jsx
@@ -0,0 +1,89 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import React, { useEffect, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { Input, Popup } from '../../../lib/custom-ui';
+
+import selectors from '../../../selectors';
+import { useField, useNestedRef } from '../../../hooks';
+import Item from './Item';
+
+import styles from './ListsFilterStep.module.scss';
+
+const ListsFilterStep = React.memo(({ currentIds, title, onSelect, onDeselect, onBack }) => {
+ const lists = useSelector(selectors.selectAvailableListsForCurrentBoard);
+
+ const [t] = useTranslation();
+ const [search, handleSearchChange] = useField('');
+ const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
+
+ const filteredLists = useMemo(
+ () =>
+ lists.filter((list) =>
+ (list.name ? list.name.toLowerCase() : list.type).includes(cleanSearch),
+ ),
+ [lists, cleanSearch],
+ );
+
+ const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
+
+ useEffect(() => {
+ searchFieldRef.current.focus({
+ preventScroll: true,
+ });
+ }, [searchFieldRef]);
+
+ return (
+ <>
+
+ {t(title, {
+ context: 'title',
+ })}
+
+
+
+ {filteredLists.length > 0 && (
+
+ {filteredLists.map((list) => (
+
+ ))}
+
+ )}
+
+ >
+ );
+});
+
+ListsFilterStep.propTypes = {
+ currentIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
+ title: PropTypes.string,
+ onSelect: PropTypes.func.isRequired,
+ onDeselect: PropTypes.func.isRequired,
+ onBack: PropTypes.func,
+};
+
+ListsFilterStep.defaultProps = {
+ title: 'common.filterByLists',
+ onBack: undefined,
+};
+
+export default ListsFilterStep;
diff --git a/client/src/components/lists/ListsFilterStep/ListsFilterStep.module.scss b/client/src/components/lists/ListsFilterStep/ListsFilterStep.module.scss
new file mode 100644
index 00000000..7487283c
--- /dev/null
+++ b/client/src/components/lists/ListsFilterStep/ListsFilterStep.module.scss
@@ -0,0 +1,32 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+:global(#app) {
+ .items {
+ margin-top: 8px;
+ max-height: 60vh;
+ overflow-x: hidden;
+ overflow-y: auto;
+
+ @supports (-moz-appearance: none) {
+ scrollbar-color: rgba(0, 0, 0, 0.32) transparent;
+ scrollbar-width: thin;
+ }
+
+ &::-webkit-scrollbar {
+ width: 9px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-clip: padding-box;
+ border-left: 0.25em transparent solid;
+ border-radius: 3px;
+ }
+ }
+}
diff --git a/client/src/components/lists/ListsFilterStep/index.js b/client/src/components/lists/ListsFilterStep/index.js
new file mode 100644
index 00000000..26267e68
--- /dev/null
+++ b/client/src/components/lists/ListsFilterStep/index.js
@@ -0,0 +1,8 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import ListsFilterStep from './ListsFilterStep';
+
+export default ListsFilterStep;
diff --git a/client/src/constants/ActionTypes.js b/client/src/constants/ActionTypes.js
index a947e279..1475c7e4 100644
--- a/client/src/constants/ActionTypes.js
+++ b/client/src/constants/ActionTypes.js
@@ -272,6 +272,8 @@ export default {
LIST_DELETE__SUCCESS: 'LIST_DELETE__SUCCESS',
LIST_DELETE__FAILURE: 'LIST_DELETE__FAILURE',
LIST_DELETE_HANDLE: 'LIST_DELETE_HANDLE',
+ LIST_TO_BOARD_FILTER_ADD: 'LIST_TO_BOARD_FILTER_ADD',
+ LIST_FROM_BOARD_FILTER_REMOVE: 'LIST_FROM_BOARD_FILTER_REMOVE',
/* Cards */
diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js
index 3a99a4f0..2d39803e 100755
--- a/client/src/constants/EntryActionTypes.js
+++ b/client/src/constants/EntryActionTypes.js
@@ -186,6 +186,8 @@ export default {
LIST_CLEAR_HANDLE: `${PREFIX}/LIST_CLEAR_HANDLE`,
LIST_DELETE: `${PREFIX}/LIST_DELETE`,
LIST_DELETE_HANDLE: `${PREFIX}/LIST_DELETE_HANDLE`,
+ LIST_TO_FILTER_IN_CURRENT_BOARD_ADD: `${PREFIX}/LIST_TO_FILTER_IN_CURRENT_BOARD_ADD`,
+ LIST_FROM_FILTER_IN_CURRENT_BOARD_REMOVE: `${PREFIX}/LIST_FROM_FILTER_IN_CURRENT_BOARD_REMOVE`,
/* Cards */
diff --git a/client/src/entry-actions/lists.js b/client/src/entry-actions/lists.js
index 13d3e7cc..e1e3753f 100755
--- a/client/src/entry-actions/lists.js
+++ b/client/src/entry-actions/lists.js
@@ -95,6 +95,20 @@ const handleListDelete = (list, cards) => ({
},
});
+const addListToFilterInCurrentBoard = (id) => ({
+ type: EntryActionTypes.LIST_TO_FILTER_IN_CURRENT_BOARD_ADD,
+ payload: {
+ id,
+ },
+});
+
+const removeListFromFilterInCurrentBoard = (id) => ({
+ type: EntryActionTypes.LIST_FROM_FILTER_IN_CURRENT_BOARD_REMOVE,
+ payload: {
+ id,
+ },
+});
+
export default {
createListInCurrentBoard,
handleListCreate,
@@ -108,4 +122,6 @@ export default {
handleListClear,
deleteList,
handleListDelete,
+ addListToFilterInCurrentBoard,
+ removeListFromFilterInCurrentBoard,
};
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index bf906854..00b984ee 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -206,6 +206,7 @@ export default {
excludedEvents: 'Excluded events',
expandTaskListsByDefault: 'Expand task lists by default',
filterByLabels_title: 'Filter By Labels',
+ filterByLists_title: 'Filter By Lists',
filterByMembers_title: 'Filter By Members',
forPersonalProjects: 'For personal projects.',
forTeamBasedProjects: 'For team-based projects.',
diff --git a/client/src/models/Board.js b/client/src/models/Board.js
index a4697fe5..71890113 100755
--- a/client/src/models/Board.js
+++ b/client/src/models/Board.js
@@ -63,6 +63,7 @@ export default class extends BaseModel {
}),
filterUsers: many('User', 'filterBoards'),
filterLabels: many('Label', 'filterBoards'),
+ filterLists: many('List', 'filterBoards'),
};
static reducer({ type, payload }, Board) {
@@ -253,6 +254,14 @@ export default class extends BaseModel {
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
Board.withId(payload.boardId).filterLabels.remove(payload.id);
+ break;
+ case ActionTypes.LIST_TO_BOARD_FILTER_ADD:
+ Board.withId(payload.boardId).filterLists.add(payload.id);
+
+ break;
+ case ActionTypes.LIST_FROM_BOARD_FILTER_REMOVE:
+ Board.withId(payload.boardId).filterLists.remove(payload.id);
+
break;
case ActionTypes.ACTIVITIES_IN_BOARD_FETCH:
Board.withId(payload.boardId).update({
@@ -389,6 +398,12 @@ export default class extends BaseModel {
});
}
+ const filterListIds = this.filterLists.toRefArray().map((list) => list.id);
+
+ if (filterListIds.length > 0) {
+ cardModels = cardModels.filter((cardModel) => filterListIds.includes(cardModel.listId));
+ }
+
return cardModels;
}
@@ -444,6 +459,7 @@ export default class extends BaseModel {
deleteClearable() {
this.filterUsers.clear();
this.filterLabels.clear();
+ this.filterLists.clear();
}
deleteRelated(exceptMemberUserId, soft) {
diff --git a/client/src/models/List.js b/client/src/models/List.js
index 2c8d72a0..a4ad68a3 100755
--- a/client/src/models/List.js
+++ b/client/src/models/List.js
@@ -443,6 +443,13 @@ export default class extends BaseModel {
deleteWithRelated(soft) {
this.deleteRelated(soft);
+
+ try {
+ this.board.filterLists.remove(this.id);
+ } catch {
+ /* empty */
+ }
+
this.delete();
}
}
diff --git a/client/src/sagas/core/services/lists.js b/client/src/sagas/core/services/lists.js
index b38e6b6c..deda6a8d 100644
--- a/client/src/sagas/core/services/lists.js
+++ b/client/src/sagas/core/services/lists.js
@@ -261,6 +261,26 @@ export function* handleListDelete(list, cards) {
yield put(actions.handleListDelete(list, cards));
}
+export function* addListToBoardFilter(id, boardId) {
+ yield put(actions.addListToBoardFilter(id, boardId));
+}
+
+export function* addListToFilterInCurrentBoard(id) {
+ const { boardId } = yield select(selectors.selectPath);
+
+ yield call(addListToBoardFilter, id, boardId);
+}
+
+export function* removeListFromBoardFilter(id, boardId) {
+ yield put(actions.removeListFromBoardFilter(id, boardId));
+}
+
+export function* removeListFromFilterInCurrentBoard(id) {
+ const { boardId } = yield select(selectors.selectPath);
+
+ yield call(removeListFromBoardFilter, id, boardId);
+}
+
export default {
createList,
createListInCurrentBoard,
@@ -275,4 +295,8 @@ export default {
handleListClear,
deleteList,
handleListDelete,
+ addListToBoardFilter,
+ addListToFilterInCurrentBoard,
+ removeListFromBoardFilter,
+ removeListFromFilterInCurrentBoard,
};
diff --git a/client/src/sagas/core/watchers/lists.js b/client/src/sagas/core/watchers/lists.js
index 9481e2b9..0ee4a7b7 100644
--- a/client/src/sagas/core/watchers/lists.js
+++ b/client/src/sagas/core/watchers/lists.js
@@ -44,5 +44,11 @@ export default function* listsWatchers() {
takeEvery(EntryActionTypes.LIST_DELETE_HANDLE, ({ payload: { list, cards } }) =>
services.handleListDelete(list, cards),
),
+ takeEvery(EntryActionTypes.LIST_TO_FILTER_IN_CURRENT_BOARD_ADD, ({ payload: { id } }) =>
+ services.addListToFilterInCurrentBoard(id),
+ ),
+ takeEvery(EntryActionTypes.LIST_FROM_FILTER_IN_CURRENT_BOARD_REMOVE, ({ payload: { id } }) =>
+ services.removeListFromFilterInCurrentBoard(id),
+ ),
]);
}
diff --git a/client/src/selectors/boards.js b/client/src/selectors/boards.js
index 4d038494..485b7a33 100644
--- a/client/src/selectors/boards.js
+++ b/client/src/selectors/boards.js
@@ -459,6 +459,24 @@ export const selectFilterLabelIdsForCurrentBoard = createSelector(
},
);
+export const selectFilterListIdsForCurrentBoard = createSelector(
+ orm,
+ (state) => selectPath(state).boardId,
+ ({ Board }, id) => {
+ if (!id) {
+ return id;
+ }
+
+ const boardModel = Board.withId(id);
+
+ if (!boardModel) {
+ return boardModel;
+ }
+
+ return boardModel.filterLists.toRefArray().map((list) => list.id);
+ },
+);
+
export const selectIsBoardWithIdExists = createSelector(
orm,
(_, id) => id,
@@ -491,5 +509,6 @@ export default {
selectActivityIdsForCurrentBoard,
selectFilterUserIdsForCurrentBoard,
selectFilterLabelIdsForCurrentBoard,
+ selectFilterListIdsForCurrentBoard,
selectIsBoardWithIdExists,
};