diff --git a/client/src/actions/lists.js b/client/src/actions/lists.js
index 1c705d27..78900f16 100644
--- a/client/src/actions/lists.js
+++ b/client/src/actions/lists.js
@@ -201,6 +201,22 @@ const handleListDelete = (list, cards) => ({
},
});
+const addListToBoardFilter = (id, boardId) => ({
+ type: ActionTypes.LIST_TO_BOARD_FILTER_ADD,
+ payload: {
+ id,
+ boardId,
+ },
+});
+
+const removeListFromBoardFilter = (id, boardId) => ({
+ type: ActionTypes.LIST_FROM_BOARD_FILTER_REMOVE,
+ payload: {
+ id,
+ boardId,
+ },
+});
+
export default {
createList,
handleListCreate,
@@ -212,4 +228,6 @@ export default {
handleListClear,
deleteList,
handleListDelete,
+ addListToBoardFilter,
+ removeListFromBoardFilter,
};
diff --git a/client/src/components/boards/BoardActions/Filters.jsx b/client/src/components/boards/BoardActions/Filters.jsx
index 9d0e04f9..e69de29b 100644
--- a/client/src/components/boards/BoardActions/Filters.jsx
+++ b/client/src/components/boards/BoardActions/Filters.jsx
@@ -1,259 +0,0 @@
-/*!
- * Copyright (c) 2024 PLANKA Software GmbH
- * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
- */
-
-import debounce from 'lodash/debounce';
-import React, { useCallback, useMemo, useState } from 'react';
-import classNames from 'classnames';
-import { useDispatch, useSelector } from 'react-redux';
-import { useTranslation } from 'react-i18next';
-import { Icon } from 'semantic-ui-react';
-import { useDidUpdate } from '../../../lib/hooks';
-import { usePopup } from '../../../lib/popup';
-import { Input, Tooltip } from '../../../lib/custom-ui';
-
-import selectors from '../../../selectors';
-import entryActions from '../../../entry-actions';
-import { useNestedRef } from '../../../hooks';
-import UserAvatar from '../../users/UserAvatar';
-import BoardMembershipsStep from '../../board-memberships/BoardMembershipsStep';
-import LabelChip from '../../labels/LabelChip';
-import LabelsStep from '../../labels/LabelsStep';
-import { LabelFilterModes } from '../../../constants/Enums';
-
-import styles from './Filters.module.scss';
-
-const Filters = React.memo(() => {
- const board = useSelector(selectors.selectCurrentBoard);
- const userIds = useSelector(selectors.selectFilterUserIdsForCurrentBoard);
- const labelIds = useSelector(selectors.selectFilterLabelIdsForCurrentBoard);
- const excludedLabelIds = useSelector(selectors.selectFilterExcludedLabelIdsForCurrentBoard);
- const currentUserId = useSelector(selectors.selectCurrentUserId);
-
- const withCurrentUserSelector = useSelector(
- (state) => !!selectors.selectCurrentUserMembershipForCurrentBoard(state),
- );
-
- const dispatch = useDispatch();
- const [t] = useTranslation();
- const [search, setSearch] = useState(board.search);
- const [isSearchFocused, setIsSearchFocused] = useState(false);
-
- const debouncedSearch = useMemo(
- () =>
- debounce((nextSearch) => {
- dispatch(entryActions.searchInCurrentBoard(nextSearch));
- }, 400),
- [dispatch],
- );
-
- const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
-
- const labelModes = useMemo(
- () => ({
- ...labelIds.reduce(
- (result, labelId) => ({
- ...result,
- [labelId]: LabelFilterModes.INCLUDE,
- }),
- {},
- ),
- ...excludedLabelIds.reduce(
- (result, labelId) => ({
- ...result,
- [labelId]: LabelFilterModes.EXCLUDE,
- }),
- {},
- ),
- }),
- [labelIds, excludedLabelIds],
- );
-
- const cancelSearch = useCallback(() => {
- debouncedSearch.cancel();
- setSearch('');
- dispatch(entryActions.searchInCurrentBoard(''));
- searchFieldRef.current.blur();
- }, [dispatch, debouncedSearch, searchFieldRef]);
-
- const handleUserSelect = useCallback(
- (userId) => {
- dispatch(entryActions.addUserToFilterInCurrentBoard(userId));
- },
- [dispatch],
- );
-
- const handleCurrentUserSelect = useCallback(() => {
- dispatch(entryActions.addUserToFilterInCurrentBoard(currentUserId));
- }, [currentUserId, dispatch]);
-
- const handleUserDeselect = useCallback(
- (userId) => {
- dispatch(entryActions.removeUserFromFilterInCurrentBoard(userId));
- },
- [dispatch],
- );
-
- const handleUserClick = useCallback(
- ({
- currentTarget: {
- dataset: { id: userId },
- },
- }) => {
- dispatch(entryActions.removeUserFromFilterInCurrentBoard(userId));
- },
- [dispatch],
- );
-
- const handleLabelClick = useCallback(
- ({
- currentTarget: {
- dataset: { id: labelId },
- },
- }) => {
- dispatch(entryActions.updateLabelFilterInCurrentBoard(labelId, LabelFilterModes.NONE));
- },
- [dispatch],
- );
-
- const handleLabelModeChange = useCallback(
- (labelId, mode) => {
- dispatch(entryActions.updateLabelFilterInCurrentBoard(labelId, mode));
- },
- [dispatch],
- );
-
- const handleSearchChange = useCallback(
- (_, { value }) => {
- setSearch(value);
- debouncedSearch(value);
- },
- [debouncedSearch],
- );
-
- const handleSearchFocus = useCallback(() => {
- setIsSearchFocused(true);
- }, []);
-
- const handleSearchKeyDown = useCallback(
- (event) => {
- if (event.key === 'Escape') {
- cancelSearch();
- }
- },
- [cancelSearch],
- );
-
- const handleSearchBlur = useCallback(() => {
- setIsSearchFocused(false);
- }, []);
-
- const handleCancelSearchClick = useCallback(() => {
- cancelSearch();
- }, [cancelSearch]);
-
- useDidUpdate(() => {
- setSearch(board.search);
- }, [board.search]);
-
- const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
- const LabelsPopup = usePopup(LabelsStep);
-
- const isSearchActive = search || isSearchFocused;
-
- const handleNoMemberClick = useCallback(() => {
- if (board.filterNoMember) {
- dispatch(entryActions.removeNoMemberFromFilterInCurrentBoard());
- } else {
- dispatch(entryActions.setNoMemberToFilterInCurrentBoard());
- }
- }, [dispatch, board.filterNoMember]);
-
- return (
- <>
-
-
-
-
-
- {userIds.length === 0 && withCurrentUserSelector && (
-
-
-
- )}
- {userIds.map((userId) => (
-
-
-
- ))}
-
-
-
-
-
- {labelIds.map((labelId) => (
-
-
-
- ))}
- {excludedLabelIds.map((labelId) => (
-
-
-
- ))}
-
-
-
- ) : (
- 'search'
- )
- }
- className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
- onFocus={handleSearchFocus}
- onKeyDown={handleSearchKeyDown}
- onChange={handleSearchChange}
- onBlur={handleSearchBlur}
- />
-
- >
- );
-});
-
-export default Filters;
diff --git a/client/src/components/lists/ListsFilterStep/Item.jsx b/client/src/components/lists/ListsFilterStep/Item.jsx
new file mode 100644
index 00000000..5f1cb393
--- /dev/null
+++ b/client/src/components/lists/ListsFilterStep/Item.jsx
@@ -0,0 +1,62 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { Icon } from 'semantic-ui-react';
+
+import selectors from '../../../selectors';
+import { ListTypes } from '../../../constants/Enums';
+import { ListTypeIcons } from '../../../constants/Icons';
+
+import styles from './Item.module.scss';
+
+const Item = React.memo(({ id, isActive, onSelect, onDeselect }) => {
+ 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 9dab996f..3a0e0f95 100644
--- a/client/src/constants/ActionTypes.js
+++ b/client/src/constants/ActionTypes.js
@@ -300,6 +300,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 1e767bb5..0de8801c 100755
--- a/client/src/constants/EntryActionTypes.js
+++ b/client/src/constants/EntryActionTypes.js
@@ -198,6 +198,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 c236b747..09433d58 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -262,6 +262,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 06d07601..e69de29b 100755
--- a/client/src/models/Board.js
+++ b/client/src/models/Board.js
@@ -1,522 +0,0 @@
-/*!
- * Copyright (c) 2024 PLANKA Software GmbH
- * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
- */
-
-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, LabelFilterModes } from '../constants/Enums';
-
-const prepareFetchedBoard = (board) => ({
- ...board,
- isFetching: false,
- context: BoardContexts.BOARD,
- // Whatever this window was last looking at, where that choice still applies —
- // see `utils/board-view-memory`. A board's configured default is where it
- // starts, not where it has to stay.
- view: recallBoardView(board.id, BoardContexts.BOARD) || board.defaultView,
- search: '',
-});
-
-export default class extends BaseModel {
- static modelName = 'Board';
-
- static fields = {
- id: attr(),
- position: attr(),
- name: attr(),
- defaultView: attr(),
- defaultCardType: attr(),
- limitCardTypesToDefaultOne: attr(),
- alwaysDisplayCardCreator: attr(),
- displayCardAges: attr(),
- expandTaskListsByDefault: attr(),
- context: attr(),
- view: attr(),
- search: attr(),
- isSubscribed: attr({
- getDefault: () => false,
- }),
- isFetching: attr({
- getDefault: () => null,
- }),
- lastActivityId: attr({
- getDefault: () => null,
- }),
- isActivitiesFetching: attr({
- getDefault: () => false,
- }),
- isAllActivitiesFetched: attr({
- getDefault: () => null,
- }),
- projectId: fk({
- to: 'Project',
- as: 'project',
- relatedName: 'boards',
- }),
- memberUsers: many({
- to: 'User',
- through: 'BoardMembership',
- relatedName: 'boards',
- }),
- filterUsers: many('User', 'filterBoards'),
- filterLabels: many('Label', 'filterBoards'),
- filterExcludedLabels: many('Label', 'filterExcludedBoards'),
- filterNoMember: attr({
- getDefault: () => false,
- }),
- };
-
- static reducer({ type, payload }, Board) {
- switch (type) {
- case ActionTypes.LOCATION_CHANGE_HANDLE:
- if (payload.board) {
- Board.upsert(prepareFetchedBoard(payload.board));
- }
-
- break;
- case ActionTypes.LOCATION_CHANGE_HANDLE__BOARD_FETCH:
- case ActionTypes.BOARD_FETCH:
- Board.withId(payload.id).update({
- isFetching: true,
- });
-
- break;
- case ActionTypes.SOCKET_RECONNECT_HANDLE: {
- const boardIds = payload.boards.map(({ id }) => id);
-
- Board.all()
- .toModelArray()
- .forEach((boardModel) => {
- if (boardModel.isFetching === null || !boardIds.includes(boardModel.id)) {
- boardModel.deleteWithClearable();
- }
- });
-
- if (payload.board) {
- const boardModel = Board.withId(payload.board.id);
-
- if (boardModel) {
- boardModel.update(payload.board);
- } else {
- Board.upsert(prepareFetchedBoard(payload.board));
- }
- }
-
- payload.boards.forEach((board) => {
- Board.upsert(board);
- });
-
- break;
- }
- case ActionTypes.SOCKET_RECONNECT_HANDLE__CORE_FETCH:
- Board.all()
- .toModelArray()
- .forEach((boardModel) => {
- if (boardModel.id !== payload.currentBoardId) {
- boardModel.update({
- isFetching: null,
- });
-
- boardModel.deleteRelated(payload.currentUserId, true);
- }
- });
-
- break;
- case ActionTypes.CORE_INITIALIZE:
- if (payload.board) {
- Board.upsert(prepareFetchedBoard(payload.board));
- }
-
- payload.boards.forEach((board) => {
- Board.upsert(board);
- });
-
- break;
- case ActionTypes.USER_UPDATE_HANDLE:
- Board.all()
- .toModelArray()
- .forEach((boardModel) => {
- if (!payload.boardIds.includes(boardModel.id)) {
- boardModel.deleteWithRelated(true);
- }
- });
-
- if (payload.board) {
- Board.upsert(prepareFetchedBoard(payload.board));
- }
-
- if (payload.boards) {
- payload.boards.forEach((board) => {
- Board.upsert(board);
- });
- }
-
- break;
- case ActionTypes.USER_TO_BOARD_FILTER_ADD: {
- const boardModel = Board.withId(payload.boardId);
-
- if (payload.replace) {
- boardModel.filterUsers.clear();
- }
-
- boardModel.filterUsers.add(payload.id);
-
- break;
- }
- case ActionTypes.USER_FROM_BOARD_FILTER_REMOVE:
- Board.withId(payload.boardId).filterUsers.remove(payload.id);
-
- break;
- case ActionTypes.PROJECT_CREATE_HANDLE:
- payload.boards.forEach((board) => {
- Board.upsert(board);
- });
-
- break;
- case ActionTypes.PROJECT_UPDATE_HANDLE:
- case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
- case ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE:
- if (payload.board) {
- Board.upsert(prepareFetchedBoard(payload.board));
- }
-
- if (payload.boards) {
- payload.boards.forEach((board) => {
- Board.upsert(board);
- });
- }
-
- break;
- case ActionTypes.BOARD_CREATE:
- case ActionTypes.BOARD_CREATE_HANDLE:
- case ActionTypes.BOARD_UPDATE__SUCCESS:
- case ActionTypes.BOARD_UPDATE_HANDLE:
- Board.upsert(payload.board);
-
- break;
- case ActionTypes.BOARD_CREATE__SUCCESS:
- Board.withId(payload.localId).delete();
- Board.upsert(payload.board);
-
- break;
- case ActionTypes.BOARD_CREATE__FAILURE:
- Board.withId(payload.localId).delete();
-
- break;
- case ActionTypes.BOARD_FETCH__SUCCESS:
- Board.upsert(prepareFetchedBoard(payload.board));
-
- break;
- case ActionTypes.BOARD_FETCH__FAILURE:
- Board.withId(payload.id).update({
- isFetching: null,
- });
-
- break;
- case ActionTypes.BOARD_UPDATE:
- Board.withId(payload.id).update(payload.data);
-
- break;
- case ActionTypes.BOARD_CONTEXT_UPDATE: {
- const boardModel = Board.withId(payload.id);
-
- boardModel.update({
- context: payload.value,
- view: payload.value === BoardContexts.BOARD ? boardModel.defaultView : BoardViews.LIST,
- });
-
- break;
- }
- case ActionTypes.IN_BOARD_SEARCH:
- Board.withId(payload.id).update({
- search: payload.value,
- });
-
- break;
- case ActionTypes.BOARD_DELETE:
- Board.withId(payload.id).deleteWithRelated();
-
- break;
- case ActionTypes.BOARD_DELETE__SUCCESS:
- case ActionTypes.BOARD_DELETE_HANDLE: {
- const boardModel = Board.withId(payload.board.id);
-
- if (boardModel) {
- boardModel.deleteWithRelated();
- }
-
- break;
- }
- case ActionTypes.LABEL_TO_BOARD_FILTER_ADD:
- Board.withId(payload.boardId).filterLabels.add(payload.id);
-
- break;
- case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
- 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,
- });
-
- break;
- case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
- Board.withId(payload.boardId).update({
- isActivitiesFetching: false,
- isAllActivitiesFetched: payload.activities.length < Config.ACTIVITIES_LIMIT,
- ...(payload.activities.length > 0 && {
- lastActivityId: payload.activities[payload.activities.length - 1].id,
- }),
- });
-
- break;
- case ActionTypes.NO_MEMBER_TO_BOARD_FILTER_SET: {
- const boardModel = Board.withId(payload.boardId);
- boardModel.filterUsers.clear();
- boardModel.update({ filterNoMember: true });
- break;
- }
- case ActionTypes.NO_MEMBER_FROM_BOARD_FILTER_REMOVE: {
- const boardModel = Board.withId(payload.boardId);
- boardModel.update({ filterNoMember: false });
- break;
- }
- default:
- }
- }
-
- getMembershipsQuerySet() {
- return this.memberships.orderBy(['id.length', 'id']);
- }
-
- getLabelsQuerySet() {
- return this.labels.orderBy(['position', 'id.length', 'id']);
- }
-
- getListsQuerySet() {
- return this.lists.orderBy(['position', 'id.length', 'id']);
- }
-
- getKanbanListsQuerySet() {
- return this.getListsQuerySet().filter((list) => isListKanban(list));
- }
-
- getCustomFieldGroupsQuerySet() {
- return this.customFieldGroups.orderBy(['position', 'id.length', 'id']);
- }
-
- getActivitiesQuerySet() {
- return this.activities.orderBy(['id.length', 'id'], ['desc', 'desc']);
- }
-
- getUnreadNotificationsQuerySet() {
- return this.notifications.filter({
- isRead: false,
- });
- }
-
- getNotificationServicesQuerySet() {
- return this.notificationServices.orderBy(['id.length', 'id']);
- }
-
- getMembershipModelByUserId(userId) {
- return this.memberships
- .filter({
- userId,
- })
- .first();
- }
-
- getCardsModelArray() {
- return this.getKanbanListsQuerySet()
- .toModelArray()
- .flatMap((listModel) => listModel.getCardsModelArray());
- }
-
- getFilteredCardsModelArray() {
- let cardModels = this.getCardsModelArray();
-
- if (cardModels.length === 0) {
- return cardModels;
- }
-
- if (this.search) {
- if (this.search.startsWith('/')) {
- let searchRegex;
- try {
- searchRegex = new RegExp(this.search.substring(1), 'i');
- } catch {
- return [];
- }
-
- cardModels = cardModels.filter(
- (cardModel) =>
- searchRegex.test(cardModel.name) ||
- (cardModel.description && searchRegex.test(cardModel.description)),
- );
- } else {
- const searchParts = buildSearchParts(this.search);
-
- cardModels = cardModels.filter((cardModel) => {
- const name = cardModel.name.toLowerCase();
- const description = cardModel.description && cardModel.description.toLowerCase();
-
- return searchParts.every(
- (searchPart) =>
- name.includes(searchPart) || (description && description.includes(searchPart)),
- );
- });
- }
- }
-
- const filterUserIds = this.filterUsers.toRefArray().map((user) => user.id);
-
- if (filterUserIds.length > 0) {
- cardModels = cardModels.filter((cardModel) => {
- const users = cardModel.users.toRefArray();
-
- if (users.some((user) => filterUserIds.includes(user.id))) {
- return true;
- }
-
- return cardModel
- .getTaskListsQuerySet()
- .toModelArray()
- .some((taskListModel) =>
- taskListModel
- .getTasksQuerySet()
- .toRefArray()
- .some((task) => task.assigneeUserId && filterUserIds.includes(task.assigneeUserId)),
- );
- });
- }
-
- const filterLabelIds = this.filterLabels.toRefArray().map((label) => label.id);
- const filterExcludedLabelIds = this.filterExcludedLabels.toRefArray().map((label) => label.id);
-
- if (filterLabelIds.length > 0 || filterExcludedLabelIds.length > 0) {
- cardModels = filterCardLabels(cardModels, filterLabelIds, filterExcludedLabelIds);
- }
-
- if (this.filterNoMember) {
- cardModels = cardModels.filter((cardModel) => cardModel.users.toRefArray().length === 0);
- return cardModels;
- }
-
- return cardModels;
- }
-
- getActivitiesModelArray() {
- if (this.isAllActivitiesFetched === null) {
- return [];
- }
-
- const activityModels = this.getActivitiesQuerySet().toModelArray();
-
- if (this.lastActivityId && this.isAllActivitiesFetched === false) {
- return activityModels.filter((activityModel) => {
- if (activityModel.id.length > this.lastActivityId.length) {
- return true;
- }
-
- if (activityModel.id.length < this.lastActivityId.length) {
- return false;
- }
-
- return activityModel.id >= this.lastActivityId;
- });
- }
-
- return activityModels;
- }
-
- hasMembershipWithUserId(userId) {
- return this.memberships
- .filter({
- userId,
- })
- .exists();
- }
-
- isAvailableForUser(userModel) {
- if (!this.project) {
- return false;
- }
-
- return (
- this.project.isExternalAccessibleForUser(userModel) ||
- this.hasMembershipWithUserId(userModel.id)
- );
- }
-
- deleteListsWithRelated(soft) {
- this.lists.toModelArray().forEach((listModel) => {
- listModel.deleteWithRelated(soft);
- });
- }
-
- deleteClearable() {
- this.filterUsers.clear();
- this.filterLabels.clear();
- this.filterExcludedLabels.clear();
- }
-
- deleteRelated(exceptMemberUserId, soft) {
- this.deleteClearable();
-
- this.memberships.toModelArray().forEach((boardMembershipModel) => {
- if (boardMembershipModel.userId !== exceptMemberUserId) {
- boardMembershipModel.deleteWithRelated();
- }
- });
-
- this.labels.toModelArray().forEach((labelModel) => {
- labelModel.deleteWithRelated();
- });
-
- this.deleteListsWithRelated(soft);
- this.notificationServices.delete();
- }
-
- deleteWithClearable() {
- this.deleteClearable();
- this.delete();
- }
-
- deleteWithRelated(soft) {
- this.deleteRelated(undefined, soft);
- this.delete();
- }
-}
diff --git a/client/src/models/List.js b/client/src/models/List.js
index 49dda9f3..c28b2161 100755
--- a/client/src/models/List.js
+++ b/client/src/models/List.js
@@ -450,6 +450,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 2eac7163..160dd644 100644
--- a/client/src/selectors/boards.js
+++ b/client/src/selectors/boards.js
@@ -479,6 +479,24 @@ export const selectFilterExcludedLabelIdsForCurrentBoard = 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,
@@ -513,5 +531,6 @@ export default {
selectFilterUserIdsForCurrentBoard,
selectFilterLabelIdsForCurrentBoard,
selectFilterExcludedLabelIdsForCurrentBoard,
+ selectFilterListIdsForCurrentBoard,
selectIsBoardWithIdExists,
};