mirror of
https://github.com/plankanban/planka.git
synced 2025-07-22 14:49:43 +02:00
feat: Filter cards by keyword with advanced capabilities (#713)
Closes #706
This commit is contained in:
parent
8747aa59de
commit
eb56b2147b
19 changed files with 267 additions and 1 deletions
|
@ -121,6 +121,14 @@ const handleCardDelete = (card) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filterText = (boardId, text) => ({
|
||||||
|
type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
|
||||||
|
payload: {
|
||||||
|
boardId,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createCard,
|
createCard,
|
||||||
handleCardCreate,
|
handleCardCreate,
|
||||||
|
@ -129,4 +137,5 @@ export default {
|
||||||
duplicateCard,
|
duplicateCard,
|
||||||
deleteCard,
|
deleteCard,
|
||||||
handleCardDelete,
|
handleCardDelete,
|
||||||
|
filterText,
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,6 +13,7 @@ const BoardActions = React.memo(
|
||||||
labels,
|
labels,
|
||||||
filterUsers,
|
filterUsers,
|
||||||
filterLabels,
|
filterLabels,
|
||||||
|
filterText,
|
||||||
allUsers,
|
allUsers,
|
||||||
canEdit,
|
canEdit,
|
||||||
canEditMemberships,
|
canEditMemberships,
|
||||||
|
@ -27,6 +28,7 @@ const BoardActions = React.memo(
|
||||||
onLabelUpdate,
|
onLabelUpdate,
|
||||||
onLabelMove,
|
onLabelMove,
|
||||||
onLabelDelete,
|
onLabelDelete,
|
||||||
|
onTextFilterUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
|
@ -46,6 +48,7 @@ const BoardActions = React.memo(
|
||||||
<Filters
|
<Filters
|
||||||
users={filterUsers}
|
users={filterUsers}
|
||||||
labels={filterLabels}
|
labels={filterLabels}
|
||||||
|
filterText={filterText}
|
||||||
allBoardMemberships={memberships}
|
allBoardMemberships={memberships}
|
||||||
allLabels={labels}
|
allLabels={labels}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
|
@ -57,6 +60,7 @@ const BoardActions = React.memo(
|
||||||
onLabelUpdate={onLabelUpdate}
|
onLabelUpdate={onLabelUpdate}
|
||||||
onLabelMove={onLabelMove}
|
onLabelMove={onLabelMove}
|
||||||
onLabelDelete={onLabelDelete}
|
onLabelDelete={onLabelDelete}
|
||||||
|
onTextFilterUpdate={onTextFilterUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,6 +75,7 @@ BoardActions.propTypes = {
|
||||||
labels: PropTypes.array.isRequired,
|
labels: PropTypes.array.isRequired,
|
||||||
filterUsers: PropTypes.array.isRequired,
|
filterUsers: PropTypes.array.isRequired,
|
||||||
filterLabels: PropTypes.array.isRequired,
|
filterLabels: PropTypes.array.isRequired,
|
||||||
|
filterText: PropTypes.string.isRequired,
|
||||||
allUsers: PropTypes.array.isRequired,
|
allUsers: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
canEdit: PropTypes.bool.isRequired,
|
canEdit: PropTypes.bool.isRequired,
|
||||||
|
@ -86,6 +91,7 @@ BoardActions.propTypes = {
|
||||||
onLabelUpdate: PropTypes.func.isRequired,
|
onLabelUpdate: PropTypes.func.isRequired,
|
||||||
onLabelMove: PropTypes.func.isRequired,
|
onLabelMove: PropTypes.func.isRequired,
|
||||||
onLabelDelete: PropTypes.func.isRequired,
|
onLabelDelete: PropTypes.func.isRequired,
|
||||||
|
onTextFilterUpdate: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BoardActions;
|
export default BoardActions;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Icon } from 'semantic-ui-react';
|
||||||
import { usePopup } from '../../lib/popup';
|
import { usePopup } from '../../lib/popup';
|
||||||
|
import { Input } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import User from '../User';
|
import User from '../User';
|
||||||
import Label from '../Label';
|
import Label from '../Label';
|
||||||
|
@ -14,6 +17,7 @@ const Filters = React.memo(
|
||||||
({
|
({
|
||||||
users,
|
users,
|
||||||
labels,
|
labels,
|
||||||
|
filterText,
|
||||||
allBoardMemberships,
|
allBoardMemberships,
|
||||||
allLabels,
|
allLabels,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
@ -25,8 +29,17 @@ const Filters = React.memo(
|
||||||
onLabelUpdate,
|
onLabelUpdate,
|
||||||
onLabelMove,
|
onLabelMove,
|
||||||
onLabelDelete,
|
onLabelDelete,
|
||||||
|
onTextFilterUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||||
|
|
||||||
|
const searchFieldRef = useRef(null);
|
||||||
|
|
||||||
|
const cancelSearch = useCallback(() => {
|
||||||
|
onTextFilterUpdate('');
|
||||||
|
searchFieldRef.current.blur();
|
||||||
|
}, [onTextFilterUpdate]);
|
||||||
|
|
||||||
const handleRemoveUserClick = useCallback(
|
const handleRemoveUserClick = useCallback(
|
||||||
(id) => {
|
(id) => {
|
||||||
|
@ -42,9 +55,39 @@ const Filters = React.memo(
|
||||||
[onLabelRemove],
|
[onLabelRemove],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSearchChange = useCallback(
|
||||||
|
(_, { value }) => {
|
||||||
|
onTextFilterUpdate(value);
|
||||||
|
},
|
||||||
|
[onTextFilterUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
|
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
|
||||||
const LabelsPopup = usePopup(LabelsStep);
|
const LabelsPopup = usePopup(LabelsStep);
|
||||||
|
|
||||||
|
const isSearchActive = filterText || isSearchFocused;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className={styles.filter}>
|
<span className={styles.filter}>
|
||||||
|
@ -100,6 +143,25 @@ const Filters = React.memo(
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
|
<span className={styles.filter}>
|
||||||
|
<Input
|
||||||
|
ref={searchFieldRef}
|
||||||
|
value={filterText}
|
||||||
|
placeholder={t('common.searchCards')}
|
||||||
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -109,6 +171,7 @@ Filters.propTypes = {
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
users: PropTypes.array.isRequired,
|
users: PropTypes.array.isRequired,
|
||||||
labels: PropTypes.array.isRequired,
|
labels: PropTypes.array.isRequired,
|
||||||
|
filterText: PropTypes.string.isRequired,
|
||||||
allBoardMemberships: PropTypes.array.isRequired,
|
allBoardMemberships: PropTypes.array.isRequired,
|
||||||
allLabels: PropTypes.array.isRequired,
|
allLabels: PropTypes.array.isRequired,
|
||||||
/* eslint-enable react/forbid-prop-types */
|
/* eslint-enable react/forbid-prop-types */
|
||||||
|
@ -121,6 +184,7 @@ Filters.propTypes = {
|
||||||
onLabelUpdate: PropTypes.func.isRequired,
|
onLabelUpdate: PropTypes.func.isRequired,
|
||||||
onLabelMove: PropTypes.func.isRequired,
|
onLabelMove: PropTypes.func.isRequired,
|
||||||
onLabelDelete: PropTypes.func.isRequired,
|
onLabelDelete: PropTypes.func.isRequired,
|
||||||
|
onTextFilterUpdate: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Filters;
|
export default Filters;
|
||||||
|
|
|
@ -43,4 +43,32 @@
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
padding: 2px 12px;
|
padding: 2px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
height: 30px;
|
||||||
|
margin: 0 12px;
|
||||||
|
transition: width 0.2s ease;
|
||||||
|
width: 280px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInactive {
|
||||||
|
color: #fff;
|
||||||
|
height: 24px;
|
||||||
|
width: 220px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: rgba(0, 0, 0, 0.24);
|
||||||
|
border: none;
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,6 +205,7 @@ export default {
|
||||||
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
|
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
|
||||||
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
|
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
|
||||||
CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE',
|
CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE',
|
||||||
|
TEXT_FILTER_IN_CURRENT_BOARD: 'TEXT_FILTER_IN_CURRENT_BOARD',
|
||||||
|
|
||||||
/* Tasks */
|
/* Tasks */
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,7 @@ export default {
|
||||||
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
|
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
|
||||||
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
|
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
|
||||||
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
|
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
|
||||||
|
TEXT_FILTER_IN_CURRENT_BOARD: `${PREFIX}/FILTER_TEXT_HANDLE`,
|
||||||
|
|
||||||
/* Tasks */
|
/* Tasks */
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ const mapStateToProps = (state) => {
|
||||||
const labels = selectors.selectLabelsForCurrentBoard(state);
|
const labels = selectors.selectLabelsForCurrentBoard(state);
|
||||||
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
|
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
|
||||||
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
|
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
|
||||||
|
const filterText = selectors.selectFilterTextForCurrentBoard(state);
|
||||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||||
|
|
||||||
const isCurrentUserEditor =
|
const isCurrentUserEditor =
|
||||||
|
@ -23,6 +24,7 @@ const mapStateToProps = (state) => {
|
||||||
labels,
|
labels,
|
||||||
filterUsers,
|
filterUsers,
|
||||||
filterLabels,
|
filterLabels,
|
||||||
|
filterText,
|
||||||
allUsers,
|
allUsers,
|
||||||
canEdit: isCurrentUserEditor,
|
canEdit: isCurrentUserEditor,
|
||||||
canEditMemberships: isCurrentUserManager,
|
canEditMemberships: isCurrentUserManager,
|
||||||
|
@ -43,6 +45,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||||
onLabelUpdate: entryActions.updateLabel,
|
onLabelUpdate: entryActions.updateLabel,
|
||||||
onLabelMove: entryActions.moveLabel,
|
onLabelMove: entryActions.moveLabel,
|
||||||
onLabelDelete: entryActions.deleteLabel,
|
onLabelDelete: entryActions.deleteLabel,
|
||||||
|
onTextFilterUpdate: entryActions.filterText,
|
||||||
},
|
},
|
||||||
dispatch,
|
dispatch,
|
||||||
);
|
);
|
||||||
|
|
|
@ -105,6 +105,13 @@ const handleCardDelete = (card) => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filterText = (text) => ({
|
||||||
|
type: EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
|
||||||
|
payload: {
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createCard,
|
createCard,
|
||||||
handleCardCreate,
|
handleCardCreate,
|
||||||
|
@ -120,4 +127,5 @@ export default {
|
||||||
deleteCard,
|
deleteCard,
|
||||||
deleteCurrentCard,
|
deleteCurrentCard,
|
||||||
handleCardDelete,
|
handleCardDelete,
|
||||||
|
filterText,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,4 +9,6 @@ export default class Input extends SemanticUIInput {
|
||||||
static Mask = InputMask;
|
static Mask = InputMask;
|
||||||
|
|
||||||
focus = (options) => this.inputRef.current.focus(options);
|
focus = (options) => this.inputRef.current.focus(options);
|
||||||
|
|
||||||
|
blur = () => this.inputRef.current.blur();
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,7 @@ export default {
|
||||||
searchLabels: 'Search labels...',
|
searchLabels: 'Search labels...',
|
||||||
searchMembers: 'Search members...',
|
searchMembers: 'Search members...',
|
||||||
searchUsers: 'Search users...',
|
searchUsers: 'Search users...',
|
||||||
|
searchCards: 'Search cards...',
|
||||||
seconds: 'Seconds',
|
seconds: 'Seconds',
|
||||||
selectBoard: 'Select board',
|
selectBoard: 'Select board',
|
||||||
selectList: 'Select list',
|
selectList: 'Select list',
|
||||||
|
|
|
@ -111,6 +111,10 @@ export default {
|
||||||
project: 'Projet',
|
project: 'Projet',
|
||||||
projectNotFound_title: 'Projet introuvable',
|
projectNotFound_title: 'Projet introuvable',
|
||||||
removeMember_title: 'Supprimer le membre',
|
removeMember_title: 'Supprimer le membre',
|
||||||
|
searchLabels: 'Chercher une étiquette...',
|
||||||
|
searchMembers: 'Chercher un membre...',
|
||||||
|
searchUsers: 'Chercher un utilisateur...',
|
||||||
|
searchCards: 'Chercher une carte...',
|
||||||
seconds: 'Secondes',
|
seconds: 'Secondes',
|
||||||
selectBoard: 'Sélectionner une carte',
|
selectBoard: 'Sélectionner une carte',
|
||||||
selectList: 'Sélectionner une liste',
|
selectList: 'Sélectionner une liste',
|
||||||
|
|
|
@ -3,6 +3,9 @@ import { attr, fk, many } from 'redux-orm';
|
||||||
import BaseModel from './BaseModel';
|
import BaseModel from './BaseModel';
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
|
import User from './User';
|
||||||
|
import Label from './Label';
|
||||||
|
|
||||||
export default class extends BaseModel {
|
export default class extends BaseModel {
|
||||||
static modelName = 'Board';
|
static modelName = 'Board';
|
||||||
|
|
||||||
|
@ -25,6 +28,9 @@ export default class extends BaseModel {
|
||||||
}),
|
}),
|
||||||
filterUsers: many('User', 'filterBoards'),
|
filterUsers: many('User', 'filterBoards'),
|
||||||
filterLabels: many('Label', 'filterBoards'),
|
filterLabels: many('Label', 'filterBoards'),
|
||||||
|
filterText: attr({
|
||||||
|
getDefault: () => '',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
static reducer({ type, payload }, Board) {
|
static reducer({ type, payload }, Board) {
|
||||||
|
@ -167,6 +173,47 @@ export default class extends BaseModel {
|
||||||
Board.withId(payload.boardId).filterLabels.remove(payload.id);
|
Board.withId(payload.boardId).filterLabels.remove(payload.id);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD: {
|
||||||
|
const board = Board.withId(payload.boardId);
|
||||||
|
let filterText = payload.text;
|
||||||
|
const posSpace = filterText.indexOf(' ');
|
||||||
|
|
||||||
|
// Shortcut to user filters
|
||||||
|
const posAT = filterText.indexOf('@');
|
||||||
|
if (posAT >= 0 && posSpace > 0 && posAT < posSpace) {
|
||||||
|
const userId = User.findUsersFromText(
|
||||||
|
filterText.substring(posAT + 1, posSpace),
|
||||||
|
board.memberships.toModelArray().map((membership) => membership.user),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
userId &&
|
||||||
|
board.filterUsers.toModelArray().filter((user) => user.id === userId).length === 0
|
||||||
|
) {
|
||||||
|
board.filterUsers.add(userId);
|
||||||
|
filterText = filterText.substring(0, posAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcut to label filters
|
||||||
|
const posSharp = filterText.indexOf('#');
|
||||||
|
if (posSharp >= 0 && posSpace > 0 && posSharp < posSpace) {
|
||||||
|
const labelId = Label.findLabelsFromText(
|
||||||
|
filterText.substring(posSharp + 1, posSpace),
|
||||||
|
board.labels.toModelArray(),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
labelId &&
|
||||||
|
board.filterLabels.toModelArray().filter((label) => label.id === labelId).length === 0
|
||||||
|
) {
|
||||||
|
board.filterLabels.add(labelId);
|
||||||
|
filterText = filterText.substring(0, posSharp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
board.update({ filterText });
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,11 @@ export default class extends BaseModel {
|
||||||
position: attr(),
|
position: attr(),
|
||||||
name: attr(),
|
name: attr(),
|
||||||
description: attr(),
|
description: attr(),
|
||||||
|
creatorUserId: oneToOne({
|
||||||
|
to: 'User',
|
||||||
|
as: 'creatorUser',
|
||||||
|
relatedName: 'ownCards',
|
||||||
|
}),
|
||||||
dueDate: attr(),
|
dueDate: attr(),
|
||||||
stopwatch: attr(),
|
stopwatch: attr(),
|
||||||
isSubscribed: attr({
|
isSubscribed: attr({
|
||||||
|
|
|
@ -80,4 +80,16 @@ export default class extends BaseModel {
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static findLabelsFromText(filterText, labels) {
|
||||||
|
const selectLabel = filterText.toLocaleLowerCase();
|
||||||
|
const matchingLabels = labels.filter((label) =>
|
||||||
|
label.name ? label.name.toLocaleLowerCase().startsWith(selectLabel) : false,
|
||||||
|
);
|
||||||
|
if (matchingLabels.length === 1) {
|
||||||
|
// Appens the user to the filter
|
||||||
|
return matchingLabels[0].id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { attr, fk } from 'redux-orm';
|
import { attr, fk } from 'redux-orm';
|
||||||
|
|
||||||
import BaseModel from './BaseModel';
|
import BaseModel from './BaseModel';
|
||||||
|
import User from './User';
|
||||||
import ActionTypes from '../constants/ActionTypes';
|
import ActionTypes from '../constants/ActionTypes';
|
||||||
|
|
||||||
export default class extends BaseModel {
|
export default class extends BaseModel {
|
||||||
|
@ -89,6 +90,37 @@ export default class extends BaseModel {
|
||||||
getFilteredOrderedCardsModelArray() {
|
getFilteredOrderedCardsModelArray() {
|
||||||
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
|
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
|
||||||
|
|
||||||
|
const { filterText } = this.board;
|
||||||
|
|
||||||
|
if (filterText !== '') {
|
||||||
|
let re = null;
|
||||||
|
const posSpace = filterText.indexOf(' ');
|
||||||
|
|
||||||
|
if (filterText.startsWith('/')) {
|
||||||
|
re = new RegExp(filterText.substring(1), 'i');
|
||||||
|
}
|
||||||
|
let doRegularSearch = true;
|
||||||
|
if (re) {
|
||||||
|
cardModels = cardModels.filter((cardModel) => re.test(cardModel.name));
|
||||||
|
doRegularSearch = false;
|
||||||
|
} else if (filterText.startsWith('!') && posSpace > 0) {
|
||||||
|
const creatorUserId = User.findUsersFromText(
|
||||||
|
filterText.substring(1, posSpace),
|
||||||
|
this.board.memberships.toModelArray().map((membership) => membership.user),
|
||||||
|
);
|
||||||
|
if (creatorUserId != null) {
|
||||||
|
doRegularSearch = false;
|
||||||
|
cardModels = cardModels.filter((cardModel) => cardModel.creatorUser.id === creatorUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (doRegularSearch) {
|
||||||
|
const lowerCasedFilter = filterText.toLocaleLowerCase();
|
||||||
|
cardModels = cardModels.filter(
|
||||||
|
(cardModel) => cardModel.name.toLocaleLowerCase().indexOf(lowerCasedFilter) >= 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
|
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
|
||||||
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.id);
|
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.id);
|
||||||
|
|
||||||
|
|
|
@ -359,4 +359,18 @@ export default class extends BaseModel {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static findUsersFromText(filterText, users) {
|
||||||
|
const selectUser = filterText.toLocaleLowerCase();
|
||||||
|
const matchingUsers = users.filter(
|
||||||
|
(user) =>
|
||||||
|
user.name.toLocaleLowerCase().startsWith(selectUser) ||
|
||||||
|
user.username.toLocaleLowerCase().startsWith(selectUser),
|
||||||
|
);
|
||||||
|
if (matchingUsers.length === 1) {
|
||||||
|
// Appens the user to the filter
|
||||||
|
return matchingUsers[0].id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,6 +207,12 @@ export function* handleCardDelete(card) {
|
||||||
yield put(actions.handleCardDelete(card));
|
yield put(actions.handleCardDelete(card));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* handleTextFilter(text) {
|
||||||
|
const { boardId } = yield select(selectors.selectPath);
|
||||||
|
|
||||||
|
yield put(actions.filterText(boardId, text));
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createCard,
|
createCard,
|
||||||
handleCardCreate,
|
handleCardCreate,
|
||||||
|
@ -222,4 +228,5 @@ export default {
|
||||||
deleteCard,
|
deleteCard,
|
||||||
deleteCurrentCard,
|
deleteCurrentCard,
|
||||||
handleCardDelete,
|
handleCardDelete,
|
||||||
|
handleTextFilter,
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,5 +39,8 @@ export default function* cardsWatchers() {
|
||||||
takeEvery(EntryActionTypes.CARD_DELETE_HANDLE, ({ payload: { card } }) =>
|
takeEvery(EntryActionTypes.CARD_DELETE_HANDLE, ({ payload: { card } }) =>
|
||||||
services.handleCardDelete(card),
|
services.handleCardDelete(card),
|
||||||
),
|
),
|
||||||
|
takeEvery(EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD, ({ payload: { text } }) =>
|
||||||
|
services.handleTextFilter(text),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,6 +175,24 @@ export const selectFilterLabelsForCurrentBoard = createSelector(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const selectFilterTextForCurrentBoard = createSelector(
|
||||||
|
orm,
|
||||||
|
(state) => selectPath(state).boardId,
|
||||||
|
({ Board }, id) => {
|
||||||
|
if (!id) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boardModel = Board.withId(id);
|
||||||
|
|
||||||
|
if (!boardModel) {
|
||||||
|
return boardModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return boardModel.filterText;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const selectIsBoardWithIdExists = createSelector(
|
export const selectIsBoardWithIdExists = createSelector(
|
||||||
orm,
|
orm,
|
||||||
(_, id) => id,
|
(_, id) => id,
|
||||||
|
@ -191,5 +209,6 @@ export default {
|
||||||
selectListIdsForCurrentBoard,
|
selectListIdsForCurrentBoard,
|
||||||
selectFilterUsersForCurrentBoard,
|
selectFilterUsersForCurrentBoard,
|
||||||
selectFilterLabelsForCurrentBoard,
|
selectFilterLabelsForCurrentBoard,
|
||||||
|
selectFilterTextForCurrentBoard,
|
||||||
selectIsBoardWithIdExists,
|
selectIsBoardWithIdExists,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue