fix: Restore two files committed empty by the list filter merge
75fcddcd wrote `client/src/models/Board.js` and the board filter bar out as
zero bytes. `BoardActions` imports `Filters` from that module, so the client
could not render a board at all.
The conflict resolution there was scripted, and the script opened each file
for writing before reading it, which truncates it first and then reads back
nothing. ESLint accepts an empty module, so lint stayed green and the commit
looked fine.
Both files are rebuilt from the two merge parents. All six conflicts resolve
to keeping both sides, which is what the merge intended: excluded labels,
cards without a member and lists are three independent filters.
One behaviour fix rides along, because the union merge would otherwise have
carried it in. The no-member branch ended its block with `return cardModels`,
which was correct while it was the last filter and wrong once the list filter
followed it: combining the two would have dropped the list filter without a
word. The three now compose.
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
/*!
|
||||
* 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 PropTypes from 'prop-types';
|
||||
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 { BoardViews, LabelFilterModes } from '../../../constants/Enums';
|
||||
import UserAvatar from '../../users/UserAvatar';
|
||||
import BoardMembershipsStep from '../../board-memberships/BoardMembershipsStep';
|
||||
import LabelChip from '../../labels/LabelChip';
|
||||
import LabelsStep from '../../labels/LabelsStep';
|
||||
import ListsFilterStep from '../../lists/ListsFilterStep';
|
||||
|
||||
import styles from './Filters.module.scss';
|
||||
|
||||
const FilterListChip = React.memo(({ id, onClick }) => {
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
const list = useSelector((state) => selectListById(state, id));
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(id);
|
||||
}, [id, onClick]);
|
||||
|
||||
if (!list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className={styles.filterButton} onClick={handleClick}>
|
||||
<span className={styles.filterLabel}>{list.name || t(`common.${list.type}`)}</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
FilterListChip.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
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 listIds = useSelector(selectors.selectFilterListIdsForCurrentBoard);
|
||||
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 handleListSelect = useCallback(
|
||||
(listId) => {
|
||||
dispatch(entryActions.addListToFilterInCurrentBoard(listId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleListDeselect = useCallback(
|
||||
(listId) => {
|
||||
dispatch(entryActions.removeListFromFilterInCurrentBoard(listId));
|
||||
},
|
||||
[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 ListsFilterPopup = usePopup(ListsFilterStep);
|
||||
|
||||
const isSearchActive = search || isSearchFocused;
|
||||
const isListView = board.view === BoardViews.LIST;
|
||||
|
||||
const handleNoMemberClick = useCallback(() => {
|
||||
if (board.filterNoMember) {
|
||||
dispatch(entryActions.removeNoMemberFromFilterInCurrentBoard());
|
||||
} else {
|
||||
dispatch(entryActions.setNoMemberToFilterInCurrentBoard());
|
||||
}
|
||||
}, [dispatch, board.filterNoMember]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.filter}>
|
||||
<BoardMembershipsPopup
|
||||
currentUserIds={userIds}
|
||||
title="common.filterByMembers"
|
||||
onUserSelect={handleUserSelect}
|
||||
onUserDeselect={handleUserDeselect}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.members')}:`}</span>
|
||||
{userIds.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
|
||||
</button>
|
||||
</BoardMembershipsPopup>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(styles.filterButton, styles.filterLabel)}
|
||||
onClick={handleNoMemberClick}
|
||||
>
|
||||
{t('common.noMember')}
|
||||
</button>
|
||||
{userIds.length === 0 && withCurrentUserSelector && (
|
||||
<Tooltip content={t('action.filterByCurrentUser')}>
|
||||
<button type="button" className={styles.filterButton} onClick={handleCurrentUserSelect}>
|
||||
<span className={styles.filterLabel}>
|
||||
<Icon fitted name="target" className={styles.filterLabelIcon} />
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{userIds.map((userId) => (
|
||||
<span key={userId} className={styles.filterItem}>
|
||||
<UserAvatar id={userId} size="tiny" onClick={handleUserClick} />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className={styles.filter}>
|
||||
<LabelsPopup
|
||||
currentIds={[]}
|
||||
currentModes={labelModes}
|
||||
isFilterModeEnabled
|
||||
title="common.filterByLabels"
|
||||
onModeChange={handleLabelModeChange}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.labels')}:`}</span>
|
||||
{labelIds.length === 0 && excludedLabelIds.length === 0 && (
|
||||
<span className={styles.filterLabel}>{t('common.all')}</span>
|
||||
)}
|
||||
</button>
|
||||
</LabelsPopup>
|
||||
{labelIds.map((labelId) => (
|
||||
<span key={labelId} className={styles.filterItem}>
|
||||
<LabelChip id={labelId} size="small" onClick={handleLabelClick} />
|
||||
</span>
|
||||
))}
|
||||
{excludedLabelIds.map((labelId) => (
|
||||
<span key={labelId} className={styles.filterItem}>
|
||||
<LabelChip id={labelId} size="small" isExcluded onClick={handleLabelClick} />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{isListView && (
|
||||
<span className={styles.filter}>
|
||||
<ListsFilterPopup
|
||||
currentIds={listIds}
|
||||
title="common.filterByLists"
|
||||
onSelect={handleListSelect}
|
||||
onDeselect={handleListDeselect}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.lists')}:`}</span>
|
||||
{listIds.length === 0 && (
|
||||
<span className={styles.filterLabel}>{t('common.all')}</span>
|
||||
)}
|
||||
</button>
|
||||
</ListsFilterPopup>
|
||||
{listIds.map((listId) => (
|
||||
<FilterListChip key={listId} id={listId} onClick={handleListDeselect} />
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.filter}>
|
||||
<Input
|
||||
ref={handleSearchFieldRef}
|
||||
value={search}
|
||||
placeholder={t('common.searchCards')}
|
||||
maxLength={128}
|
||||
icon={
|
||||
isSearchActive ? (
|
||||
<Icon link name="cancel" onClick={handleCancelSearchClick} />
|
||||
) : (
|
||||
'search'
|
||||
)
|
||||
}
|
||||
className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
|
||||
onFocus={handleSearchFocus}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
onChange={handleSearchChange}
|
||||
onBlur={handleSearchBlur}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Filters;
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
/*!
|
||||
* 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,
|
||||
}),
|
||||
filterLists: many('List', 'filterBoards'),
|
||||
};
|
||||
|
||||
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.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({
|
||||
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);
|
||||
}
|
||||
|
||||
// The three filters below are independent of one another and compose: a
|
||||
// board can be narrowed to unassigned cards AND to a set of lists at the
|
||||
// same time, so neither one may return early.
|
||||
if (this.filterNoMember) {
|
||||
cardModels = cardModels.filter((cardModel) => cardModel.users.toRefArray().length === 0);
|
||||
}
|
||||
|
||||
const filterListIds = this.filterLists.toRefArray().map((list) => list.id);
|
||||
|
||||
if (filterListIds.length > 0) {
|
||||
cardModels = cardModels.filter((cardModel) => filterListIds.includes(cardModel.listId));
|
||||
}
|
||||
|
||||
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();
|
||||
this.filterLists.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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user